mitchkman
mitchkman

Reputation: 6680

Encrypting a private key in Ruby, using aes-128-ctr + scrypt

I need to build a private key encryption for Ethereum, which should be compatible to the go-ethereum implementation (Ruby-encrypted keys should work with the Ethereum implementation as well).

Ethereum uses a 32-bit private key, like this one, for example (hex encoded):

1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

If I import this key the go-ethereum implementation and encrypting it with the password "password", it generates this output:

{
    "address":"1be31a94361a391bbafb2a4ccd704f57dc04d4bb",
    "crypto":{
        "cipher":"aes-128-ctr",
        "ciphertext":"62bbf1a5a93b8ba8c66b70b3381f9f5badf44b35287614d309d760ebeec47139",
        "cipherparams":{
            "iv":"a4a6638ea73872c07d62fa065f37f790"
        },
        "kdf":"scrypt",
        "kdfparams":{
            "dklen":32,
            "n":262144,
            "p":1,
            "r":8,
            "salt":"69ccd8c258bb50ac2effd65837e09e45b8bd9a747a1a1f3558b65a16e2f46f1a"
        },
        "mac":"68ca6bc011d4d656e12a34cefd28005dbf76d9cfac15db2eaa83920eec5b38a9"
    },
    "id":"9863070b-6c16-4aef-8188-2a34660192bf",
    "version":3
}

So using all the kdf (key derivation function) parameters, it generates the cipher text

62bbf1a5a93b8ba8c66b70b3381f9f5badf44b35287614d309d760ebeec47139

I now try to reproduce the same cipher text using Ruby, also looking at the Go implementation. This is my code:

# hard coded password
password = "password"

# hard coded test private key
plain_private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
puts "------------ Encryption input ------------ "
puts "Clear private key = " + plain_private_key

# Scrypt params, same as in Geth/Ethereum
n = 262144
r = 8
p = 1
dklen = 32

# using same salt as Ethereum used
salt = "69ccd8c258bb50ac2effd65837e09e45b8bd9a747a1a1f3558b65a16e2f46f1a"
# using same iv as Ethereum used
iv = "a4a6638ea73872c07d62fa065f37f790"

puts "------------ Scrypt parameters ------------ "
puts "Salt str = " + salt
puts "Iv str = " + iv
puts "n = " + n.to_s
puts "r = " + r.to_s
puts "p = " + p.to_s
puts "dklen = " + dklen.to_s

# Generate derived key
derived_key = SCrypt::Engine.scrypt(password, salt, n, r, p, dklen)
puts "------------ Scrypt output ------------ "
puts "Derived key from password = " + derived_key.unpack("H*")[0]

# Encrypt with derived key
cipher_name = "aes-128-ctr"
cipher = OpenSSL::Cipher.new cipher_name
cipher.encrypt
cipher.iv = iv
cipher.key = derived_key
encrypted = cipher.update([plain_private_key].pack("H*")) + cipher.final
puts "------------ Encryption output ------------ "
puts "Cipher text = " + encrypted.unpack("H*")[0]

# Decrypt with derived key
decipher = OpenSSL::Cipher.new cipher_name
decipher.decrypt
decipher.iv = iv
decipher.key = derived_key
decrypted = decipher.update(encrypted) + decipher.final
decrypted_str = decrypted.unpack("H*")[0]
puts "------------ Decryption output ------------ "
puts "Decrypted: " + decrypted_str
puts "Decryption worked: " + (plain_private_key == decrypted_str).to_s

This is the output:

------------ Encryption input ------------
Clear private key = 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
------------ Scrypt parameters ------------
Salt str = 69ccd8c258bb50ac2effd65837e09e45b8bd9a747a1a1f3558b65a16e2f46f1a
Iv str = a4a6638ea73872c07d62fa065f37f790
n = 262144
r = 8
p = 1
dklen = 32
------------ Scrypt output ------------
Derived key = b6e4410aa658f21213c7e55bacbbd8093e67f7f1738e7235335b58a2b690dcf5
------------ Encryption output ------------
Cipher text = 6fddd3d2199edf65a17d9277d2328f5357e70a5be2e173d17681883ef5a3a27e
------------ Decryption output ------------
Decrypted: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
Decryption worked: true

But the cipher text is different from what go-ethereum generated, using the same inputs and parameters.

6fddd3d2199edf65a17d9277d2328f5357e70a5be2e173d17681883ef5a3a27e

Can anybody help me out?

Upvotes: 2

Views: 1386

Answers (1)

matt
matt

Reputation: 79783

The salt for the key derivation and the iv for the encryption both need to be converted from hex to binary strings, the same way as you do for the private key:

# using same salt as Ethereum used
salt = ["69ccd8c258bb50ac2effd65837e09e45b8bd9a747a1a1f3558b65a16e2f46f1a"].pack('H*')
# using same iv as Ethereum used
iv = ["a4a6638ea73872c07d62fa065f37f790"].pack('H*')

This gives the same result for the encrypted key as the go implementation:

------------ Encryption output ------------
Cipher text = 62bbf1a5a93b8ba8c66b70b3381f9f5badf44b35287614d309d760ebeec47139

Something else I noticed, that isn’t related to your immediate problem: the encryption and decryption only use the first 16 bytes of the derived key. Currently the Ruby OpenSSL bindings just truncate the key to the correct length so everthing works at the moment, but this will change in future releases. This means your code won’t work as it is after you upgrade. You’ll need to provide the correct key length:

cipher.key = derived_key[0...16]

The other 16 bytes of the derived key are used as an authentication key, so you can check if anything has been tampered with (you would need a Ruby implementation of the Keccak hash function to implement that).

Upvotes: 1

Related Questions