Convert a binary string (SecureRandom.random_bytes) into a hexadecimal string? - ruby

I'm generating a 32 byte key and 16 byte iv for my AES-256 CBC Ruby encryption implementation:
key = SecureRandom.random_bytes(32) # => "m\xD4\x90\x85\xF9\xCD\x13\x98\xAB\v\xBB\xCD\x0E\x17\xFAA\xF9\x99\xAF\e\x8A\xB5\x8Ate\x93[m\x9As\xC7\xCB"
iv = SecureRandom.random_bytes(16) # => "\xDF\x95[\xD5\xDD(\x0F\xB8SE\xFCZr\xF1\xB1W"
ruby_cipher = SymmetricEncryption::Cipher.new(
key: key,
iv: iv,
cipher_name: 'aes-256-cbc'
)
ruby_cipher.encrypt("Hello!") # => 'qAnTLy7jyiLRkUqBnME8sw=='
Question:
How do I convert the key and iv to a hexadecimal string, so I can transport them to the other applications?
Context:
In another application, that uses Javascript via CryptoJS I need to receive the key and iv and convert them back to bytes like this:
CryptoJS.AES.encrypt(
"Hello!",
CryptoJS.enc.Utf8.parse(key),
{ iv: CryptoJS.enc.Utf8.parse(iv) }
).toString() // 'qAnTLy7jyiLRkUqBnME8sw=='
In a third PHP application I will use the Hex strings directly like this:
<?php
openssl_encrypt(
'Hello!', 'aes-256-cbc',
key,
0,
iv
); // => 'qAnTLy7jyiLRkUqBnME8sw=='

I think this should do the job:
key = SecureRandom.random_bytes(32)
key_as_str = key.each_byte.map{ |byte| '%02x' % byte }.join
I did verify this solution with the following scripts:
test.rb
require 'securerandom'
require 'symmetric-encryption'
key = SecureRandom.random_bytes(32)
iv = SecureRandom.random_bytes(16)
ruby_cipher = SymmetricEncryption::Cipher.new(
key: key,
iv: iv,
cipher_name: 'aes-256-cbc'
)
hex_key = key.each_byte.map{ |byte| '%02x' % byte }.join
hex_iv = iv.each_byte.map{ |byte| '%02x' % byte }.join
encoded = ruby_cipher.encrypt("Hello!")
puts "Ruby encoded: #{encoded}"
system("php test.php #{hex_key} #{hex_iv}")
test.php
<?php
$encoded = openssl_encrypt(
'Hello!', 'aes-256-cbc',
hex2bin($argv[1]),
0,
hex2bin($argv[2])
);
print "php encoded: $encoded\n";
looks the same on my machine.

You could do it the same way SecureRandom does for their #hex method:
key = SecureRandom.random_bytes(32)
key_as_hex = key.unpack('H*')[0]
(Using a single-quoted string works fine here for this non-interpolated string, and is rubocop friendly.)

Related

How to decrypt of AES-256-GCM created with ruby in sjcl.js

I'm trying to decrypt an AES cipher generated by Ruby with the sjcl.js library.
I'm getting a "corrupt" error for an unknown reason……. I want to fix the problem.
For reference, when encryption and decryption were attempted in CBC mode, decryption was successful.
Ruby Code:
cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.encrypt
iv = cipher.random_iv
cipher.key = Digest::SHA256.digest(password)
ciphertext = cipher.update(plaintext) + cipher.final
return Base64.strict_encode64(iv) + Base64.strict_encode64(ciphertext)
Javascript Code:
var iv = sjcl.codec.base64.toBits(IV_BASE64);
var ciphertext = sjcl.codec.base64.toBits(CIPHERTEXT_BASE64);
var key = sjcl.hash.sha256.hash(KEY_UTF8);
var decrypted = sjcl.mode.gcm.decrypt(new sjcl.cipher.aes(key), ciphertext, iv);
AES-GCM is an authenticated encryption algorithm. It automatically generates an authentication tag during encryption, which is used for authentication during decryption. This tag is not considered in the current Ruby code. It is 16 bytes by default, can be retrieved with cipher.auth_tag and must be added, e.g.:
ciphertext = cipher.update(plaintext) + cipher.final + cipher.auth_tag
Regarding nonce/IV, note that Base64 encoding should actually be done after concatenation (which, however, is not critical for a 12 bytes nonce/IV commonly used with GCM).
On the JavaScript side the separation of the nonce/IV is missing. Ciphertext and tag do not need to be separated because the sjcl processes the concatenation of both (ciphertext|tag):
const GCM_NONCE_LENGTH = 12 * 8
const GCM_TAG_LENGTH = 16 * 8
// Separate IV and ciptertext/tag combination
let ivCiphertextTagB64 = "2wLsVLuOJFX1pfwwjoLhQrW7f/86AefyZ7FwJEhJVIpU+iG2EITzushCpDRxgqK2cwVYvfNt7KFZ39obMMmIqhrDCIeifzs="
let ivCiphertextTag = sjcl.codec.base64.toBits(ivCiphertextTagB64)
let iv = sjcl.bitArray.bitSlice(ivCiphertextTag, 0, GCM_NONCE_LENGTH)
let ciphertextTag = sjcl.bitArray.bitSlice(ivCiphertextTag, GCM_NONCE_LENGTH)
// Derive key via SHA256
let key = sjcl.hash.sha256.hash("my password")
// Decrypt
let cipher = new sjcl.cipher.aes(key)
let plaintext = sjcl.mode.gcm.decrypt(cipher, ciphertextTag, iv, null, GCM_TAG_LENGTH)
//let plaintext = sjcl.mode.gcm.decrypt(cipher, ciphertextTag, iv) // works also; here the defaults for the AAD ([]) and the tag size (16 bytes) are applied
console.log(sjcl.codec.utf8String.fromBits(plaintext))
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/sjcl/1.0.8/sjcl.min.js "></script>
The ciphertext used in the above code was generated with the Ruby code considering the authentication tag and is successfully decrypted.
Note that key derivation with a digest is insecure. Instead, a reliable key derivation function like PBKDF2 should be used.

