├── .gitmodules ├── .yardopts ├── spec ├── openssl │ ├── plaintext.txt │ ├── plaintext.aes │ ├── plaintext.crypted │ ├── public.pem │ └── private.pem ├── spec_helper.rb ├── digest_spec.rb ├── hmac_spec.rb ├── aes_benchmark.rb ├── fixtures │ └── secret.txt ├── rsa_spec.rb └── aes_spec.rb ├── .gitignore ├── .travis.yml ├── lib ├── gibberish │ ├── version.rb │ ├── hmac.rb │ ├── rsa.rb │ ├── digest.rb │ └── aes.rb └── gibberish.rb ├── Gemfile ├── Makefile ├── Rakefile ├── gibberish.gemspec ├── LICENSE ├── CHANGELOG.mdown └── README.markdown /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --output-dir doc 2 | --markup markdown 3 | -------------------------------------------------------------------------------- /spec/openssl/plaintext.txt: -------------------------------------------------------------------------------- 1 | This is in plaintext!!! 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .yardoc* 2 | *.gem 3 | Gemfile.lock 4 | /doc 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.6 4 | - 2.1 5 | - 2.0 6 | -------------------------------------------------------------------------------- /lib/gibberish/version.rb: -------------------------------------------------------------------------------- 1 | module Gibberish 2 | VERSION = "2.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/openssl/plaintext.aes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdp/gibberish/HEAD/spec/openssl/plaintext.aes -------------------------------------------------------------------------------- /spec/openssl/plaintext.crypted: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdp/gibberish/HEAD/spec/openssl/plaintext.crypted -------------------------------------------------------------------------------- /lib/gibberish.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | require 'base64' 3 | require 'gibberish/aes' 4 | require 'gibberish/digest' 5 | require 'gibberish/hmac' 6 | require 'gibberish/rsa' 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | group :development, :test do 4 | gem 'yard' 5 | gem 'minitest' 6 | gem 'mini_shoulda' 7 | gem 'rake' 8 | end 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: install test docs 2 | 3 | install: 4 | bundle install; 5 | 6 | test: 7 | bundle exec rake; 8 | 9 | benchmark: 10 | ruby spec/*_benchmark.rb; 11 | 12 | docs: 13 | rm -rf doc; 14 | bundle exec yard; 15 | 16 | .PHONY: install test docs benchmark 17 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | gem 'minitest' # ensures you're using the gem, and not the built in MT 4 | require 'minitest/autorun' 5 | 6 | require 'gibberish' 7 | 8 | print "Ruby version #{RUBY_VERSION} - OpenSSL version: #{OpenSSL::OPENSSL_VERSION}\n" 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | desc "run specs" 2 | task :spec do 3 | $LOAD_PATH.unshift '.', 'spec', 'lib' 4 | require 'spec/spec_helper' 5 | Minitest.autorun 6 | Dir.glob('spec/**/*_spec.rb') do |file| 7 | load file 8 | end 9 | end 10 | 11 | task :test => :spec 12 | task :default => [:test] 13 | -------------------------------------------------------------------------------- /spec/openssl/public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApkrxFVDdfsB1OnOg6CjD 3 | 3FX/uuYf3bmdiVfzBkNJvZruqCALjesAu4uGrbmBGhe/qyKYQHPzY2KwqpfwtXft 4 | 61QFtDdZVTjPzi5iqZcpUIYD6sMdJPH0E8YBADAMPyvKbWtjsLZADxRhKQN3Dwb0 5 | DTaq0UFVzq/40YZMFzXH9SIWH/4qJnCsqC24DQM9f8mKAGSC5KCReJIzuXorT0RI 6 | 9ghrwqr8EnglFWT51vcDjR80EfZusvLlep378AmTgYpf9Qxq7BRBx2wp8jWnxsV+ 7 | xYHuruJbZeqIs4FM2dRLLcMlA8Ky6IrViAR+wfy0CyK/ZGdozvXSOV8lh7gspwBh 8 | KQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /gibberish.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "gibberish/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "gibberish" 7 | s.version = Gibberish::VERSION 8 | s.platform = Gem::Platform::RUBY 9 | s.authors = ["Mark Percival"] 10 | s.email = ["m@mdp.im"] 11 | s.homepage = "http://github.com/mdp/gibberish" 12 | s.summary = %q{An opinionated ruby encryption library} 13 | s.description = %q{Supports SJCL compatible AES encryption, HMAC, and Digests} 14 | s.required_ruby_version = '>= 2.0.0' 15 | s.license = "MIT" 16 | 17 | s.rubyforge_project = "gibberish" 18 | 19 | s.files = `git ls-files`.split("\n") 20 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 21 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 22 | s.require_paths = ["lib"] 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Mark Percival 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /spec/digest_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "A variety of digest methods" do 4 | 5 | it "should work with MD5" do 6 | Gibberish::MD5("password").must_equal("5f4dcc3b5aa765d61d8327deb882cf99") 7 | end 8 | 9 | it "should work with SHA1" do 10 | Gibberish::SHA1("password").must_equal("5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8") 11 | end 12 | 13 | it "should work with SHA224" do 14 | Gibberish::SHA224("password").must_equal("d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01") 15 | end 16 | 17 | it "should work with SHA256" do 18 | Gibberish::SHA256("password").must_equal("5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8") 19 | end 20 | 21 | it "should work with SHA384" do 22 | Gibberish::SHA384("password").must_equal("a8b64babd0aca91a59bdbb7761b421d4f2bb38280d3a75ba0f21f2bebc45583d446c598660c94ce680c47d19c30783a7") 23 | end 24 | 25 | it "should work with SHA512" do 26 | Gibberish::SHA512("password").must_equal("b109f3bbbc244eb82441917ed06d618b9008dd09b3befd1b5e07394c706a8bb980b1d7785e5976ec049b46df5f1326af5a2ea6d103fd07c95385ffab0cacbc86") 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /CHANGELOG.mdown: -------------------------------------------------------------------------------- 1 | ### v2.1.1 2 | * Fix deprection warning on OpenSSL::Cipher [PR#27](https://github.com/mdp/gibberish/pull/27) 3 | 4 | ### v2.1.0 5 | * Improve exception handling [PR#25](https://github.com/mdp/gibberish/pull/25) 6 | 7 | ### v2.0.0 8 | * Breaking changes to default AES mode 9 | - Moving to [SJCL](http://bitwiseshiftleft.github.io/sjcl/) compatible AES 10 | - AES now uses only authenticated modes ("GCM" and "CCM") 11 | - Deprecating streaming and file AES encryption. This would be better handled 12 | by another library. Gibberish will focus on compatibility with Javascript/Browser encryption. 13 | - 1.x CBC encryption/decryption is still available. You can still encrypt and decrypt 14 | older Gibberish generated AES ciphertexts, but you must call it explicitly 15 | * Breaking change to HMAC - User must explicitly choose a digest. No longer defaults to SHA1 16 | * Deprecating Ruby 1.9.3 since its OpenSSL bindings do not allow for authenticated modes 17 | 18 | ### v1.4.0 19 | * Fix deprecation. Support for 1.8.7 is deprecated, so bumping minor rev [PR #15](https://github.com/mdp/gibberish/pull/15) 20 | 21 | ### v1.3.1 22 | * Better errors [PR #14](https://github.com/mdp/gibberish/pull/14) 23 | 24 | ### v1.3.0 25 | * Add support for file encryption [PR #12](https://github.com/mdp/gibberish/pull/12) 26 | * Add support for streaming encryption [PR #11](https://github.com/mdp/gibberish/pull/11) 27 | 28 | ### v1.2.2 29 | * Added support for EBC mode on AES [PR #10](https://github.com/mdp/gibberish/pull/10) 30 | 31 | ### v1.2.1 32 | * Added support for an optional salt - PR mdp/gibberish#9 33 | * Adding gem signing 34 | 35 | ### v1.2.0 36 | * Added digest support for SHA224/384 and HMAC SHA224/384/512. [Pull request #5](https://github.com/mdp/gibberish/pull/5) 37 | 38 | ### v1.1.0 39 | 40 | * Add SHA512 support 41 | -------------------------------------------------------------------------------- /spec/openssl/private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: DES-EDE3-CBC,9DFA13921C765D75 4 | 5 | as7z49axWP7a0p44YlnLwiY9di9BOjSJo98z/Ld0V02p1BHPBElbwIrxcPbAix+I 6 | 0sl43NtkVlHkiLJR9ZKVWqF8Uly9PCicqKRliPHBw1hiVQuejrmv+6Zx3Aktdunm 7 | mdXnwa8TVgpSHZcSUynvwp6nfJOtPAmAQLKRFyrOrNRDfLBqZ0Ha5l1+9MhzZxW6 8 | j6EFw5gOzvcR35ghBN6nCD/UNArh9mKPJFjv6Pfkcwlqxiy7OqHS7lPwN+7WnrFf 9 | 7jyLEAAaOGQxWjtfb9/3A0sjlrfn7j0uO9I/A6ogOd7wcIaBwy0qtGQ3bsj8ctZw 10 | 6cHWvSiExLDLdxARa1wROqLM5da3eD/W7BvA+9hPuAJxz6iDWPNWHj0IrsGWiHtF 11 | dGDbbSyg81/tPKbR0DNpl1bn0ft9h7oQWiM9uubNFCFdLrjJ+z2hMW52PHu89C1f 12 | swaG+CgB0754eNoBSNm2z4OaBebA8zbRP+8eqOs/ZDCSK4w2e0OFR2T51eEDDsg6 13 | Q7WHBg1Hq9tBTb/xwJ1u/CzzbHcIksnpE5fiHr0Xs/HMdTYXylcDwqN5df2He7FH 14 | PyAK41lH4K9MZVpIsWu/6gWk2Nt7UAkG9O/d/el0qMdtWNdJjxP/yFgWi1/zUBH+ 15 | jqavta/jxbEvqVycL+dU0tZSnukarmziMLg3644WW6AVtUeEScO9DibBYdMDIlgg 16 | m+2oOW90uzgdM236OurBQ240TzJYKRu60uOfjjbYzGw9MM4rwHXx3vk3plOw/vpe 17 | 827LBvSiw5WSD5wwR5nOpkDkMiNFvCO1JoSYkPYo/xy3yhps3Ea2PtnsoA5VcJRA 18 | VM0L8ad/taubVqVjDKyMggD6ynB9D0X+SYxbW/UAEKgHbEt6eSRF/egbRhDkt6zl 19 | im75EuZpres+5rlAi2X+yOwkHGDh3vDRk0Xmr+Eb5VfY+NQQFJy7sDYXKAEy0lsO 20 | wEsmtxam5u1FNVsFALKXTP2HPn+2fzH25ovA/ujXdlD9Q7LgtQpe+N1LjrK2SzWE 21 | hKmZZznVVSpDYsIEk95N/yPuDb57+N+UcesasekFkZu/14qseeIZRXYNEgQQqNsS 22 | wFJua9A4pcfSefSvMMa43ZFXtc+lRi8oCy0X0nN+Gs/f5LzxKexMfJLoXxORLq1Q 23 | s2zOQnyToHV52cGmE5stuWU1K2XIr8LLv/5nqDLscGZcJ/SWBwth1Lrpw/eLHxEY 24 | ltzsJzyShsuXFjAIgroKeGpv37D+ekqjVsTOPGtPJ04Q8kX34qTDQA6BvJ8kRQba 25 | 4hGNtlJlLFwpAIEd43VFkgsiACj2DuqeNGwvtSGkopq0Y5pfSKniezxuezekAn80 26 | uIGIT1slYrUm8w6qQOka64W2yE/YaV3lv8HH2eQQ9XmRI4UgFQzSAk1sdOmJadzW 27 | EF55t1GJN0BLyg8v/gmk+N3Tj83ZcxbY4fbQGaroOS83HhzLPoaT05PBqChSYhBo 28 | 97YKZPLDvxfbMjM5XyltNNEqZyKvR/9FIGvNWNT7uwEThqUIQ+OWiaRLo7y2duq4 29 | CvPwa8FkA0geEX5RTA05CuxuHriRrWtyoYhtNK2ZJ5pEuRoocP4jaw== 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /spec/hmac_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "HMAC" do 4 | 5 | it "should just work and default to sha256" do 6 | Gibberish::HMAC256("password", "data").must_equal( 7 | "cccf6f0334130a7010d62332c75b53e7d8cea715e52692b06e9cd41b05644be3") 8 | end 9 | 10 | it "should work with OpenSSL HMAC" do 11 | hmac = Gibberish::HMAC256("password", "data\n") 12 | o_hmac = `echo "data" | openssl dgst -sha256 -hmac 'password' | sed 's/^.*= //'` 13 | hmac.must_equal(o_hmac.chomp) 14 | end 15 | 16 | it "should hopefully work for sha224" do 17 | Gibberish::HMAC224("password", "data").must_equal( 18 | "f66aa39e91d003f7d3fc1205f77bd4947af51735a49e197fbd478728") 19 | end 20 | 21 | it "should work with OpenSSL HMAC for sha224" do 22 | hmac = Gibberish::HMAC224("password", "data\n") 23 | o_hmac = `echo "data" | openssl dgst -sha224 -hmac 'password' | sed 's/^.*= //'` 24 | hmac.must_equal(o_hmac.chomp) 25 | end 26 | 27 | it "should hopefully work for sha256" do 28 | Gibberish::HMAC256("password", "data").must_equal( 29 | "cccf6f0334130a7010d62332c75b53e7d8cea715e52692b06e9cd41b05644be3") 30 | end 31 | 32 | it "should work with OpenSSL HMAC for sha256" do 33 | hmac = Gibberish::HMAC256("password", "data\n") 34 | o_hmac = `echo "data" | openssl dgst -sha256 -hmac 'password' | sed 's/^.*= //'` 35 | hmac.must_equal(o_hmac.chomp) 36 | end 37 | 38 | it "should work for sha384" do 39 | Gibberish::HMAC384("password", "data").must_equal( 40 | "2ed475691214fb85d086577d8d525c609b92520ebd793a74856b3ffd8d3477eaaf0b06ef9e06c8aa81cf29f95078aca6") 41 | end 42 | 43 | it "should work with OpenSSL HMAC for sha384" do 44 | hmac = Gibberish::HMAC384("password", "data\n") 45 | o_hmac = `echo "data" | openssl dgst -sha384 -hmac 'password' | sed 's/^.*= //'` 46 | hmac.must_equal(o_hmac.chomp) 47 | end 48 | 49 | it "should hopefully work for sha512" do 50 | Gibberish::HMAC512("password", "data").must_equal("abf85192282b501874f4803ea08672f2c9d6e656c57801023a0b1f4dd9492ba960efdb560a8618ec783327d6dc31577422651a4cf7eaf722d2caefbc04038c6e") 51 | end 52 | 53 | it "should work with OpenSSL HMAC for sha512" do 54 | hmac = Gibberish::HMAC512("password", "data\n") 55 | o_hmac = `echo "data" | openssl dgst -sha512 -hmac 'password' | sed 's/^.*= //'` 56 | hmac.must_equal(o_hmac.chomp) 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /lib/gibberish/hmac.rb: -------------------------------------------------------------------------------- 1 | module Gibberish 2 | # Easy to use HMAC 3 | # 4 | # ## Example 5 | # 6 | # Gibberish::HMAC256('key', 'data') 7 | # #=> 5031fe3d989c6d1537a013fa6e739da23463fdaec3b70137d828e36ace221bd0 8 | # Gibberish::HMAC1('key', 'data') # SHA1 9 | # #=> 104152c5bfdca07bc633eebd46199f0255c9f49d 10 | # Gibberish::HMAC224('key', 'data') # SHA224 11 | # #=> 19424d4210e50d7a4521b5f0d54b4b0cff3060deddccfd894fda5b3b 12 | # Gibberish::HMAC384('key', 'data') # SHA384 13 | # #=> c5f97ad9fd1020c174d7dc02cf83c4c1bf15ee20ec555b690ad58e62da8a00ee 14 | # 44ccdb65cb8c80acfd127ebee568958a 15 | # Gibberish::HMAC512('key', 'data') # SHA512 16 | # #=> 3c5953a18f7303ec653ba170ae334fafa08e3846f2efe317b87efce82376253c 17 | # b52a8c31ddcde5a3a2eee183c2b34cb91f85e64ddbc325f7692b199473579c58 18 | # 19 | # ## OpenSSL CLI Interop 20 | # 21 | # echo -n "stuff" | openssl dgst -sha256 -hmac 'password' 22 | # 23 | # is the same as 24 | # 25 | # Gibberish::HMAC256('password', 'stuff') 26 | # 27 | class HMAC 28 | DIGEST = { 29 | :sha1 => OpenSSL::Digest.new('sha1'), 30 | :sha224 => OpenSSL::Digest.new('sha224'), 31 | :sha256 => OpenSSL::Digest.new('sha256'), 32 | :sha384 => OpenSSL::Digest.new('sha384'), 33 | :sha512 => OpenSSL::Digest.new('sha512') 34 | } 35 | 36 | # Returns the HMAC for the key and data 37 | # 38 | # Shorcut alias: Gibberish::HMAC(key, data) 39 | # 40 | # @param [String] key 41 | # @param [#to_s] data 42 | # @param [Symbol] digest 43 | # @param [Hash] opts 44 | # @option opts [Symbol] :digest (:sha1) the digest to encode with 45 | # @option opts [Boolean] :binary (false) encode the data in binary, not Base64 46 | def self.digest(key, data, digest, opts={}) 47 | data = data.to_s 48 | if opts[:binary] 49 | OpenSSL::HMAC.digest(DIGEST[digest], key, data) 50 | else 51 | OpenSSL::HMAC.hexdigest(DIGEST[digest], key, data) 52 | end 53 | end 54 | end 55 | 56 | def self.HMAC1(key, data) 57 | Gibberish::HMAC.digest(key, data, :sha1) 58 | end 59 | 60 | def self.HMAC224(key, data) 61 | Gibberish::HMAC.digest(key, data, :sha224) 62 | end 63 | 64 | def self.HMAC256(key, data) 65 | Gibberish::HMAC.digest(key, data, :sha256) 66 | end 67 | 68 | def self.HMAC384(key, data) 69 | Gibberish::HMAC.digest(key, data, :sha384) 70 | end 71 | 72 | def self.HMAC512(key, data) 73 | Gibberish::HMAC.digest(key, data, :sha512) 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /spec/aes_benchmark.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'benchmark' 4 | require 'gibberish' 5 | 6 | N = 50 7 | ITERATIONS = ENV["ITER"].to_i || 100_000 8 | PLAINTEXT = "Doner meatball turducken pig chuck turkey cow. Beef ribs picanha leberkas filet mignon chicken sirloin kevin jerky. Turkey venison beef ribs, turducken capicola biltong pork loin meatball cupim jowl pork chop pancetta. Filet mignon t-bone flank spare ribs chuck kielbasa capicola turkey shank doner shoulder meatloaf pancetta. Pork belly t-bone pork loin hamburger brisket alcatra. Ham tail brisket sausage hamburger, filet mignon landjaeger jerky corned beef biltong pork chop ball tip. Shoulder pork loin ham hock pastrami brisket chuck flank. Doner meatball turducken pig chuck turkey cow. Beef ribs picanha leberkas filet mignon chicken sirloin kevin jerky. Turkey venison beef ribs, turducken capicola biltong pork loin meatball cupim jowl pork chop pancetta. Filet mignon t-bone flank spare ribs chuck kielbasa capicola turkey shank doner shoulder meatloaf pancetta. Pork belly t-bone pork loin hamburger brisket alcatra. Ham tail brisket sausage hamburger, filet mignon landjaeger jerky corned beef biltong pork chop ball tip. Shoulder pork loin ham hock pastrami brisket chuck flank. Doner meatball turducken pig chuck turkey cow. Beef ribs picanha leberkas filet mignon chicken sirloin kevin jerky. Turkey venison beef ribs, turducken capicola biltong pork loin meatball cupim jowl pork chop pancetta. Filet mignon t-bone flank spare ribs chuck kielbasa capicola turkey shank doner shoulder meatloaf pancetta. Pork belly t-bone pork loin hamburger brisket alcatra. Ham tail brisket sausage hamburger, filet mignon landjaeger jerky corned beef biltong pork chop ball tip. Shoulder pork loin ham hock pastrami brisket chuck flank. Doner meatball turducken pig chuck turkey cow. Beef ribs picanha leberkas filet mignon chicken sirloin kevin jerky. Turkey venison beef ribs, turducken capicola biltong pork loin meatball cupim jowl pork chop pancetta. Filet mignon t-bone flank spare ribs chuck kielbasa capicola turkey shank doner shoulder meatloaf pancetta. Pork belly t-bone pork loin hamburger brisket alcatra. Ham tail brisket sausage hamburger, filet mignon landjaeger jerky corned beef biltong pork chop ball tip. Shoulder pork loin ham hock pastrami brisket chuck flank." 9 | 10 | puts "Benchmarking AES GCM: Encrypting 512 bytes #{N} times, at #{ITERATIONS} iterations\n" 11 | cipher = Gibberish::AES.new("s33krit", iter: ITERATIONS) 12 | plaintext = PLAINTEXT.slice(0,512) 13 | time = Benchmark.realtime { 14 | N.times { 15 | cipher.encrypt(plaintext) 16 | } 17 | } 18 | 19 | puts "Avg time per encryption: #{'%.5f' % (time/N)}ms" 20 | 21 | -------------------------------------------------------------------------------- /spec/fixtures/secret.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 2 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 3 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 4 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 5 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 6 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 7 | 8 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 9 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 10 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 11 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 12 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 13 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 14 | 15 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 16 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 17 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 18 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 19 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 20 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 21 | 22 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 23 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 24 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 25 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 26 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 27 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 28 | 29 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 30 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 31 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 32 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 33 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 34 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 35 | 36 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod 37 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, 38 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo 39 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse 40 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non 41 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -------------------------------------------------------------------------------- /spec/rsa_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "RSA key generation" do 4 | it "should generate a key" do 5 | keypair = Gibberish::RSA.generate_keypair 6 | keypair.must_be_instance_of(Gibberish::RSA::KeyPair) 7 | end 8 | 9 | it "should generate a key with custom bits" do 10 | keypair = Gibberish::RSA.generate_keypair(1024) 11 | keypair.must_be_instance_of(Gibberish::RSA::KeyPair) 12 | end 13 | 14 | it "should answer to public and private key methods" do 15 | keypair = Gibberish::RSA.generate_keypair(1024) 16 | keypair.must_be_instance_of(Gibberish::RSA::KeyPair) 17 | keypair.public_key.wont_be_nil 18 | keypair.private_key.wont_be_nil 19 | end 20 | 21 | end 22 | 23 | describe "RSA" do 24 | before do 25 | k = Gibberish::RSA.generate_keypair(1024) 26 | @cipher = Gibberish::RSA.new(k.private_key) 27 | @pub_cipher = Gibberish::RSA.new(k.public_key) 28 | end 29 | 30 | it "should encrypt/decrypt with a keypair" do 31 | encrypted = @cipher.encrypt("Some data") 32 | decrypted = @cipher.decrypt(encrypted) 33 | encrypted.must_match(/^[a-zA-Z0-9\+\/\n=]+$/) # Be base64 34 | decrypted.must_equal("Some data") 35 | end 36 | 37 | it "should work without private key" do 38 | enc = @pub_cipher.encrypt("Some data") 39 | enc.must_match(/^[a-zA-Z0-9\+\/\n=]+$/) # Be base64 40 | end 41 | 42 | end 43 | 44 | describe "OpenSSL interop" do 45 | 46 | before do 47 | @ossl_private_key = File.read('spec/openssl/private.pem') 48 | @ossl_public_key = File.read('spec/openssl/public.pem') 49 | @keypair = Gibberish::RSA.generate_keypair(1024) 50 | @keypair.passphrase = "p4ssw0rd" 51 | tmp_file = "/tmp/gibberish-spec" 52 | @pub_key_file = "#{tmp_file}-pub.pem" 53 | @priv_key_file = "#{tmp_file}-priv.pem" 54 | File.open(@pub_key_file, 'w') {|f| f.write(@keypair.public_key) } 55 | File.open(@priv_key_file, 'w') {|f| f.write(@keypair.private_key) } 56 | end 57 | 58 | it "should decode and OpenSSL generated key and crypted message" do 59 | # openssl genrsa -des3 -out spec/openssl/private.pem 2048 60 | # openssl rsa -in spec/openssl/private.pem -out spec/openssl/public.pem -outform PEM -pubout 61 | # openssl rsautl -encrypt -inkey public.pem -pubin -in spec/openssl/plaintext.txt -out spec/openssl/plaintext.crypted 62 | cipher = Gibberish::RSA.new(@ossl_private_key, @keypair.passphrase) 63 | cipher.decrypt(File.read('spec/openssl/plaintext.crypted'), :binary => true).must_equal(File.read('spec/openssl/plaintext.txt')) 64 | end 65 | 66 | if ENV['INTERACTIVE'] 67 | it "should encode an OpenSSL compatible format" do 68 | # openssl rsautl -decrypt -inkey /tmp/gibberish-spec-priv.pem -in /tmp/gibberish-spec-test.crypted 69 | cipher = Gibberish::RSA.new(@keypair.public_key) 70 | tmp_crypt_file = '/tmp/gibberish-spec-test.crypted' 71 | File.open(tmp_crypt_file, 'w') {|f| f.write(cipher.encrypt("secret text", :binary => true))} 72 | puts "\n Please type '#{@keypair.passphrase}' when prompted" 73 | output = `openssl rsautl -decrypt -inkey /tmp/gibberish-spec-priv.pem -in /tmp/gibberish-spec-test.crypted` 74 | output.must_equal("secret text") 75 | end 76 | end 77 | 78 | end 79 | -------------------------------------------------------------------------------- /lib/gibberish/rsa.rb: -------------------------------------------------------------------------------- 1 | module Gibberish 2 | 3 | # This wraps the OpenSSL RSA functions 4 | # Simply instantiate with a public key or private key 5 | # 6 | # cipher = Gibberish::RSA.new(private_key) 7 | # enc = cipher.encrypt(data) 8 | # dec = cipher.decrypt(enc) 9 | # 10 | # cipher = Gibberish::RSA(public_key) 11 | # cipher.decrypt(enc) 12 | # 13 | # 14 | # You can also generate a keypair using Gibberish::RSA.generate_keypair 15 | # 16 | # kp = Gibberish::RSA.generate_keypair(4096) 17 | # kp.public_key #=> Outputs a Base64 encoded public key 18 | # kp.private_key #=> Outputs the Base64 pem 19 | # 20 | # KeyPair will hand back the private key when passed 21 | # to the RSA class. 22 | # 23 | # cipher = Gibberish::RSA.new(kp) 24 | # 25 | # ## OpenSSL CLI Interop 26 | # 27 | # openssl rsautl -decrypt -inkey [pem_file] -in [BinaryEncodedCryptedFile] 28 | # 29 | # or if you're using the default base64 output, you'll need to decode that first 30 | # 31 | # openssl enc -d -base64 -in [gibberish.crypted] | \ 32 | # openssl rsautl -decrypt -inkey [pem_file] 33 | # 34 | 35 | class RSA 36 | class KeyPair 37 | def self.generate(bits=2048) 38 | self.new(OpenSSL::PKey::RSA.generate(bits)) 39 | end 40 | 41 | attr_accessor :passphrase 42 | 43 | def initialize(key) 44 | @key = key 45 | @cipher = OpenSSL::Cipher.new('aes-256-cbc') 46 | end 47 | 48 | def public_key 49 | @key.public_key 50 | end 51 | 52 | def private_key 53 | if @passphrase 54 | @key.to_pem(@cipher, @passphrase) 55 | else 56 | @key.to_pem 57 | end 58 | end 59 | 60 | def to_s 61 | private_key 62 | end 63 | 64 | end 65 | 66 | # Generate an RSA keypair - defaults to 2048 bits 67 | # 68 | # @param [Integer] bits 69 | def RSA.generate_keypair(bits=2048) 70 | KeyPair.generate(bits) 71 | end 72 | 73 | # Expects a public key at the minumum 74 | # 75 | # @param [#to_s] key public or private 76 | # @param [String] passphrase to key 77 | # 78 | def initialize(key, passphrase=nil) 79 | @key = OpenSSL::PKey::RSA.new(key.to_s, passphrase) 80 | end 81 | 82 | # Encrypt data using the key 83 | # 84 | # @param [#to_s] data 85 | # @param [Hash] opts 86 | # @option opts [Boolean] :binary (false) encode the data in binary, not Base64 87 | def encrypt(data, opts={}) 88 | data = data.to_s 89 | enc = @key.public_encrypt(data) 90 | if opts[:binary] 91 | enc 92 | else 93 | Base64.encode64(enc) 94 | end 95 | end 96 | 97 | # Decrypt data using the key 98 | # 99 | # @param [#to_s] data 100 | # @param [Hash] opts 101 | # @option opts [Boolean] :binary (false) don't decode the data as Base64 102 | def decrypt(data, opts={}) 103 | data = data.to_s 104 | raise "No private key set!" unless @key.private? 105 | unless opts[:binary] 106 | data = Base64.decode64(data) 107 | end 108 | @key.private_decrypt(data) 109 | end 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Gibberish - A ruby encryption library 2 | [![Travis](https://travis-ci.org/mdp/gibberish.svg?branch=master)](https://travis-ci.org/mdp/gibberish) 3 | 4 | **Note: It's 2017 and if you're looking for a modern and actively maintained Ruby encryption library you should do yourself a favor and check out [RbNaCl](https://github.com/cryptosphere/rbnacl). Gibberish was started in 2011 when encryption on Ruby was not a trivial matter, however thanks to projects like [NaCl](https://nacl.cr.yp.to/) and [LibSodium](https://download.libsodium.org/doc/) that's no longer the case.** 5 | 6 | *NOTICE: Breaking Changes in 2.0* 7 | 8 | Checkout the [Changelog](CHANGELOG.mdown) for a full list of changes in 2.0 9 | 10 | ## Goals 11 | - AES encryption should have sensible defaults 12 | - AES should be interoperable with SJCL for browser based decryption/encryption 13 | - Simple API for HMAC/Digests 14 | - Targets more recent versions of Ruby(>=2.0) with better OpenSSL support 15 | 16 | ## Requirements 17 | 18 | Ruby 2.0 or later, compiled with OpenSSL support 19 | 20 | ## Installation 21 | 22 | gem install gibberish 23 | 24 | ## AES 25 | 26 | AES encryption with sensible defaults: 27 | 28 | - 100,000 iterations of PBKDF2 password hardening 29 | - GCM mode with authentication 30 | - Ability to include authenticated data 31 | - Compatible with [SJCL](http://bitwiseshiftleft.github.io/sjcl/), meaning all ciphertext is decryptable in JS via [SJCL](http://bitwiseshiftleft.github.io/sjcl/) 32 | 33 | ### Encrypting 34 | 35 | cipher = Gibberish::AES.new('p4ssw0rd') 36 | cipher.encrypt("some secret text") 37 | # => Outputs a JSON string containing everything that needs to be saved for future decryption 38 | # Example: 39 | # '{"v":1,"adata":"","ks":256,"ct":"ay2varjSFUMUmtvZeh9755GVyCkWHG0/BglJLQ==","ts":96,"mode":"gcm", 40 | # "cipher":"aes","iter":100000,"iv":"K4ZShCQGL3UZr78y","salt":"diDUzbc9Euo="}' 41 | 42 | ### Decrypting 43 | 44 | cipher = Gibberish::AES.new('p4ssw0rd') 45 | cipher.decrypt('{"v":1,"adata":"","ks":256,"ct":"ay2varjSFUMUmtvZeh9755GVyCkWHG0/BglJLQ==","ts":96,"mode":"gcm","cipher":"aes","iter":100000,"iv":"K4ZShCQGL3UZr78y","salt":"diDUzbc9Euo="}') 46 | # => "some secret text" 47 | 48 | ### Interoperability with SJCL (JavaScript - Browser/Node.js) 49 | 50 | AES ciphertext from Gibberish is compatible with [SJCL](http://bitwiseshiftleft.github.io/sjcl/), a JavaScript library which 51 | works in the browser and Node.js 52 | 53 | [See the full docs](http://www.rubydoc.info/github/mdp/gibberish/Gibberish/AES) for information on SJCL interoperability. 54 | 55 | ### Gibberish 1.x Encryption (CBC) 56 | 57 | Prior to Gibberish 2.0, the default encryption mode was CBC. You can still access this 58 | by calling it explicitly: 59 | 60 | cipher = Gibberish::AES::CBC.new('p4ssw0rd') 61 | cipher.encrypt("Some secret text") 62 | 63 | ## HMAC 64 | 65 | Gibberish::HMAC256("password", "data") 66 | # => "cccf6f0334130a7010d62332c75b53e7d8cea715e52692b06e9cd41b05644be3" 67 | 68 | [See the full docs](http://www.rubydoc.info/github/mdp/gibberish/Gibberish/HMAC) 69 | 70 | ## Digests 71 | 72 | Gibberish::MD5("somedata") 73 | #=> aefaf7502d52994c3b01957636a3cdd2 74 | 75 | Gibberish::SHA1("somedata") 76 | #=> efaa311ae448a7374c122061bfed952d940e9e37 77 | 78 | Gibberish::SHA224("somedata") 79 | #=> a39b86d838273f5ff4879c26f85e3cb333bb44d73b24f275bad1a6c6 80 | 81 | Gibberish::SHA256("somedata") 82 | #=> 87d149cb424c0387656f211d2589fb5b1e16229921309e98588419ccca8a7362 83 | 84 | Gibberish::SHA384("somedata") 85 | #=> b6800736973cc061e3efb66a34f8bda8fa946804c6cc4f26a6b9b3950211078801709d0d82707c569a07c8f63c804c87 86 | 87 | Gibberish::SHA512("somedata") 88 | #=> a053441b6de662599ecb14c580d6637dcb856a66b2a40a952d39df772e47e98ea22f9e105b31463c5cf2472feae7649464fe89d99ceb6b0bc398a6926926f416 89 | 90 | [See the full docs](http://www.rubydoc.info/github/mdp/gibberish/Gibberish/Digest) 91 | 92 | ## Run the tests 93 | 94 | git clone https://github.com/mdp/gibberish.git 95 | cd gibberish 96 | make 97 | 98 | ### Benchmarking AES with PBKDF2 99 | 100 | make benchmark 101 | # Change the PBKDF2 iterations 102 | ITER=10000 make benchmark 103 | 104 | -------------------------------------------------------------------------------- /lib/gibberish/digest.rb: -------------------------------------------------------------------------------- 1 | module Gibberish 2 | # Allows for the simple digest of data, supports MD5, SHA1, and SHA256 3 | # 4 | # ## Examples 5 | # 6 | # Gibberish::MD5("data") #=> 8d777f385d3dfec8815d20f7496026dc 7 | # Gibberish::SHA1("data") #=> a17c9aaa61e80a1bf71d0d850af4e5baa9800bbd 8 | # Gibberish::SHA224("data") #=> f4739673acc03c424343b452787ee23dd62999a8a9f14f4250995769 9 | # Gibberish::SHA256("data") #=> 3a6eb0790f39ac87c94f3856b2dd2c5d110e6811602261a9a923d3bb23adc8b7 10 | # Gibberish::SHA384("data") #=> 2039e0f0b92728499fb88e23ebc3cfd0554b28400b0ed7b753055c88b5865c3c2aa72c6a1a9ae0a755d87900a4a6ff41 11 | # Gibberish::SHA512("data") #=> 77c7ce9a5d86bb386d443bb96390faa120633158699c8844c30b13ab0bf92760b7e4416aea397db91b4ac0e5dd56b8ef7e4b066162ab1fdc088319ce6defc876 12 | # 13 | # ## OpenSSL CLI Interop 14 | # 15 | # echo -n 'data' | openssl dgst -sha1 16 | # echo -n 'data' | openssl dgst -sha224 17 | # echo -n 'data' | openssl dgst -sha256 18 | # echo -n 'data' | openssl dgst -sha384 19 | # echo -n 'data' | openssl dgst -sha512 20 | # echo -n 'data' | openssl dgst -md5 21 | # 22 | # is the same as 23 | # 24 | # Gibberish::SHA1("data") 25 | # Gibberish::SHA224("data") 26 | # Gibberish::SHA256("data") 27 | # Gibberish::SHA384("data") 28 | # Gibberish::SHA512("data") 29 | # Gibberish::MD5("data") 30 | # 31 | class Digest 32 | 33 | # Returns the SHA1 digest for the data 34 | # 35 | # Shorcut alias: Gibberish::SHA1(data) 36 | # 37 | # @param [#to_s] data 38 | # @param [Hash] opts 39 | # @option opts [Boolean] :binary (false) encode the data in binary, not Base64 40 | def self.sha1(data, opts={}) 41 | data = data.to_s 42 | if opts[:binary] 43 | OpenSSL::Digest::SHA1.digest(data) 44 | else 45 | OpenSSL::Digest::SHA1.hexdigest(data) 46 | end 47 | end 48 | 49 | # Returns the SHA224 digest for the data 50 | # 51 | # Shorcut alias: Gibberish::SHA224(data) 52 | # 53 | # @param [#to_s] data 54 | # @param [Hash] opts 55 | # @option opts [Boolean] :binary (false) encode the data in binary, not Base64 56 | def self.sha224(data, opts={}) 57 | data = data.to_s 58 | if opts[:binary] 59 | OpenSSL::Digest::SHA224.digest(data) 60 | else 61 | OpenSSL::Digest::SHA224.hexdigest(data) 62 | end 63 | end 64 | 65 | # Returns the SHA256 digest for the data 66 | # 67 | # Shorcut alias: Gibberish::SHA256(data) 68 | # 69 | # @param [#to_s] data 70 | # @param [Hash] opts 71 | # @option opts [Boolean] :binary (false) encode the data in binary, not Base64 72 | def self.sha256(data, opts={}) 73 | data = data.to_s 74 | if opts[:binary] 75 | OpenSSL::Digest::SHA256.digest(data) 76 | else 77 | OpenSSL::Digest::SHA256.hexdigest(data) 78 | end 79 | end 80 | 81 | # Returns the SHA384 digest for the data 82 | # 83 | # Shorcut alias: Gibberish::SHA384(data) 84 | # 85 | # @param [#to_s] data 86 | # @param [Hash] opts 87 | # @option opts [Boolean] :binary (false) encode the data in binary, not Base64 88 | def self.sha384(data, opts={}) 89 | data = data.to_s 90 | if opts[:binary] 91 | OpenSSL::Digest::SHA384.digest(data) 92 | else 93 | OpenSSL::Digest::SHA384.hexdigest(data) 94 | end 95 | end 96 | 97 | # Returns the SHA512 digest for the data 98 | # 99 | # Shorcut alias: Gibberish::SHA512(data) 100 | # 101 | # @param [#to_s] data 102 | # @param [Hash] opts 103 | # @option opts [Boolean] :binary (false) encode the data in binary, not Base64 104 | def self.sha512(data, opts={}) 105 | data = data.to_s 106 | if opts[:binary] 107 | OpenSSL::Digest::SHA512.digest(data) 108 | else 109 | OpenSSL::Digest::SHA512.hexdigest(data) 110 | end 111 | end 112 | 113 | # Returns the MD5 digest for the data 114 | # 115 | # Shorcut alias: Gibberish::MD5(data) 116 | # 117 | # @param [#to_s] data 118 | # @param [Hash] opts 119 | # @option opts [Boolean] :binary (false) encode the data in binary, not Base64 120 | def self.md5(data, opts={}) 121 | data = data.to_s 122 | if opts[:binary] 123 | OpenSSL::Digest::MD5.digest(data) 124 | else 125 | OpenSSL::Digest::MD5.hexdigest(data) 126 | end 127 | end 128 | end 129 | 130 | def self.SHA1(data, opts={}) 131 | Digest.sha1(data,opts) 132 | end 133 | 134 | def self.SHA224(data, opts={}) 135 | Digest.sha224(data,opts) 136 | end 137 | 138 | def self.SHA256(data, opts={}) 139 | Digest.sha256(data,opts) 140 | end 141 | 142 | def self.SHA384(data, opts={}) 143 | Digest.sha384(data,opts) 144 | end 145 | 146 | def self.SHA512(data, opts={}) 147 | Digest.sha512(data,opts) 148 | end 149 | 150 | def self.MD5(data, opts={}) 151 | Digest.md5(data,opts) 152 | end 153 | 154 | end 155 | -------------------------------------------------------------------------------- /spec/aes_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require 'spec_helper' 3 | require 'tempfile' 4 | 5 | describe "the sjcl compatible implementation of aes" do 6 | 7 | describe "decryption" do 8 | 9 | before do 10 | @cipher = Gibberish::AES.new("s33krit") 11 | end 12 | it "should decrypt gcm encoded text from SJCL" do 13 | # With a 64bit authentication tag 14 | json = '{"iv":"pO1RiSKSfmlLPMIS","v":1,"iter":1000,"ks":128,"ts":64,"mode":"gcm","adata":"","cipher":"aes","salt":"BC60XoGJqnY=","ct":"Jgm8bExXvpbEDxOxFDroBuFmczMlfF4G"}' 15 | @cipher.decrypt(json).must_equal("This is a secret"); 16 | # With a 96bit authentication tag 17 | json = '{"iv":"6ru5wmyPl2hfhMmb","v":1,"iter":1000,"ks":128,"ts":96,"mode":"gcm","adata":"","cipher":"aes","salt":"KhrgNREkjN4=","ct":"/0LMJz7pYDXSdFa+x3vL7uc46Nz7y5kV9DhEBQ=="}' 18 | @cipher.decrypt(json).must_equal("This is a secret"); 19 | # With a 128bit authentication tag 20 | json = '{"iv":"S79wFwpjbSMz1FSB","v":1,"iter":1000,"ks":128,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"KhrgNREkjN4=","ct":"j8pJmmilaJ6We2fEq/NvAxka4Z70F7IEK/m9/y3hHoo="}' 21 | @cipher.decrypt(json).must_equal("This is a secret"); 22 | end 23 | it "should include the adata with the plaintext" do 24 | json = '{"iv":"w9Iugnn0HztMpm+y","v":1,"iter":1000,"ks":128,"ts":64,"mode":"gcm","adata":"123abc","cipher":"aes","salt":"Sw6NOinzVZ8=","ct":"djCIRln1PbuiLEkMb2AJZdT/"}' 25 | plaintext = @cipher.decrypt(json) 26 | plaintext.must_equal("plain text") 27 | plaintext.adata.must_equal("123abc") 28 | end 29 | describe "exceptions" do 30 | it "should check the iterations length before attempting to decrypt" do 31 | json = '{"iv":"S79wFwpjbSMz1FSB","v":1,"iter":1000000,"ks":128,"ts":128,"mode":"gcm","adata":"","cipher":"aes","salt":"KhrgNREkjN4=","ct":"j8pJmmilaJ6We2fEq/NvAxka4Z70F7IEK/m9/y3hHoo="}' 32 | e = assert_raises(Gibberish::AES::SJCL::CipherOptionsError) { @cipher.decrypt(json) } 33 | assert_match(/Iteration count/, e.message) 34 | end 35 | it "should only allow authenticated modes" do 36 | json = '{"iv":"6ru5wmyPl2hfhMmb","v":1,"iter":1000,"ks":128,"ts":96,"mode":"cbc","adata":"","cipher":"aes","salt":"KhrgNREkjN4=","ct":"/0LMJz7pYDXSdFa+x3vL7uc46Nz7y5kV9DhEBQ=="}' 37 | e = assert_raises(Gibberish::AES::SJCL::CipherOptionsError) { @cipher.decrypt(json) } 38 | assert_equal("Mode 'cbc' not supported", e.message) 39 | end 40 | it "should fail gracefully when attempting to decrypt an SJCL generated ciphertext with a >12 byte IV" do 41 | json = '{"iv":"fGuapJg66vk0eNNyLHUk1w==","v":1,"iter":1000,"ks":128,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"GRywsuW0M8E=","ct":"MUq4sLzEHtnUy2nTF8NEJQ=="}' 42 | e = assert_raises(Gibberish::AES::SJCL::CipherOptionsError) { @cipher.decrypt(json) } 43 | assert_match(/Initialization vector/, e.message) 44 | end 45 | it "should fail if the password is incorrect" do 46 | json = '{"iv":"ovFbwlWH+tTHFORl","v":1,"iter":1000,"ks":128,"ts":64,"mode":"gcm","adata":"","cipher":"aes","salt":"ib5/ig2qqL8=","ct":"ruxTz/VWArVfte4qzUwF/z74"}' 47 | assert_raises(Gibberish::AES::SJCL::DecryptionError) { @cipher.decrypt(json) } 48 | end 49 | it "should fail if the adata has be modified" do 50 | json = '{"iv":"S79wFwpjbSMz1FSB","v":1,"iter":1000,"ks":128,"ts":128,"mode":"gcm","adata":"foo","cipher":"aes","salt":"KhrgNREkjN4=","ct":"j8pJmmilaJ6We2fEq/NvAxka4Z70F7IEK/m9/y3hHoo="}' 51 | assert_raises(Gibberish::AES::SJCL::DecryptionError) { 52 | @cipher.decrypt(json) 53 | } 54 | end 55 | end 56 | end 57 | 58 | describe "encryption" do 59 | 60 | it "should encrypt text" do 61 | @cipher = Gibberish::AES.new("s33krit") 62 | plaintext = "This is some text, and some UTF-8 中华人民共和" 63 | ciphertext = @cipher.encrypt(plaintext) 64 | @cipher.decrypt(ciphertext).must_equal(plaintext); 65 | end 66 | 67 | it "should allow users to override the number of iterations" do 68 | @cipher = Gibberish::AES.new("s33krit", {iter: 10_000}) 69 | plaintext = "This is some text" 70 | ciphertext = @cipher.encrypt(plaintext) 71 | JSON.parse(ciphertext)["iter"].must_equal(10_000) 72 | @cipher.decrypt(ciphertext).must_equal(plaintext); 73 | end 74 | 75 | it "should set the correct JSON attributes in the ciphertext" do 76 | @cipher = Gibberish::AES.new("s33krit") 77 | plaintext = "This is some text" 78 | ciphertext = JSON.parse(@cipher.encrypt(plaintext)) 79 | ciphertext["iter"].must_equal(100_000) 80 | ciphertext["v"].must_equal(1) 81 | ciphertext["ks"].must_equal(256) 82 | ciphertext["ts"].must_equal(96) 83 | ciphertext["mode"].must_equal("gcm") 84 | ciphertext["cipher"].must_equal("aes") 85 | end 86 | 87 | end 88 | end 89 | 90 | describe "the openssl command line compatible aes cipher" do 91 | 92 | before do 93 | @cipher = Gibberish::AES::CBC.new("password") 94 | end 95 | 96 | it "should encrypt text and be compatible with OpenSSL CLI" do 97 | secret_text = "Made with Gibberish" 98 | encrypted = @cipher.e(secret_text) 99 | from_openssl = `echo "#{encrypted}" | openssl enc -d -aes-256-cbc -a -k password` 100 | from_openssl.must_equal(secret_text) 101 | end 102 | 103 | it "should encrypt file and be compatible with OpenSSL CLI" do 104 | source_file_path = "spec/fixtures/secret.txt" 105 | encrypted_file = Tempfile.new('secret.txt.enc') 106 | @cipher.ef(source_file_path, encrypted_file.path) 107 | decrypted_file = Tempfile.new('secret.txt') 108 | `openssl aes-256-cbc -d -in #{encrypted_file.path} -out #{decrypted_file.path} -k password` 109 | FileUtils.cmp(source_file_path, decrypted_file.path).must_equal(true) 110 | end 111 | 112 | it "when salt is not specified, encrypted text from repeated calls should not be the same" do 113 | secret_text = "Made with Gibberish" 114 | encrypted1 = @cipher.e(secret_text) 115 | encrypted2 = @cipher.e(secret_text) 116 | encrypted1.wont_equal(encrypted2) 117 | end 118 | 119 | it "when salt is specified, encrypted text from repeated calls (with same salt) be the same" do 120 | secret_text = "Made with Gibberish" 121 | salt = 'NaClNaCl' 122 | encrypted1 = @cipher.e(secret_text, {:salt => salt}) 123 | encrypted2 = @cipher.e(secret_text, {:salt => salt}) 124 | encrypted1.must_equal(encrypted2) 125 | end 126 | 127 | it "when supplied salt is too long, text should still encrypt/decrypt correctly" do 128 | secret_text = "Made with Gibberish" 129 | salt = 'NaClNaClNaClNaClNaClNaClNaClNaClNaClNaCl' 130 | encrypted1 = @cipher.e(secret_text, {:salt => salt}) 131 | @cipher.d(encrypted1).must_equal(secret_text) 132 | end 133 | 134 | it "when supplied salt is too short, text should still encrypt/decrypt correctly" do 135 | secret_text = "Made with Gibberish" 136 | salt = 'NaCl' 137 | encrypted1 = @cipher.e(secret_text, {:salt => salt}) 138 | @cipher.d(encrypted1).must_equal(secret_text) 139 | end 140 | 141 | it "when number is supplied for salt, text should still encrypt/decrypt correctly" do 142 | secret_text = "Made with Gibberish" 143 | salt = 42 144 | encrypted1 = @cipher.e(secret_text, {:salt => salt}) 145 | @cipher.d(encrypted1).must_equal(secret_text) 146 | end 147 | 148 | it "when idiotic value is supplied for salt, text should still encrypt/decrypt correctly" do 149 | secret_text = "Made with Gibberish" 150 | salt = {:whoknew => "I'm an idiot"} 151 | encrypted1 = @cipher.e(secret_text, {:salt => salt}) 152 | @cipher.d(encrypted1).must_equal(secret_text) 153 | end 154 | 155 | it "should decrypt base64 encoded data from the OpenSSL CLI" do 156 | secret_text = "Made with Gibberish" 157 | from_openssl = `echo #{secret_text} | openssl enc -aes-256-cbc -a -k password` 158 | decrypted_text = @cipher.d(from_openssl).chomp 159 | decrypted_text.must_equal(secret_text) 160 | end 161 | 162 | it "should decrypt file encrypted with OpenSSL CLI" do 163 | source_file_path = "spec/fixtures/secret.txt" 164 | encrypted_file = Tempfile.new('secret.txt.enc') 165 | `openssl aes-256-cbc -salt -in #{source_file_path} -out #{encrypted_file.path} -k password` 166 | decrypted_file = Tempfile.new('secret.txt') 167 | @cipher.df(encrypted_file.path, decrypted_file.path) 168 | FileUtils.cmp(source_file_path, decrypted_file.path).must_equal(true) 169 | end 170 | 171 | it "should throw correct exception when decryption string is too short" do 172 | assert_raises(ArgumentError) {@cipher.d("short")} 173 | end 174 | 175 | describe 'stream encryption' do 176 | 177 | it 'encrypts a file' do 178 | File.open('spec/openssl/plaintext.txt', 'rb') do |in_file| 179 | File.open(Tempfile.new('gib'), 'wb') do |enc_file| 180 | @cipher.encrypt_stream in_file, enc_file, salt: 'SOMESALT' 181 | File.read(enc_file.path).must_equal(File.read('spec/openssl/plaintext.aes')) 182 | end 183 | end 184 | end 185 | 186 | it 'decrypts a file' do 187 | File.open('spec/openssl/plaintext.aes', 'rb') do |in_file| 188 | File.open(Tempfile.new('gib'), 'wb') do |dec_file| 189 | @cipher.decrypt_stream in_file, dec_file 190 | File.read(dec_file.path).must_equal(File.read('spec/openssl/plaintext.txt')) 191 | end 192 | end 193 | end 194 | 195 | end 196 | 197 | end 198 | -------------------------------------------------------------------------------- /lib/gibberish/aes.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require 'delegate' 3 | require 'securerandom' 4 | 5 | module Gibberish 6 | # # Handles AES encryption and decryption with some sensible defaults 7 | # - 256 bit AES encryption 8 | # - GCM mode with Authentication 9 | # - 100,000 iterations of PBKDF2_HMAC for key strengthening 10 | # 11 | # ## Compatibility with SJCL 12 | # It outputs into a format that is compatible with SJCL and easy to 13 | # consume in browsers/Node.js 14 | # 15 | # ## Basic Usage 16 | # 17 | # ### Encrypting 18 | # 19 | # cipher = Gibberish::AES.new('p4ssw0rd') 20 | # cipher.encrypt("some secret text") 21 | # #=> Outputs a JSON string containing all the necessary information 22 | # 23 | # ### Decrypting 24 | # 25 | # cipher = Gibberish::AES.new('p4ssw0rd') 26 | # cipher.decrypt('{"iv":"I4XKgNfMNkYhvzXc","v":1,"iter":1000,"ks":128,"ts":64,"mode":"gcm","adata":"123abc","cipher":"aes","salt":"PJsit8L16Ug=","ct":"5sEBsHXQqLXLOjxuVQK7fGZVdrMyRGDJ"}') 27 | # #=> "some secret text" 28 | # 29 | # #### Including Authenticated data. 30 | # 31 | # GCM mode allows you to include "Authenticated Data" with the ciphertext, if you wish. 32 | # For an overview of Authenticated Data, see this post: [http://crypto.stackexchange.com/a/15701](http://crypto.stackexchange.com/a/15701) 33 | # 34 | # Using AD is easy with Gibberish 35 | # 36 | # cipher = Gibberish::AES.new('p4ssw0rd') 37 | # ciphertext = cipher.encrypt("Some secret data", "my authenticated data") 38 | # plaintext = cipher.decrypt(ciphertext) 39 | # #=> "some secret text" 40 | # plaintext.adata 41 | # # => "my authenticated data" 42 | # 43 | # ## Interoperability with SJCL's GCM mode AES 44 | # 45 | # #### Decrypting 46 | # 47 | # ```javascript 48 | # // In the browser 49 | # var cleartext = sjcl.decrypt('key', '[output from Gibberish AES]'); 50 | # ``` 51 | # 52 | # #### Encrypting 53 | # 54 | # Ruby OpenSSL cannot handle an IV longer than 12 bytes, therefore we need to tell SJCL to 55 | # only use a 3 word IV value. See: https://github.com/bitwiseshiftleft/sjcl/issues/180 56 | # 57 | # ```javascript 58 | # // In the browser 59 | # var ciphertext = sjcl.encrypt('key', 'plain text', {mode: 'gcm', iv: sjcl.random.randomWords(3, 0)}); 60 | # ``` 61 | # 62 | # ## Backward compatibility with older pre 2.0 Gibberish 63 | # 64 | # Gibberish was previously designed to be compatible with OpenSSL on the command line with CBC mode AES. 65 | # This has been deprecated in favor of GCM mode. However, you may still 66 | # decrypt and encrypt using legacy convenience methods below: 67 | # 68 | # (Note: OpenSSL "enc" uses a non-standard file format which lacks [key stretching](http://en.wikipedia.org/wiki/Key_stretching), this means less secure passwords are more susceptible to brute forcing.) 69 | # 70 | # ### AES-256-CBC mode 71 | # 72 | # cipher = Gibberish::AES::CBC.new('p4ssw0rd') 73 | # cipher_text = cipher.encrypt("some secret text") 74 | # # => U2FsdGVkX1/D7z2azGmmQELbMNJV/n9T/9j2iBPy2AM= 75 | # 76 | # cipher.decrypt(cipher_text) 77 | # 78 | # # From the command line 79 | # echo "U2FsdGVkX1/D7z2azGmmQELbMNJV/n9T/9j2iBPy2AM=\n" | openssl enc -d -aes-256-cbc -a -k p4ssw0rd 80 | # 81 | class AES 82 | # Returns the AES object 83 | # 84 | # @param [String] password 85 | # @param [Hash] opts 86 | # @option opts [Symbol] :mode ('gcm') the AES mode to use 87 | # @option opts [Symbol] :ks (256) keystrength 88 | # @option opts [Symbol] :iter (100_000) number of PBKDF2 iterations to run on the password 89 | # @option opts [Symbol] :max_iter (100_000) maximum allow iterations, set to prevent DOS attack of someone setting a large 'iter' value in the ciphertext JSON 90 | # @option opts [Symbol] :ts (64) length of the authentication data hash 91 | def initialize(password, opts={}) 92 | @cipher = SJCL.new(password, opts) 93 | end 94 | 95 | # Returns the ciphertext in the form of a JSON string 96 | # 97 | # @param [String] data 98 | # @param [String] authenticated_data (Won't be encrypted) 99 | def encrypt(data, authenticated_data='') 100 | @cipher.encrypt(data, authenticated_data) 101 | end 102 | 103 | # Returns a Plaintext object (essentially a String with an additional 'adata' attribute) 104 | # 105 | # @param [String] ciphertext 106 | def decrypt(ciphertext) 107 | @cipher.decrypt(ciphertext) 108 | end 109 | 110 | end 111 | 112 | class AES::SJCL 113 | class CipherOptionsError < ArgumentError; end 114 | class DecryptionError < StandardError; end 115 | class Plaintext < SimpleDelegator 116 | attr_reader :adata 117 | def initialize(str, adata) 118 | @adata = adata; 119 | super(str) 120 | end 121 | end 122 | 123 | MAX_ITER = 100_000 124 | ALLOWED_MODES = ['ccm', 'gcm'] 125 | ALLOWED_KS = [128, 192, 256] 126 | ALLOWED_TS = [64, 96, 128] 127 | DEFAULTS = { 128 | v:1, iter:100_000, ks:256, ts:96, 129 | mode:"gcm", adata:"", cipher:"aes", max_iter: MAX_ITER 130 | } 131 | def initialize(password, opts={}) 132 | @password = password 133 | @opts = DEFAULTS.merge(opts) 134 | check_cipher_options(@opts) 135 | end 136 | 137 | def encrypt(plaintext, adata='') 138 | salt = SecureRandom.random_bytes(8) 139 | iv = SecureRandom.random_bytes(12) 140 | key = OpenSSL::PKCS5.pbkdf2_hmac(@password, salt, @opts[:iter], @opts[:ks]/8, 'SHA256') 141 | cipherMode = "#{@opts[:cipher]}-#{@opts[:ks]}-#{@opts[:mode]}" 142 | c = OpenSSL::Cipher.new(cipherMode) 143 | c.encrypt 144 | c.key = key 145 | c.iv = iv 146 | c.auth_data = adata 147 | ct = c.update(plaintext) + c.final 148 | tag = c.auth_tag(@opts[:ts]/8); 149 | ct = ct + tag 150 | out = { 151 | v: @opts[:v], adata: adata, ks: @opts[:ks], ct: Base64.strict_encode64(ct).encode('utf-8'), ts: tag.length * 8, 152 | mode: @opts[:mode], cipher: 'aes', iter: @opts[:iter], iv: Base64.strict_encode64(iv), 153 | salt: Base64.strict_encode64(salt) 154 | } 155 | out.to_json 156 | end 157 | 158 | def decrypt(h) 159 | begin 160 | h = JSON.parse(h, {:symbolize_names => true}) 161 | rescue => e 162 | raise "Unable to parse JSON of crypted text. #{e.inspect}" 163 | end 164 | check_cipher_options(h) 165 | key = OpenSSL::PKCS5.pbkdf2_hmac(@password, Base64.decode64(h[:salt]), h[:iter], h[:ks]/8, 'SHA256') 166 | iv = Base64.decode64(h[:iv]) 167 | ct = Base64.decode64(h[:ct]) 168 | tag = ct[ct.length-h[:ts]/8,ct.length] 169 | ct = ct[0,ct.length-h[:ts]/8] 170 | cipherMode = "#{h[:cipher]}-#{h[:ks]}-#{h[:mode]}" 171 | begin 172 | c = OpenSSL::Cipher.new(cipherMode) 173 | rescue RuntimeError => e 174 | raise "OpenSSL error when initializing: #{e.inspect}" 175 | end 176 | c.decrypt 177 | c.key = key 178 | c.iv = iv 179 | c.auth_tag = tag; 180 | c.auth_data = h[:adata] || "" 181 | begin 182 | out = c.update(ct) + c.final(); 183 | rescue OpenSSL::Cipher::CipherError => e 184 | raise DecryptionError.new(e.inspect); 185 | end 186 | return Plaintext.new(out.force_encoding('utf-8'), h[:adata]) 187 | end 188 | 189 | # Assume the worst 190 | def check_cipher_options(c_opts) 191 | if @opts[:max_iter] < c_opts[:iter] 192 | # Prevent DOS attacks from high PBKDF iterations 193 | # You an increase this by passing in opts[:max_iter] 194 | raise CipherOptionsError.new("Iteration count of #{c_opts[:iter]} exceeds the maximum of #{@opts[:max_iter]}") 195 | elsif !ALLOWED_MODES.include?(c_opts[:mode]) 196 | raise CipherOptionsError.new("Mode '#{c_opts[:mode]}' not supported") 197 | elsif !ALLOWED_KS.include?(c_opts[:ks]) 198 | raise CipherOptionsError.new("Keystrength of #{c_opts[:ks]} not supported") 199 | elsif !ALLOWED_TS.include?(c_opts[:ts]) 200 | raise CipherOptionsError.new("Tag length of #{c_opts[:ts]} not supported") 201 | elsif c_opts[:iv] && Base64.decode64(c_opts[:iv]).length > 12 202 | raise CipherOptionsError.new("Initialization vector's greater than 12 bytes are not supported in Ruby.") 203 | end 204 | end 205 | end 206 | 207 | class AES::CBC 208 | 209 | BUFFER_SIZE = 4096 210 | 211 | attr_reader :password, :size, :cipher 212 | 213 | # Initialize with the password 214 | # 215 | # @param [String] password 216 | # @param [Integer] size 217 | # @param [String] mode 218 | def initialize(password, size=256, mode="cbc") 219 | @password = password 220 | @size = size 221 | @mode = mode 222 | @cipher = OpenSSL::Cipher.new("aes-#{size}-#{mode}") 223 | end 224 | 225 | def encrypt(data, opts={}) 226 | salt = generate_salt(opts[:salt]) 227 | setup_cipher(:encrypt, salt) 228 | e = cipher.update(data) + cipher.final 229 | e = "Salted__#{salt}#{e}" #OpenSSL compatible 230 | opts[:binary] ? e : Base64.encode64(e) 231 | end 232 | alias :enc :encrypt 233 | alias :e :encrypt 234 | 235 | def decrypt(data, opts={}) 236 | raise ArgumentError, 'Data is too short' unless data.length >= 16 237 | data = Base64.decode64(data) unless opts[:binary] 238 | salt = data[8..15] 239 | data = data[16..-1] 240 | setup_cipher(:decrypt, salt) 241 | cipher.update(data) + cipher.final 242 | end 243 | alias :dec :decrypt 244 | alias :d :decrypt 245 | 246 | def encrypt_file(from_file, to_file, opts={}) 247 | salt = generate_salt(opts[:salt]) 248 | setup_cipher(:encrypt, salt) 249 | buf = "" 250 | File.open(to_file, "wb") do |outf| 251 | outf << "Salted__#{salt}" 252 | File.open(from_file, "rb") do |inf| 253 | while inf.read(4096, buf) 254 | outf << self.cipher.update(buf) 255 | end 256 | outf << self.cipher.final 257 | end 258 | end 259 | end 260 | alias :enc_file :encrypt_file 261 | alias :ef :encrypt_file 262 | 263 | def decrypt_file(from_file, to_file) 264 | buf = "" 265 | salt = "" 266 | File.open(to_file, "wb") do |outf| 267 | File.open(from_file, "rb") do |inf| 268 | inf.seek(8, IO::SEEK_SET) 269 | inf.read(8, salt) 270 | setup_cipher(:decrypt, salt) 271 | while inf.read(4096, buf) 272 | outf << self.cipher.update(buf) 273 | end 274 | outf << self.cipher.final 275 | end 276 | end 277 | end 278 | alias :dec_file :decrypt_file 279 | alias :df :decrypt_file 280 | 281 | def encrypt_stream(in_stream, out_stream, opts={}) 282 | salt = generate_salt(opts[:salt]) 283 | setup_cipher(:encrypt, salt) 284 | out_stream << "Salted__#{salt}" 285 | copy_stream in_stream, out_stream 286 | end 287 | 288 | def decrypt_stream(in_stream, out_stream) 289 | header = in_stream.read(16) 290 | salt = header[8..15] 291 | setup_cipher(:decrypt, salt) 292 | copy_stream in_stream, out_stream 293 | end 294 | 295 | private 296 | 297 | def generate_salt(supplied_salt) 298 | if supplied_salt 299 | return supplied_salt.to_s[0,8].ljust(8,'.') 300 | end 301 | s = '' 302 | 8.times {s << rand(255).chr} 303 | s 304 | end 305 | 306 | def setup_cipher(method, salt) 307 | cipher.send(method) 308 | cipher.pkcs5_keyivgen(password, salt, 1) 309 | end 310 | 311 | def copy_stream(in_stream, out_stream) 312 | buf = '' 313 | while in_stream.read(BUFFER_SIZE, buf) 314 | out_stream << cipher.update(buf) 315 | end 316 | out_stream << cipher.final 317 | out_stream.flush 318 | end 319 | 320 | end 321 | end 322 | --------------------------------------------------------------------------------