Module: Philiprehberger::Crypt
- Defined in:
- lib/philiprehberger/crypt.rb,
lib/philiprehberger/crypt/version.rb
Defined Under Namespace
Classes: DecryptionError, Error
Constant Summary collapse
- CIPHER =
'aes-256-gcm'- IV_LENGTH =
12- AUTH_TAG_LENGTH =
16- KEY_LENGTH =
32- SALT_LENGTH =
32- PBKDF2_ITERATIONS =
100_000- HASH_ALGORITHMS =
{ sha256: 'SHA256', sha384: 'SHA384', sha512: 'SHA512' }.freeze
- VERSION =
'0.4.0'
Class Method Summary collapse
-
.decrypt(data, key:) ⇒ String
Decrypt data encrypted with Crypt.encrypt.
-
.derive_key(password, salt:, iterations: PBKDF2_ITERATIONS) ⇒ String
Derive an encryption key from a password using PBKDF2-HMAC-SHA256.
-
.encrypt(data, key:) ⇒ String
Encrypt data using AES-256-GCM.
-
.envelope_decrypt(envelope, master_key:) ⇒ String
Decrypt data encrypted with Crypt.envelope_encrypt.
-
.envelope_encrypt(data, master_key:) ⇒ Hash
Encrypt data using envelope encryption.
-
.hash(data, algorithm: :sha256) ⇒ String
Compute a cryptographic hash of data.
-
.hash_and_hmac(data, key:, algorithm: :sha256) ⇒ Hash
Compute both a cryptographic hash and HMAC signature of data in one call.
-
.hmac(data, key:, algorithm: :sha256) ⇒ String
Compute an HMAC signature for data using the given key.
-
.hmac_verify(data, signature:, key:, algorithm: :sha256) ⇒ Boolean
Verify an HMAC signature in constant time.
-
.random_bytes(n) ⇒ String
Generate cryptographically secure random bytes.
-
.random_hex(n = 32) ⇒ String
Generate a cryptographically secure random hex string.
-
.random_salt ⇒ String
Generate a cryptographically secure random salt.
-
.random_token ⇒ String
Generate a cryptographically secure random token.
-
.rotate_key(encrypted, old_key:, new_key:) ⇒ String
Re-encrypt data with a new key.
-
.secure_compare(a, b) ⇒ Boolean
Constant-time string comparison to prevent timing attacks.
Class Method Details
.decrypt(data, key:) ⇒ String
Decrypt data encrypted with encrypt.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/philiprehberger/crypt.rb', line 50 def self.decrypt(data, key:) raw_key = normalize_key(key) raw = Base64.strict_decode64(data) iv = raw[0, IV_LENGTH] auth_tag = raw[IV_LENGTH, AUTH_TAG_LENGTH] ciphertext = raw[(IV_LENGTH + AUTH_TAG_LENGTH)..] cipher = OpenSSL::Cipher.new(CIPHER) cipher.decrypt cipher.key = raw_key cipher.iv = iv cipher.auth_tag = auth_tag cipher.auth_data = '' ciphertext.empty? ? cipher.final : cipher.update(ciphertext) + cipher.final rescue OpenSSL::Cipher::CipherError => e raise DecryptionError, "Decryption failed: #{e.}" end |
.derive_key(password, salt:, iterations: PBKDF2_ITERATIONS) ⇒ String
Derive an encryption key from a password using PBKDF2-HMAC-SHA256.
77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/philiprehberger/crypt.rb', line 77 def self.derive_key(password, salt:, iterations: PBKDF2_ITERATIONS) raise ArgumentError, 'iterations must be >= 1' if iterations.to_i < 1 OpenSSL::PKCS5.pbkdf2_hmac( password.to_s, salt, iterations.to_i, KEY_LENGTH, OpenSSL::Digest.new('SHA256') ) end |
.encrypt(data, key:) ⇒ String
Encrypt data using AES-256-GCM.
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/philiprehberger/crypt.rb', line 27 def self.encrypt(data, key:) raw_key = normalize_key(key) cipher = OpenSSL::Cipher.new(CIPHER) cipher.encrypt cipher.key = raw_key iv = cipher.random_iv cipher.auth_data = '' plaintext = data.to_s ciphertext = plaintext.empty? ? cipher.final : cipher.update(plaintext) + cipher.final auth_tag = cipher.auth_tag(AUTH_TAG_LENGTH) Base64.strict_encode64(iv + auth_tag + ciphertext) end |
.envelope_decrypt(envelope, master_key:) ⇒ String
Decrypt data encrypted with envelope_encrypt.
208 209 210 211 |
# File 'lib/philiprehberger/crypt.rb', line 208 def self.envelope_decrypt(envelope, master_key:) data_key = decrypt(envelope[:encrypted_key], key: master_key) decrypt(envelope[:encrypted_data], key: data_key) end |
.envelope_encrypt(data, master_key:) ⇒ Hash
Encrypt data using envelope encryption.
Generates a random data key, encrypts data with it, then encrypts the data key with the master key.
193 194 195 196 197 198 199 |
# File 'lib/philiprehberger/crypt.rb', line 193 def self.envelope_encrypt(data, master_key:) data_key = SecureRandom.random_bytes(KEY_LENGTH) encrypted_data = encrypt(data, key: data_key) encrypted_key = encrypt(data_key, key: master_key) { encrypted_data: encrypted_data, encrypted_key: encrypted_key } end |
.hash(data, algorithm: :sha256) ⇒ String
Compute a cryptographic hash of data.
149 150 151 152 153 154 |
# File 'lib/philiprehberger/crypt.rb', line 149 def self.hash(data, algorithm: :sha256) algo = HASH_ALGORITHMS[algorithm] raise ArgumentError, "Unsupported algorithm: #{algorithm}. Use :sha256, :sha384, or :sha512" unless algo OpenSSL::Digest.new(algo).hexdigest(data.to_s) end |
.hash_and_hmac(data, key:, algorithm: :sha256) ⇒ Hash
165 166 167 168 169 170 |
# File 'lib/philiprehberger/crypt.rb', line 165 def self.hash_and_hmac(data, key:, algorithm: :sha256) { hash: hash(data, algorithm: algorithm), hmac: hmac(data, key: key, algorithm: algorithm) } end |
.hmac(data, key:, algorithm: :sha256) ⇒ String
Compute an HMAC signature for data using the given key.
96 97 98 99 100 101 |
# File 'lib/philiprehberger/crypt.rb', line 96 def self.hmac(data, key:, algorithm: :sha256) algo = HASH_ALGORITHMS[algorithm] raise ArgumentError, "Unsupported algorithm: #{algorithm}. Use :sha256, :sha384, or :sha512" unless algo OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new(algo), key.to_s, data.to_s) end |
.hmac_verify(data, signature:, key:, algorithm: :sha256) ⇒ Boolean
Verify an HMAC signature in constant time.
110 111 112 113 |
# File 'lib/philiprehberger/crypt.rb', line 110 def self.hmac_verify(data, signature:, key:, algorithm: :sha256) expected = hmac(data, key: key, algorithm: algorithm) secure_compare(expected, signature.to_s) end |
.random_bytes(n) ⇒ String
Generate cryptographically secure random bytes.
217 218 219 |
# File 'lib/philiprehberger/crypt.rb', line 217 def self.random_bytes(n) SecureRandom.random_bytes(n) end |
.random_hex(n = 32) ⇒ String
Generate a cryptographically secure random hex string.
133 134 135 |
# File 'lib/philiprehberger/crypt.rb', line 133 def self.random_hex(n = 32) SecureRandom.hex(n) end |
.random_salt ⇒ String
Generate a cryptographically secure random salt.
118 119 120 |
# File 'lib/philiprehberger/crypt.rb', line 118 def self.random_salt SecureRandom.random_bytes(SALT_LENGTH) end |
.random_token ⇒ String
Generate a cryptographically secure random token.
125 126 127 |
# File 'lib/philiprehberger/crypt.rb', line 125 def self.random_token SecureRandom.urlsafe_base64(32) end |
.rotate_key(encrypted, old_key:, new_key:) ⇒ String
Re-encrypt data with a new key.
180 181 182 183 |
# File 'lib/philiprehberger/crypt.rb', line 180 def self.rotate_key(encrypted, old_key:, new_key:) plaintext = decrypt(encrypted, key: old_key) encrypt(plaintext, key: new_key) end |
.secure_compare(a, b) ⇒ Boolean
Constant-time string comparison to prevent timing attacks.
226 227 228 229 230 |
# File 'lib/philiprehberger/crypt.rb', line 226 def self.secure_compare(a, b) return false unless a.bytesize == b.bytesize OpenSSL.fixed_length_secure_compare(a, b) end |