Module: AesBridge

Defined in:
lib/aes_bridge.rb,
lib/aes_bridge/cbc.rb,
lib/aes_bridge/gcm.rb,
lib/aes_bridge/common.rb,
lib/aes_bridge/legacy.rb,
lib/aes_bridge/version.rb

Defined Under Namespace

Classes: Error, RandomGenerator

Constant Summary collapse

BLOCK_SIZE =
16
KEY_LEN =
32
IV_LEN =
16
VERSION =
"2.0.0"

Class Method Summary collapse

Class Method Details

.decrypt(data, passphrase) ⇒ String

Decrypts a base64-encoded string encrypted with AES-GCM and verifies its integrity using an authentication tag.

Parameters:

  • data (String)

    The base64-encoded ciphertext to decrypt.

  • passphrase (String)

    The passphrase from which to derive the encryption and HMAC keys.

Returns:

  • (String)

    The decrypted plaintext.



26
27
28
# File 'lib/aes_bridge.rb', line 26

def self.decrypt(data, passphrase)
  self.decrypt_gcm(data, passphrase)
end

.decrypt_cbc(data, passphrase) ⇒ String

Decrypts a base64-encoded string encrypted with AES-CBC-256 and verifies its integrity using HMAC-SHA-256.

Parameters:

  • data (String)

    The base64-encoded ciphertext to decrypt.

  • passphrase (String)

    The passphrase from which to derive the encryption and HMAC keys.

Returns:

  • (String)

    The decrypted plaintext.



104
105
106
# File 'lib/aes_bridge/cbc.rb', line 104

def self.decrypt_cbc(data, passphrase)
  decrypt_cbc_bin(Base64.decode64(data), passphrase)
end

.decrypt_cbc_bin(data, passphrase) ⇒ String

Decrypts the given ciphertext using AES-CBC-256 and HMAC-SHA-256 for integrity verification.

Parameters:

  • data (String)

    The ciphertext to decrypt.

  • passphrase (String)

    The passphrase from which to derive the encryption and HMAC keys.

Returns:

  • (String)

    The decrypted plaintext.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/aes_bridge/cbc.rb', line 62

def self.decrypt_cbc_bin(data, passphrase)
  data = self.to_bytes(data)
  passphrase = self.to_bytes(passphrase)

  salt = data[0, 16]
  iv = data[16, 16]
  tag = data[-32, 32]
  ciphertext = data[32...-32]

  aes_key, hmac_key = self.derive_keys_cbc(passphrase, salt)

  expected_tag = OpenSSL::HMAC.digest('sha256', hmac_key, iv + ciphertext)
  raise 'HMAC verification failed' unless expected_tag == tag

  cipher = OpenSSL::Cipher.new('aes-256-cbc')
  cipher.decrypt
  cipher.key = aes_key
  cipher.iv = iv

  # fix empty plaintext decryption
  if ciphertext.bytesize == 0
    return ''
  end
  cipher.update(ciphertext) + cipher.final
end

.decrypt_gcm(data, passphrase) ⇒ String

Decrypts a base64-encoded string encrypted with AES-GCM and verifies its integrity using an authentication tag.

Parameters:

  • data (String)

    The base64-encoded ciphertext to decrypt.

  • passphrase (String)

    The passphrase from which to derive the encryption and HMAC keys.

Returns:

  • (String)

    The decrypted plaintext.



89
90
91
# File 'lib/aes_bridge/gcm.rb', line 89

def self.decrypt_gcm(data, passphrase)
  decrypt_gcm_bin(Base64.decode64(to_bytes(data)), passphrase)
end

.decrypt_gcm_bin(data, passphrase) ⇒ String

Decrypts a binary string encrypted with AES-GCM.

Parameters:

  • data (String)

    The ciphertext to decrypt.

  • passphrase (String)

    The passphrase from which to derive the encryption key.

Returns:

  • (String)

    The decrypted plaintext.



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/aes_bridge/gcm.rb', line 52

def self.decrypt_gcm_bin(data, passphrase)
  data = to_bytes(data)
  passphrase = to_bytes(passphrase)

  salt = data[0,16]
  nonce = data[16,12]
  tag = data[-16,16]
  ciphertext = data[28...-16]

  cipher = OpenSSL::Cipher.new('aes-256-gcm')
  cipher.decrypt
  cipher.key = derive_key_gcm(passphrase, salt)
  cipher.iv = nonce
  cipher.auth_tag = tag

  # fix empty plaintext decryption
  if ciphertext.bytesize == 0
    return ''
  end
  cipher.update(ciphertext) + cipher.final