Can't decrypt Ruby encrypted string in Scala

I'm using ActiveSupport::MessageEncryptor to encrypt a string in Ruby as follows:
salt = SecureRandom.random_bytes(64)
// => "s\x90L\xB8\xEF\x8BBp\xB6\xF5A\x95\xA8]+\x94\xF3\xA7\x9A\x84+jC\xBF\xB0\x15\xEF*\x8C\xDD.\xE5\xC7Y\xCE\xE1\xAA\xA4I/%.\x9E\x14\xC1\xA8\x9E\x122\xE0\x19.\x19\xD8\xB6\xE8\x83\xE1\xFE\x16\xB5\x11N\x18"
key = ActiveSupport::KeyGenerator.new('password').generate_key(salt)
// => "\x12\xBD1\xA0Q\xBF)\\\x89\xDF\x95\xD0\f\x03\x17P'\x87\xAD\x92b\xB5%\xC7X\x01\x9Ar\xCB\xC9\x1A\x10'\xC4\x95w\xBF\xED]\x17\xEB\x9F#\xC6\xEE8S\xE1^\x18\xE2^\x85Z\rJ\x9A\xEE\xA5\xEC|\xA2\xA9\x8E"
crypt = ActiveSupport::MessageEncryptor.new(key)
encrypted_username = crypt.encrypt_and_sign("daniel")
// => "N0dHcFM3MnQrcW1HUk9UTGwxeUJsZmlCNzcwUGhrdUdtbE9YWnUxamZFST0tLUVUcUlIU2k1ZHIvTmlDRUgzM2FsS0E9PQ==--1ede80eb2b498ddf5133f8f3a45a82db2476c740"
Then in Scala I'm trying to decrypt like so:
var encrypted_str = "N0dHcFM3MnQrcW1HUk9UTGwxeUJsZmlCNzcwUGhrdUdtbE9YWnUxamZFST0tLUVUcUlIU2k1ZHIvTmlDRUgzM2FsS0E9PQ==--1ede80eb2b498ddf5133f8f3a45a82db2476c740"
val parts = encrypted_str.split("--");
val encryptedData = Base64.decodeBase64(parts(0))
val iv = Base64.decodeBase64(parts(1))
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(iv.take(16)));
val result = cipher.doFinal(encryptedData);
println(new String(result, "UTF-8"))
But I run into this error:
javax.crypto.IllegalBlockSizeException: Input length must be multiple of 16 when decrypting with padded cipher
I think the problem might be here:
val iv = Base64.decodeBase64(parts(1))
The second part of your message is 1ede80eb2b498ddf5133f8f3a45a82db2476c740 which is not a base 64 encoded string, it's a hex encoded string. Try this instead:
Hex.decodeHex(parts(1))

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?
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).

AES 128 CTR Mode in Ruby

