I was looking at the Geth implementation in Go, as well as keythereum. For generating the key, I am using the Gem https://github.com/se3000/ruby-eth. I verified with Geth that it generates the correct address for the private key.
require "eth"
require 'fileutils'
require 'json'
require 'openssl'
require 'securerandom'
require 'test/unit'
class SimpleTest < Test::Unit::TestCase
def test_create_keys
# hardcoded password
password = "password"
# create new keypair using https://github.com/se3000/ruby-eth
# This key generates address a8a659893d9d458d7c9ae88215c55f6a7cc561e6
keypair = Eth::Key.new priv: "a0d10d8045ed900e74a0a662e288646569f12a1624efcaed0c639d1f6d09e993"
# key version
version = 3
# Generate key uuid v4
uuid = SecureRandom.uuid
# address
address = keypair.to_address
# more on aes-128-ctr:
# https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_.28CTR.29
cipher_name = "aes-128-ctr"
cipher = OpenSSL::Cipher.new cipher_name
# iv "Initialization Vector"
iv = SecureRandom.random_bytes(16) #store this with the generated value
# Derive encryption key from password
# kdf = key derivation function, for password-based keys
digest_name = "sha256"
prf = "hmac-" + digest_name
digest = OpenSSL::Digest.new digest_name
key_length = 32
salt_length = 32
iterations = 65536
salt = SecureRandom.random_bytes(salt_length) #store this with the generated value
derived_key = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest)
# Encrypt private key with key derived from password
cipher.encrypt
cipher.iv = iv
iv = cipher.random_iv
cipher.key = derived_key
encrypted = cipher.update([keypair.private_key.to_hex].pack("H*")) + cipher.final
# Generate mac
derived_key_hex = derived_key.unpack("H*")[0]
encrypted_hex = encrypted.unpack("H*")[0]
mac = Eth::Utils.keccak256(derived_key_hex[32, 64] + encrypted)
json = {
"address" => address,
"crypto" => {
"cipher" => cipher_name,
"ciphertext" => encrypted_hex,
"cipherparams" => {
"iv" => iv.unpack("H*")[0]
},
"mac" => mac.unpack("H*")[0],
"kdf" => "pbkdf2",
"kdfparams" => {
"c" => iterations,
"dklen" => key_length,
"prf" => prf,
"salt" => salt.unpack("H*")[0],
}
},
"id": uuid,
"version": version
}
file_name = "UTC--" + Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%LZ") + "--" + address
FileUtils.mkdir_p 'keystore'
File.open("keystore/" + file_name,"w") do |f|
f.write(json.to_json)
end
end
end
> personal.listAccounts
["0xf31081983dcf4984304c14563f82ab29a630144d"]["0xa8a659893d9d458d7c9ae88215c55f6a7cc561e6"]
> personal.unlockAccount("0xf31081983dcf4984304c14563f82ab29a630144d""0xa8a659893d9d458d7c9ae88215c55f6a7cc561e6")
Unlock account 0xf31081983dcf4984304c14563f82ab29a630144d0xa8a659893d9d458d7c9ae88215c55f6a7cc561e6
Passphrase:
could not decrypt key with given passphrase