end

.decrypt_legacy(enc, passphrase) ⇒ String

Decrypts the given ciphertext using the legacy AES Everywhere format with AES-256-CBC. The ciphertext must have a “Salted__” prefix.

Parameters:

  • enc (String)

    The base64-encoded ciphertext to decrypt.

  • passphrase (String)

    The passphrase used for key derivation.

Returns:

  • (String)

    The decrypted plaintext.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/aes_bridge/legacy.rb', line 43

def self.decrypt_legacy(enc, passphrase)
  data = Base64.decode64(enc)
  raise 'Invalid OpenSSL header' unless data.start_with?('Salted__')

  salt = data[8, 8]
  key, iv = derive_key_iv_legacy(passphrase, salt)

  cipher = OpenSSL::Cipher.new('AES-256-CBC')
  cipher.decrypt
  cipher.key = key
  cipher.iv = iv
  ciphertext = data[16..]

  # fix empty plaintext decryption
  if ciphertext.bytesize == 0
    return ''
  end
  cipher.update(ciphertext) + cipher.final
end

.derive_key_gcm(passphrase, salt) ⇒ Object



9
10
11
12
13
14
15
16
17
# File 'lib/aes_bridge/gcm.rb', line 9

def self.derive_key_gcm(passphrase, salt)
  OpenSSL::KDF.pbkdf2_hmac(
    passphrase,
    salt: salt,
    iterations: 100_000,
    length: 32,
    hash: 'sha256'
  )
end

.derive_key_iv_legacy(passphrase, salt) ⇒ Array<String>

Derives an AES key and initialization vector (IV) from a passphrase and salt using an iterative hashing process.

Parameters:

  • passphrase (String)

    The passphrase used for key derivation.

  • salt (String)

    The salt value to add randomness to the key derivation process.

Returns:

  • (Array<String>)

    An array containing the derived AES key and IV.



69
70
71
72
73
74
75
76
77
# File 'lib/aes_bridge/legacy.rb', line 69

def self.derive_key_iv_legacy(passphrase, salt)
  d = +''
  prev = +''
  while d.bytesize < KEY_LEN + IV_LEN
    prev = OpenSSL::Digest::MD5.digest(prev + passphrase.b + salt.b)
    d << prev
  end
  [d[0, KEY_LEN], d[KEY_LEN, IV_LEN]]
end

.derive_keys_cbc(passphrase, salt) ⇒ Array<String>

Derives AES and HMAC keys from a passphrase and salt using PBKDF2-HMAC-SHA256.

Parameters:

  • passphrase (String)

    The passphrase from which to derive the keys.

  • salt (String)

    The salt value to use in the key derivation process.

Returns:

  • (Array<String>)

    An array containing the derived AES key and HMAC key.



15
16
17
18
19
20
21
22
23
24
# File 'lib/aes_bridge/cbc.rb', line 15

def self.derive_keys_cbc(passphrase, salt)
  key_material = OpenSSL::KDF.pbkdf2_hmac(
    passphrase,
    salt: salt,
    iterations: 100_000,
    length: 64,
    hash: 'sha256'
  )
  [key_material[0, 32], key_material[32, 32]]
end

.encrypt(plaintext, passphrase) ⇒ String

Encrypts a string using AES-GCM.

Parameters:

  • data (String)

    The plaintext to encrypt.

  • passphrase (String)

    The passphrase from which to derive the encryption key.

Returns:

  • (String)

    The encrypted, base64-encoded string.



16
17
18
# File 'lib/aes_bridge.rb', line 16

def self.encrypt(plaintext, passphrase)
  self.encrypt_gcm(plaintext, passphrase)
end

.encrypt_cbc(data, passphrase) ⇒ String

Encrypts the given plaintext using AES-CBC-256 with a randomly generated IV, and HMAC-SHA-256 for integrity verification.

Parameters:

  • plaintext (String)

    The plaintext to encrypt.

  • passphrase (String)

    The passphrase from which to derive the encryption and HMAC keys.

Returns:

  • (String)

    The encrypted, base64-encoded string.



95
96
97
# File 'lib/aes_bridge/cbc.rb', line 95

def self.encrypt_cbc(data, passphrase)
  Base64.strict_encode64(encrypt_cbc_bin(data, passphrase))
end

.encrypt_cbc_bin(plaintext, passphrase) ⇒ String

Encrypts the given plaintext using AES-CBC-256 with a randomly generated IV, and HMAC-SHA-256 for integrity verification.