I'm trying to decrypt some cipher text in CBC and CTR mode. The IV is said to be the first 16 bytes of the cipher text. I wrote this function in Ruby to solve it and with CBC, I got the correct results but as soon as I changed the mode to CTR, I get a random string of bytes.
This post and this post ask similar questions, but neither were using the correct IV text and I've check mine multiple times.
def decrypt(key, cipher, mode=:CBC)
k = [key].pack('H*')
iv = [cipher.scan(/../).first(16).join].pack('H*')
c = [cipher].pack('H*')
aes = OpenSSL::Cipher::AES.new(128, mode)
aes.decrypt
aes.iv = iv
aes.key = k
aes.update(c) + aes.final
end
With mode=CBC (Correct with disregared first few bytes):
decrypt('140b41b22a29beb4061bda66b6747e14', '4ca00ff4c898d61e1edbf1800618fb2828a226d160dad07883d04e008a7897ee2e4b7465d5290d0c0e6c6822236e1daafb94ffe0c5da05d9476be028ad7c1d81')
=> "LQ\xFCXTr#\t\xC7\eb\x0Ex\xB3\nQBasic CBC mode encryption needs padding."
With mode=CTR:
decrypt('36f18357be4dbd77f050515c73fcf9f2', '69dda8455c7dd4254bf353b773304eec0ec7702330098ce7f7520d1cbbb20fc388d1b0adb5054dbd7370849dbf0b88d393f252e764f1f5f7ad97ef79d59ce29f5f51eeca32eabedd9afa9329', :CTR)
=> "$N\x8AF\x01\e<\xA7\x9C\xCD;\xDF\xBB\xA28#\xF36\xA2\xFB\xEC`\xA5z\xE5\x02\xFA\xF5v\xDC\xE6};#\x8B:\xB9\x91\xCAj\xB8\x95\x04\x89J\xF6J.\xA0\xCC\xDFFvx\"Z_\b\x0E~[\x1F\x92+&U\xEB\x9E\xE0\xA7}\r\xC9Y?\xB2"
Thanks in advance for any help!
Thanks to Maarten's help in the comments above, I was able to refine my method to not include the IV in the cipher text when decrypting.
def decrypt(key, cipher, mode=:CBC)
k = [key].pack('H*')
bytes = cipher.scan(/../)
iv = [bytes[0..15].join].pack('H*')
c = [bytes[16..-1].join].pack('H*')
aes = OpenSSL::Cipher::AES128.new(mode)
aes.decrypt
aes.iv = iv
aes.key = k
aes.update(c) + aes.final
end

Random Number Generator with AES Counter Mode - Ruby

my goal is to implement a random number generator based on AES Counter Mode in Ruby.
I have implemented the counter mode myself as follows:
require './aes_helpers'
require 'openssl'
class AES_CTR_Random
# Setup basic aes, since a mode is required ecb is used,
# which constructs same cipher text blocks for same plain text blocks
def intialize_openssl_aes(encrypt_or_decrypt, mode, key, iv, data, bits)
key_s = byte_array_to_byte_string(key)
data_s = byte_array_to_byte_string(data)
aes = OpenSSL::Cipher::AES.new(bits, mode)
aes.send(encrypt_or_decrypt)
aes.padding = 0
aes.key = key_s
aes.iv = byte_array_to_byte_string(iv) if iv
encrypted = aes.update(data_s) + aes.final
byte_string_to_byte_array(encrypted)
end
# Encrypt or decrypt one AES block
def process_aes_block(encrypt_or_decrypt, key, block)
intialize_openssl_aes(encrypt_or_decrypt, 'ECB', key, nil, block, 128)
end
# Generate a stream cipher key using given AES key and initialization vector
def generate_aes_ctr_stream_key(key, iv, length)
iv = byte_array_to_integer(iv)
stream_key = []
while stream_key.length < length
stream_key += process_aes_block(:encrypt, key, integer_to_byte_array(iv))
iv += 1
end
stream_key.take(length)
end
# Run Cipher Counter Mode
def run_ctr(key, iv, data)
# Get a properly sized stream cipher key using aes cipher counter mode
stream_key = generate_aes_ctr_stream_key(key, iv, data.length)
# Stream cipher decryption
byte_array_xor_byte_array(stream_key, data)
end
# Encrypt plaintext with aes 128 ctr using given key and iv
def encrypt_ctr(key, iv, plaintext)
key = hexadecimal_string_to_byte_buffer(key)
iv = hexadecimal_string_to_byte_buffer(iv)
plaintext = byte_string_to_byte_array(plaintext)
encrypted = run_ctr(key, iv, plaintext)
byte_buffer_to_hexadecimal_string(iv + encrypted)
end
# Decrypt aes 128 ctr encrypted ciphertext
def decrypt_ctr(key, ciphertext)
key = hexadecimal_string_to_byte_buffer(key)
ciphertext = hexadecimal_string_to_byte_buffer(ciphertext)
iv = ciphertext.take(16)
ciphertext = ciphertext.drop(16)
plaintext = run_ctr(key, iv, ciphertext)
byte_array_to_byte_string(plaintext)
end
end
But now I do not quite understand how to get random numbers out of this implementation.
Can anyone guide me to a solution?
You could use AES to build CTR_DRBG as specified in NIST Special Publication 800-90A, section 10.2: 10.2 DRBG Mechanisms Based on Block Ciphers which uses CTR block cipher mode of operation as underlying primitive.
A stream cipher has the disadvantage that it doesn't repeat blocks, which may slightly bias the output. A previous answer mentioned AES-CTR, which has this disadvantage.

Resources