├── LICENSE.txt ├── README.md ├── Rakefile.rb ├── example.rb ├── lib └── ruby_rncryptor.rb ├── ruby_rncryptor.gemspec └── spec └── ruby_rncryptor_spec.rb /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2013 Erik Wrenholt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | software and associated documentation files (the "Software"), to deal in the Software 7 | without restriction, including without limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 9 | to whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or 12 | substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 15 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 16 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 17 | FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Ruby RNCryptor 2 | -------------- 3 | 4 | This is a Ruby port of Rob Napier's Cocoa [RNCryptor](https://github.com/RNCryptor) library. Like RNCryptor, Ruby RNCryptor intends to be an easy-to-use class that correctly handles random initialization vectors, password stretching with PBKDF2, and HMAC verification. 5 | 6 | This port is based on his [Data Format](https://github.com/RNCryptor/RNCryptor-Spec/blob/master/RNCryptor-Spec-v3.md) wiki page. It currently implements version 2 and version 3. 7 | 8 | Usage Example: 9 | -------------- 10 | 11 | ```ruby 12 | require './lib/ruby_rncryptor' 13 | require "base64" 14 | 15 | password = "n3v3r gue55!!" 16 | encrypted = RubyRNCryptor.encrypt("This is a tiny bit of text to encrypt", password) 17 | 18 | puts Base64.encode64(encrypted) 19 | puts "Decrypting..." 20 | 21 | decrypted = RubyRNCryptor.decrypt(encrypted, password) 22 | 23 | puts decrypted 24 | ``` 25 | 26 | Release Notes 27 | ------------- 28 | 29 | 2017-04-01 - version 3.0.2 30 | 31 | - Fixed for OpenSSL deprecation warning. 32 | 33 | 2016-01-25 - version 3.0.1 34 | 35 | - Addressed a timing attack vulnerability by making the HMAC comparison constant time. 36 | - Updated the tests to use the newer rspec expect syntax. 37 | 38 | 2013-12-20 - version 3.0 39 | 40 | - Generates version 3 file format files by default. 41 | - Made a gemspec for this library, version 3.0 for file format version 3. 42 | - Decrypts files encrypted with a bug in the Cocoa version where multibyte passwords were truncated. 43 | 44 | Generate and install as a gemfile 45 | --------------------------------- 46 | 47 | gem build ruby_rncryptor.gemspec 48 | gem install ./ruby_rncryptor-3.0.1.gem 49 | 50 | Credits 51 | ------- 52 | 53 | - Ruby port by Erik Wrenholt 2013. 54 | - Original RNCrypto library and format are by Rob Napier. 55 | -------------------------------------------------------------------------------- /Rakefile.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec/core/rake_task' 3 | require 'rake' 4 | 5 | task :default => :spec 6 | RSpec::Core::RakeTask.new(:spec) -------------------------------------------------------------------------------- /example.rb: -------------------------------------------------------------------------------- 1 | require './lib/ruby_rncryptor' 2 | require "base64" 3 | 4 | password = "n3v3r gue55!!" 5 | 6 | encrypted = RubyRNCryptor.encrypt("This is a tiny bit of text to encrypt", password) 7 | puts Base64.encode64(encrypted) 8 | 9 | puts 10 | puts "Decrypting..." 11 | 12 | decrypted = RubyRNCryptor.decrypt(encrypted, password) 13 | 14 | puts decrypted -------------------------------------------------------------------------------- /lib/ruby_rncryptor.rb: -------------------------------------------------------------------------------- 1 | # RubyRNCryptor by Erik Wrenholt. 2 | # Based on data format described by Rob Napier 3 | # https://github.com/RNCryptor/RNCryptor-Spec/blob/master/RNCryptor-Spec-v3.md 4 | # MIT License 5 | 6 | require 'openssl' 7 | require 'securerandom' 8 | 9 | class RubyRNCryptor 10 | include OpenSSL 11 | 12 | def self.decrypt(data, password) 13 | 14 | version = data[0,1] 15 | raise "RubyRNCryptor only decrypts version 2 or 3" unless (version == "\x02" || version == "\x03") 16 | options = data[1,1] 17 | encryption_salt = data[2,8] 18 | hmac_salt = data[10,8] 19 | iv = data[18,16] 20 | cipher_text = data[34,data.length-66] 21 | hmac = data[data.length-32,32] 22 | 23 | msg = version + options + encryption_salt + hmac_salt + iv + cipher_text 24 | 25 | # Verify password is correct. First try with correct encoding 26 | hmac_key = PKCS5.pbkdf2_hmac_sha1(password, hmac_salt, 10000, 32) 27 | verified = eql_time_cmp([HMAC.hexdigest('sha256', hmac_key, msg)].pack('H*'), hmac) 28 | 29 | if !verified && version == "\x02" 30 | # Version 2 Cocoa version truncated multibyte passwords, so try truncating. 31 | password = RubyRNCryptor.truncate_multibyte_password(password) 32 | hmac_key = PKCS5.pbkdf2_hmac_sha1(password, hmac_salt, 10000, 32) 33 | verified = eql_time_cmp([HMAC.hexdigest('sha256', hmac_key, msg)].pack('H*'), hmac) 34 | end 35 | 36 | raise "Password may be incorrect, or the data has been corrupted. (HMAC could not be verified)" unless verified 37 | 38 | # HMAC was verified, now decrypt it. 39 | cipher = Cipher.new('aes-256-cbc') 40 | cipher.decrypt 41 | cipher.iv = iv 42 | cipher.key = PKCS5.pbkdf2_hmac_sha1(password, encryption_salt, 10000, 32) 43 | 44 | cipher.update(cipher_text) + cipher.final 45 | end 46 | 47 | def self.encrypt(data, password, version = 3) 48 | 49 | raise "RubyRNCryptor only encrypts version 2 or 3" unless (version == 2 || version == 3) 50 | 51 | version = version.chr.to_s # Currently version 3 52 | options = 1.chr.to_s # Uses password 53 | encryption_salt = SecureRandom.random_bytes(8) 54 | hmac_salt = SecureRandom.random_bytes(8) 55 | iv = SecureRandom.random_bytes(16) 56 | cipher_text = data[34,data.length-66] 57 | 58 | hmac_key = PKCS5.pbkdf2_hmac_sha1(password, hmac_salt, 10000, 32) 59 | 60 | cipher = Cipher.new('aes-256-cbc') 61 | cipher.encrypt 62 | cipher.iv = iv 63 | cipher.key = PKCS5.pbkdf2_hmac_sha1(password, encryption_salt, 10000, 32) 64 | cipher_text = cipher.update(data) + cipher.final 65 | 66 | msg = version + options + encryption_salt + hmac_salt + iv + cipher_text 67 | hmac = [HMAC.hexdigest('sha256', hmac_key, msg)].pack('H*') 68 | 69 | msg + hmac 70 | end 71 | 72 | def self.truncate_multibyte_password(str) 73 | if str.bytes.to_a.count == str.length 74 | return str 75 | end 76 | str.bytes.to_a[0...str.length].map {|c| c.chr}.join 77 | end 78 | 79 | # From http://ruby-doc.org/stdlib-2.0.0/libdoc/openssl/rdoc/OpenSSL/PKCS5.html#module-OpenSSL::PKCS5-label-Important+Note+on+Checking+Passwords 80 | def self.eql_time_cmp(a, b) 81 | unless a.length == b.length 82 | return false 83 | end 84 | cmp = b.bytes.to_a 85 | result = 0 86 | a.bytes.each_with_index {|c,i| 87 | result |= c ^ cmp[i] 88 | } 89 | result == 0 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /ruby_rncryptor.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'ruby_rncryptor' 3 | s.version = '3.0.2' 4 | s.date = '2017-04-01' 5 | s.summary = "Encrypt and Decrypt the RNCryptor format" 6 | s.description = "Encrypt and Decrypt the RNCryptor format." 7 | s.authors = ["Erik Wrenholt"] 8 | s.email = 'erik@timestretch.com' 9 | s.files = ["lib/ruby_rncryptor.rb"] 10 | s.homepage = 'https://github.com/RNCryptor/ruby_rncryptor' 11 | s.license = 'MIT' 12 | end 13 | -------------------------------------------------------------------------------- /spec/ruby_rncryptor_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require File.join(File.dirname(__FILE__), '../lib/ruby_rncryptor.rb') 4 | 5 | describe RubyRNCryptor do 6 | 7 | before :each do 8 | @plain_text = "Hello, World! Let's use a few blocks with a longer sentence." 9 | @password = "P@ssw0rd!" 10 | end 11 | 12 | it "Decrypts v2 data with password" do 13 | encrypted_data = ["02013F194AA9969CF70C8ACB76824DE4CB6CDCF78B7449A87C679FB8EDB6A0109C513481DE877F3A855A184C4947F2B3E8FEF7E916E4739F9F889A717FCAF277402866341008A09FD3EBAC7FA26C969DD7EE72CFB695547C971A75D8BF1CC5980E0C727BD9F97F6B7489F687813BEB94DEB61031260C246B9B0A78C2A52017AA8C92"].pack('H*') 14 | decrypted = RubyRNCryptor.decrypt(encrypted_data, @password) 15 | expect(decrypted).to eq(@plain_text) 16 | end 17 | 18 | it "Decrypts v3 data with password" do 19 | encrypted_data = ["0301835b93e734143340ca8b55fc77865be906abe119073b77d5bc461fcc8bc8aea42fde3eb01b33bd3b54f2d58aaaef7747d24e1bde83aab5f81d7e68e3e2ba6c4f1420b638faea3d6dec7c801345d5bc059289f52b4d030786fc11e22a3939efd7c88a6cad3e23a9fc87e6bbfbc38901525b2ef7384045923260b3928a5bedbf7b"].pack('H*') 20 | decrypted = RubyRNCryptor.decrypt(encrypted_data, @password) 21 | expect(decrypted).to eq(@plain_text) 22 | end 23 | 24 | it "Decrypts sample v2 data with truncated password" do 25 | encrypted_data = ["0201ce29ed6bca00cf0c39390c1227284443f39133a35100539ba3f8fc84cf6da8d9db450233f2689858de35b40570f85bd2f81a70218ac72bf0f299fb33b35836d3141dffe9a96fd8b590d53086e1ca53d2"].pack('H*') 26 | decrypted = RubyRNCryptor.decrypt(encrypted_data, "中文密码") 27 | expect(decrypted).to eq("Attack at dawn") 28 | end 29 | 30 | it "Encrypt with password should decrypt" do 31 | encrypted = RubyRNCryptor.encrypt(@plain_text, @password) 32 | expect(RubyRNCryptor.decrypt(encrypted, @password)).to eq(@plain_text) 33 | end 34 | 35 | it "Encrypts and decrypts larger blocks of data" do 36 | bigger_data = OpenSSL::Random.random_bytes(4043) 37 | encrypted = RubyRNCryptor.encrypt(bigger_data, @password) 38 | expect(RubyRNCryptor.decrypt(encrypted, @password)).to eq(bigger_data) 39 | end 40 | 41 | it "Fails to decrypt when wrong password is used" do 42 | encrypted = RubyRNCryptor.encrypt(@plain_text, @password) 43 | expect { RubyRNCryptor.decrypt(encrypted, "WRONG") }.to raise_error("Password may be incorrect, or the data has been corrupted. (HMAC could not be verified)") 44 | end 45 | 46 | it "Raises an error when unsupported file format versions are requested" do 47 | expect { RubyRNCryptor.encrypt(@plain_text, @password, 1) }.to raise_error("RubyRNCryptor only encrypts version 2 or 3") 48 | end 49 | 50 | it "Should properly encrypt and decrypt multibyte passwords in v2" do 51 | encrypted = RubyRNCryptor.encrypt(@plain_text, "中文密码", 2) 52 | expect(RubyRNCryptor.decrypt(encrypted, "中文密码")).to eq(@plain_text) 53 | end 54 | 55 | it "Should properly encrypt and decrypt multibyte passwords in v3" do 56 | encrypted = RubyRNCryptor.encrypt(@plain_text, "中文密码") # default to v3 57 | expect(RubyRNCryptor.decrypt(encrypted, "中文密码")).to eq(@plain_text) 58 | end 59 | 60 | it "Should properly decrypt truncated multibyte passwords generated in Cocoa v2 format" do 61 | # This works around a bug in the Cocoa version where the password string was truncated 62 | # a string to the first x bytes where x is the number of multibyte characters in the string. 63 | 64 | encrypted = RubyRNCryptor.encrypt(@plain_text, RubyRNCryptor.truncate_multibyte_password("中文密码"), 2) 65 | expect(RubyRNCryptor.decrypt(encrypted, "中文中文")).to eq(@plain_text) 66 | end 67 | 68 | it "Should fail to decrypt v3 format multibyte passwords." do 69 | encrypted = RubyRNCryptor.encrypt(@plain_text, "中文密码") # default to v3 70 | expect { RubyRNCryptor.decrypt(encrypted, "中文中文") }.to raise_error("Password may be incorrect, or the data has been corrupted. (HMAC could not be verified)") 71 | end 72 | 73 | end 74 | --------------------------------------------------------------------------------