Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
se3000 committed Apr 25, 2017
2 parents 4c514ec + 6c69920 commit 75d5bff
Show file tree
Hide file tree
Showing 12 changed files with 333 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
/pkg/
/spec/reports/
/tmp/
.ruby-version
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [0.4.3]

### Added
- Eth::Key::Encrypter class to handle encrypting keys.
- Eth::Key.encrypt as a nice wrapper around Encrypter class.
- Eth::Key::Decrypter class to handle encrypting keys.
- Eth::Key.decrypt as a nice wrapper around Decrypter class.

## [0.4.2]

### Added
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,18 @@ key.private_hex
key.public_hex
key.address # EIP55 checksummed address
```
Or import and existing one:
Import an existing key:
```ruby
old_key = Eth::Key.new priv: private_key
```
Or decrypt an [encrypted key](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition):
```ruby
decrypted_key = Eth::Key.decrypt File.read('./some/path.json'), 'p455w0rD'
```
You can also encrypt your keys for use with other ethereum libraries:
```ruby
encrypted_key_info = Eth::Key.encrypt key, 'p455w0rD'
```

### Transactions

Expand Down Expand Up @@ -66,7 +74,7 @@ Eth::Utils.valid_address? address

Or add a checksum to an existing address:
```ruby
Eth::Utils.valid_address? "0x4bc787699093f11316e819b5692be04a712c4e69" # => "0x4bc787699093f11316e819B5692be04A712C4E69"
Eth::Utils.format_address "0x4bc787699093f11316e819b5692be04a712c4e69" # => "0x4bc787699093f11316e819B5692be04A712C4E69"
```

### Configure
Expand Down
14 changes: 14 additions & 0 deletions lib/eth/key.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
module Eth
class Key
autoload :Decrypter, 'eth/key/decrypter'
autoload :Encrypter, 'eth/key/encrypter'

attr_reader :private_key, :public_key

def self.encrypt(key, password)
key = new(priv: key) unless key.is_a?(Key)

Encrypter.perform key.private_hex, password
end

def self.decrypt(data, password)
priv = Decrypter.perform data, password
new priv: priv
end


def initialize(priv: nil)
@private_key = MoneyTree::PrivateKey.new key: priv
@public_key = MoneyTree::PublicKey.new private_key, compressed: false
Expand Down
87 changes: 87 additions & 0 deletions lib/eth/key/decrypter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
require 'json'

class Eth::Key::Decrypter
include Eth::Utils

def self.perform(data, password)
new(data, password).perform
end

def initialize(data, password)
@data = JSON.parse(data)
@password = password
end

def perform
derive_key password
check_macs
bin_to_hex decrypted_data
end


private

attr_reader :data, :key, :password

def derive_key(password)
@key = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest)
end

def check_macs
mac1 = keccak256(key[(key_length/2), key_length] + ciphertext)
mac2 = hex_to_bin crypto_data['mac']

if mac1 != mac2
raise "Message Authentications Codes do not match!"
end
end

def decrypted_data
@decrypted_data ||= cipher.update(ciphertext) + cipher.final
end

def crypto_data
@crypto_data ||= data['crypto'] || data['Crypto']
end

def ciphertext
hex_to_bin crypto_data['ciphertext']
end

def cipher_name
"aes-128-ctr"
end

def cipher
@cipher ||= OpenSSL::Cipher.new(cipher_name).tap do |cipher|
cipher.decrypt
cipher.key = key[0, (key_length/2)]
cipher.iv = iv
end
end

def iv
hex_to_bin crypto_data['cipherparams']['iv']
end

def salt
hex_to_bin crypto_data['kdfparams']['salt']
end

def iterations
crypto_data['kdfparams']['c'].to_i
end

def key_length
32
end

def digest
OpenSSL::Digest.new digest_name
end

def digest_name
"sha256"
end

end
127 changes: 127 additions & 0 deletions lib/eth/key/encrypter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
require 'json'

class Eth::Key::Encrypter
include Eth::Utils

def self.perform(key, password, options = {})
new(key, options).perform(password)
end

def initialize(key, options = {})
@key = key
@options = options
end

def perform(password)
derive_key password
encrypt

data.to_json
end

def data
{
crypto: {
cipher: cipher_name,
cipherparams: {
iv: bin_to_hex(iv),
},
ciphertext: bin_to_hex(encrypted_key),
kdf: "pbkdf2",
kdfparams: {
c: iterations,
dklen: 32,
prf: prf,
salt: bin_to_hex(salt),
},
mac: bin_to_hex(mac),
},
id: id,
version: 3,
}.tap do |data|
data[:address] = address unless options[:skip_address]
end
end

def id
@id ||= options[:id] || SecureRandom.uuid
end


private

attr_reader :derived_key, :encrypted_key, :key, :options

def cipher
@cipher ||= OpenSSL::Cipher.new(cipher_name).tap do |cipher|
cipher.encrypt
cipher.iv = iv
cipher.key = derived_key[0, (key_length/2)]
end
end

def digest
@digest ||= OpenSSL::Digest.new digest_name
end

def derive_key(password)
@derived_key = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest)
end

def encrypt
@encrypted_key = cipher.update(hex_to_bin key) + cipher.final
end

def mac
keccak256(derived_key[(key_length/2), key_length] + encrypted_key)
end

def cipher_name
"aes-128-ctr"
end

def digest_name
"sha256"
end

def prf
"hmac-#{digest_name}"
end

def key_length
32
end

def salt_length
32
end

def iv_length
16
end

def iterations
options[:iterations] || 262_144
end

def salt
@salt ||= if options[:salt]
hex_to_bin options[:salt]
else
SecureRandom.random_bytes(salt_length)
end
end

def iv
@iv ||= if options[:iv]
hex_to_bin options[:iv]
else
SecureRandom.random_bytes(iv_length)
end
end

def address
Eth::Key.new(priv: key).address
end

end
2 changes: 1 addition & 1 deletion lib/eth/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module Eth
VERSION = "0.4.2"
VERSION = "0.4.3"
end
13 changes: 13 additions & 0 deletions spec/eth/key/decrypter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
describe Eth::Key::Decrypter do

describe ".perform" do
let(:password) { 'testpassword' }
let(:key_data) { read_key_fixture password }

it "recovers the examle key" do
result = Eth::Key::Decrypter.perform key_data, password
expect(result).to eq('7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d')
end
end

end
51 changes: 51 additions & 0 deletions spec/eth/key/encrypter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
describe Eth::Key::Encrypter do

describe ".perform" do
let(:password) { 'testpassword' }
let(:key) { '7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d' }
let(:uuid) { "3198bc9c-6672-5ab3-d995-4942343ae5b6" }
let(:iv) { '6087dab2f9fdbbfaddc31a909735c1e6' }
let(:salt) { 'ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd' }
let(:options) do
{
iv: iv,
salt: salt,
id: uuid,
}
end

it "recovers the key" do
result = Eth::Key::Encrypter.perform key, password, options
json = JSON.parse(result)

expect(json['address']).to eq('0x008AeEda4D805471dF9b2A5B0f38A0C3bCBA786b')
expect(json['crypto']['cipher']).to eq('aes-128-ctr')
expect(json['crypto']['cipherparams']['iv']).to eq(iv)
expect(json['crypto']['ciphertext']).to eq('5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46')
expect(json['crypto']['kdf']).to eq('pbkdf2')
expect(json['crypto']['kdfparams']['c']).to eq(262_144)
expect(json['crypto']['kdfparams']['dklen']).to eq(32)
expect(json['crypto']['kdfparams']['prf']).to eq("hmac-sha256")
expect(json['crypto']['kdfparams']['salt']).to eq(salt)
expect(json['crypto']['mac']).to eq('517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2')
expect(json['id']).to eq(uuid)
expect(json['version']).to eq(3)
end

context "when specifying not to include the address" do
let(:options) do
{
skip_address: true,
}
end

it "recovers the key" do
result = Eth::Key::Encrypter.perform key, password, options
json = JSON.parse(result)
expect(json['address']).to be_nil
expect(json.keys).not_to include 'address'
end
end
end

end
16 changes: 16 additions & 0 deletions spec/eth/key_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,20 @@
it { is_expected.to eq('0x759b427456623a33030bbC2195439C22A8a51d25') }
it { is_expected.to eq(key.to_address) }
end

describe ".encrypt/.decrypt" do
# see: https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition

let(:password) { SecureRandom.base64 }
let(:key) { Eth::Key.new }

it "reads and writes keys in the Ethereum Secret Storage definition" do
encrypted = Eth::Key.encrypt key, password
decrypted = Eth::Key.decrypt encrypted, password

expect(key.address).to eq(decrypted.address)
expect(key.public_hex).to eq(decrypted.public_hex)
expect(key.private_hex).to eq(decrypted.private_hex)
end
end
end
1 change: 1 addition & 0 deletions spec/fixtures/keys/testpassword.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "crypto" : { "cipher" : "aes-128-ctr", "cipherparams" : { "iv" : "6087dab2f9fdbbfaddc31a909735c1e6" }, "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", "kdf" : "pbkdf2", "kdfparams" : { "c" : 262144, "dklen" : 32, "prf" : "hmac-sha256", "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" }, "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" }, "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", "version" : 3 }
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def configure_tx_data_hex(using_hex = true)
end
end

def read_key_fixture(path)
File.read "./spec/fixtures/keys/#{path}.json"
end

end

RSpec.configure do |config|
Expand Down

0 comments on commit 75d5bff

Please sign in to comment.