Parameters:

  • plaintext (String)

    The plaintext to encrypt.

  • passphrase (String)

    The passphrase from which to derive the encryption and HMAC keys.

Returns:

  • (String)

    The encrypted binary data.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/aes_bridge/cbc.rb', line 32

def self.encrypt_cbc_bin(plaintext, passphrase)
  plaintext = self.to_bytes(plaintext)
  passphrase = self.to_bytes(passphrase)
  salt = self.generate_random(16)
  iv = self.generate_random(16)
  aes_key, hmac_key = self.derive_keys_cbc(passphrase, salt)

  cipher = OpenSSL::Cipher.new('aes-256-cbc')
  cipher.encrypt
  cipher.key = aes_key
  cipher.iv = iv

  # fix empty plaintext encryption
  if plaintext.bytesize > 0
    ciphertext = cipher.update(plaintext) + cipher.final
  else
    ciphertext = '' + cipher.final
  end

  tag = OpenSSL::HMAC.digest('sha256', hmac_key, iv + ciphertext)
  salt + iv + ciphertext + tag
end

.encrypt_gcm(data, passphrase) ⇒ String

Encrypts a string using AES-GCM.

Parameters:

  • data (String)

    The plaintext to encrypt.

  • passphrase (String)

    The passphrase from which to derive the encryption key.

Returns:

  • (String)

    The encrypted, base64-encoded string.



79
80
81
# File 'lib/aes_bridge/gcm.rb', line 79

def self.encrypt_gcm(data, passphrase)
  Base64.strict_encode64(encrypt_gcm_bin(data, passphrase))
end

.encrypt_gcm_bin(plaintext, passphrase) ⇒ String

Encrypts the given plaintext using AES-GCM.

Parameters:

  • plaintext (String)

    The plaintext to encrypt.

  • passphrase (String)

    The passphrase from which to derive the encryption key.

Returns:

  • (String)

    The encrypted binary data.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/aes_bridge/gcm.rb', line 24

def self.encrypt_gcm_bin(plaintext, passphrase)
  passphrase = to_bytes(passphrase)
  plaintext = to_bytes(plaintext)
  salt = generate_random(16)
  nonce = generate_random(12)
  key = derive_key_gcm(passphrase, salt)

  cipher = OpenSSL::Cipher.new('aes-256-gcm')
  cipher.encrypt
  cipher.key = key
  cipher.iv = nonce

  # fix empty plaintext encryption
  if plaintext.bytesize > 0
    ciphertext = cipher.update(plaintext) + cipher.final
  else
    ciphertext = '' + cipher.final
  end

  tag = cipher.auth_tag
  salt + nonce + ciphertext + tag
end

.encrypt_legacy(raw, passphrase) ⇒ String

Encrypts the given plaintext using the legacy AES Everywhere format with AES-256-CBC. A random salt is generated and used along with the passphrase to derive the encryption key and IV.

Parameters:

  • raw (String)

    The plaintext to encrypt.

  • passphrase (String)

    The passphrase used for key derivation.

Returns:

  • (String)

    The encrypted data, encoded in base64 format, with a “Salted__” prefix.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/aes_bridge/legacy.rb', line 17

def self.encrypt_legacy(raw, passphrase)
  salt = OpenSSL::Random.random_bytes(8)
  key, iv = derive_key_iv_legacy(passphrase, salt)

  cipher = OpenSSL::Cipher.new('AES-256-CBC')
  cipher.encrypt
  cipher.key = key
  cipher.iv = iv

  # fix empty plaintext encryption
  if raw.bytesize > 0
    encrypted = cipher.update(raw) + cipher.final
  else
    encrypted = ''
  end

  result = "Salted__" + salt + encrypted
  Base64.strict_encode64(result)
end

.generate_random(size) ⇒ String

Generates a cryptographically secure random string of a given size.

Parameters:

  • size (Integer)

    The length of the random string to generate.

Returns:

  • (String)

    A cryptographically secure random string of length ‘size`.



61
62
63
64
# File 'lib/aes_bridge/common.rb', line 61

def self.generate_random(size)
  generator = RandomGenerator.new
  generator.generate_random_bytes(size)
end

.to_bytes(data) ⇒ String

Converts data to bytes. If data is a String, it’s converted to binary encoding. Otherwise, it’s returned as is.

Parameters:

  • data (String, Object)

    The data to convert.

Returns:

  • (String)

    The data in binary encoding, or the original data if not a String.



14
15
16
# File 'lib/aes_bridge/common.rb', line 14

def self.to_bytes(data)
  data.is_a?(String) ? data.b : data
end