Module: KeeperSecretsManager::Crypto
- Defined in:
- lib/keeper_secrets_manager/crypto.rb
Constant Summary collapse
- GCM_IV_LENGTH =
AES GCM constants
12- GCM_TAG_LENGTH =
16- AES_KEY_LENGTH =
32- BLOCK_SIZE =
Block size for padding
16
Class Method Summary collapse
-
.base64_to_bytes(str) ⇒ Object
Convert base64 to bytes.
-
.bytes_to_base64(bytes) ⇒ Object
Convert bytes to base64.
-
.bytes_to_url_safe_str(bytes) ⇒ Object
Convert bytes to URL-safe base64 string (no padding).
-
.decrypt_aes_cbc(encrypted_data, key) ⇒ Object
Legacy AES-CBC decryption.
-
.decrypt_aes_gcm(encrypted_data, key) ⇒ Object
Decrypt with AES-GCM or fallback to CBC.
-
.decrypt_ec(encrypted_data, private_key) ⇒ Object
Decrypt with EC private key.
-
.encrypt_aes_cbc(data, key, iv = nil) ⇒ Object
Legacy AES-CBC encryption (for compatibility).
-
.encrypt_aes_gcm(data, key) ⇒ Object
Encrypt with AES-GCM or fallback to CBC.
-
.encrypt_ec(data, public_key_bytes) ⇒ Object
Encrypt with EC public key (ECIES-like).
-
.export_private_key_der(ec_key) ⇒ Object
Export EC private key to DER.
-
.export_public_key_der(ec_key) ⇒ Object
Export EC public key to DER.
-
.generate_ecc_keys ⇒ Object
Generate ECC key pair.
-
.generate_encryption_key_bytes ⇒ Object
Generate encryption key (32 bytes).
-
.generate_hmac(key, data) ⇒ Object
Generate HMAC signature.
-
.generate_random_bytes(length) ⇒ Object
Generate random bytes.
-
.load_private_key_der(der_bytes, password = nil) ⇒ Object
Load private key from DER format.
-
.load_public_key_der(der_bytes) ⇒ Object
Load public key from DER format.
-
.pad_data(data) ⇒ Object
PKCS7 padding.
-
.sign_ec(data, private_key) ⇒ Object
Generate ECDSA signature.
-
.unpad_data(data) ⇒ Object
Remove PKCS7 padding.
-
.url_safe_str_to_bytes(str) ⇒ Object
Convert URL-safe base64 string to bytes.
-
.verify_hmac(key, data, signature) ⇒ Object
Verify HMAC signature.
Class Method Details
.base64_to_bytes(str) ⇒ Object
Convert base64 to bytes
44 45 46 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 44 def base64_to_bytes(str) Base64.strict_decode64(str) end |
.bytes_to_base64(bytes) ⇒ Object
Convert bytes to base64
39 40 41 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 39 def bytes_to_base64(bytes) Base64.strict_encode64(bytes) end |
.bytes_to_url_safe_str(bytes) ⇒ Object
Convert bytes to URL-safe base64 string (no padding)
27 28 29 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 27 def bytes_to_url_safe_str(bytes) Base64.urlsafe_encode64(bytes).delete('=') end |
.decrypt_aes_cbc(encrypted_data, key) ⇒ Object
Legacy AES-CBC decryption
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 167 def decrypt_aes_cbc(encrypted_data, key) # Extract IV iv = encrypted_data[0...BLOCK_SIZE] ciphertext = encrypted_data[BLOCK_SIZE..] cipher = OpenSSL::Cipher.new('AES-256-CBC') cipher.decrypt cipher.iv = iv cipher.key = key # OpenSSL handles PKCS7 padding removal automatically in cipher.final decrypted = cipher.update(ciphertext) + cipher.final decrypted rescue OpenSSL::Cipher::CipherError => e raise DecryptionError, "Failed to decrypt data: #{e.message}" end |
.decrypt_aes_gcm(encrypted_data, key) ⇒ Object
Decrypt with AES-GCM or fallback to CBC
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 120 def decrypt_aes_gcm(encrypted_data, key) # Try GCM first # Extract components iv = encrypted_data[0...GCM_IV_LENGTH] tag = encrypted_data[-GCM_TAG_LENGTH..] ciphertext = encrypted_data[GCM_IV_LENGTH...-GCM_TAG_LENGTH] cipher = OpenSSL::Cipher.new('AES-256-GCM') cipher.decrypt cipher.iv = iv cipher.key = key cipher.auth_tag = tag cipher.update(ciphertext) + cipher.final rescue RuntimeError => e if e..include?('unsupported cipher') # Fallback to AES-CBC decrypt_aes_cbc(encrypted_data, key) else raise e end rescue OpenSSL::Cipher::CipherError => e # Maybe it's CBC encrypted? begin decrypt_aes_cbc(encrypted_data, key) rescue StandardError raise DecryptionError, "Failed to decrypt data: #{e.message}" end end |
.decrypt_ec(encrypted_data, private_key) ⇒ Object
Decrypt with EC private key
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 280 def decrypt_ec(encrypted_data, private_key) # Extract ephemeral public key (65 bytes for uncompressed) ephemeral_public_bytes = encrypted_data[0...65] ciphertext = encrypted_data[65..] # Create EC key with ephemeral public key group = OpenSSL::PKey::EC::Group.new('prime256v1') ephemeral_point = OpenSSL::PKey::EC::Point.new(group, ephemeral_public_bytes) # Compute shared secret using ECDH shared_secret = private_key.dh_compute_key(ephemeral_point) # Derive decryption key decryption_key = OpenSSL::Digest::SHA256.digest(shared_secret) # Decrypt data decrypt_aes_gcm(ciphertext, decryption_key) end |
.encrypt_aes_cbc(data, key, iv = nil) ⇒ Object
Legacy AES-CBC encryption (for compatibility)
151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 151 def encrypt_aes_cbc(data, key, iv = nil) cipher = OpenSSL::Cipher.new('AES-256-CBC') cipher.encrypt iv ||= generate_random_bytes(BLOCK_SIZE) cipher.iv = iv cipher.key = key # OpenSSL handles PKCS7 padding automatically in cipher.final encrypted = cipher.update(data) + cipher.final # Return IV + encrypted iv + encrypted end |
.encrypt_aes_gcm(data, key) ⇒ Object
Encrypt with AES-GCM or fallback to CBC
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 93 def encrypt_aes_gcm(data, key) cipher = OpenSSL::Cipher.new('AES-256-GCM') cipher.encrypt # Generate random IV iv = generate_random_bytes(GCM_IV_LENGTH) cipher.iv = iv cipher.key = key # Encrypt data encrypted = cipher.update(data) + cipher.final # Get authentication tag tag = cipher.auth_tag(GCM_TAG_LENGTH) # Combine IV + encrypted + tag iv + encrypted + tag rescue RuntimeError => e if e..include?('unsupported cipher') # Fallback to AES-CBC for older Ruby/OpenSSL encrypt_aes_cbc(data, key) else raise e end end |
.encrypt_ec(data, public_key_bytes) ⇒ Object
Encrypt with EC public key (ECIES-like)
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 257 def encrypt_ec(data, public_key_bytes) # Load public key public_key = load_ec_public_key(public_key_bytes) # Generate ephemeral key pair ephemeral = OpenSSL::PKey::EC.generate('prime256v1') # Perform ECDH to get shared secret # The shared secret is computed using ECDH between ephemeral private key and server public key shared_secret = ephemeral.dh_compute_key(public_key.public_key) # Derive encryption key using SHA256 encryption_key = OpenSSL::Digest::SHA256.digest(shared_secret) # Encrypt data with AES-GCM encrypted_data = encrypt_aes_gcm(data, encryption_key) # Return ephemeral public key + encrypted data ephemeral_public = ephemeral.public_key.to_octet_string(:uncompressed) ephemeral_public + encrypted_data end |
.export_private_key_der(ec_key) ⇒ Object
Export EC private key to DER
247 248 249 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 247 def export_private_key_der(ec_key) ec_key.to_der end |
.export_public_key_der(ec_key) ⇒ Object
Export EC public key to DER
252 253 254 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 252 def export_public_key_der(ec_key) ec_key.public_key.to_der end |
.generate_ecc_keys ⇒ Object
Generate ECC key pair
49 50 51 52 53 54 55 56 57 58 59 60 61 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 87 88 89 90 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 49 def generate_ecc_keys # Generate private key bytes private_key_bytes = generate_encryption_key_bytes private_key_str = bytes_to_url_safe_str(private_key_bytes) # Create EC key from private key bytes private_key_bn = OpenSSL::BN.new(private_key_bytes, 2) # OpenSSL 3.0 compatibility - use ASN1 sequence to create key group = OpenSSL::PKey::EC::Group.new('prime256v1') # Generate public key point public_key_point = group.generator.mul(private_key_bn) # Create ASN1 sequence for the key asn1 = OpenSSL::ASN1::Sequence([ OpenSSL::ASN1::Integer(1), OpenSSL::ASN1::OctetString(private_key_bytes), OpenSSL::ASN1::ObjectId('prime256v1', 0, :EXPLICIT), OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT) ]) # Create key from DER key = OpenSSL::PKey::EC.new(asn1.to_der) # Get public key bytes (uncompressed format) public_key_bytes = key.public_key.to_octet_string(:uncompressed) public_key_str = bytes_to_url_safe_str(public_key_bytes) # Also store the EC key in DER format for compatibility private_key_der = key.to_der { private_key_str: private_key_str, public_key_str: public_key_str, private_key_bytes: private_key_bytes, # Use raw 32 bytes private_key_der: private_key_der, # Also provide DER format public_key_bytes: public_key_bytes, private_key_obj: key } end |
.generate_encryption_key_bytes ⇒ Object
Generate encryption key (32 bytes)
22 23 24 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 22 def generate_encryption_key_bytes generate_random_bytes(AES_KEY_LENGTH) end |
.generate_hmac(key, data) ⇒ Object
Generate HMAC signature
209 210 211 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 209 def generate_hmac(key, data) OpenSSL::HMAC.digest('SHA512', key, data) end |
.generate_random_bytes(length) ⇒ Object
Generate random bytes
17 18 19 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 17 def generate_random_bytes(length) SecureRandom.random_bytes(length) end |
.load_private_key_der(der_bytes, password = nil) ⇒ Object
Load private key from DER format
233 234 235 236 237 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 233 def load_private_key_der(der_bytes, password = nil) OpenSSL::PKey.read(der_bytes, password) rescue StandardError => e raise CryptoError, "Failed to load private key: #{e.message}" end |
.load_public_key_der(der_bytes) ⇒ Object
Load public key from DER format
240 241 242 243 244 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 240 def load_public_key_der(der_bytes) OpenSSL::PKey.read(der_bytes) rescue StandardError => e raise CryptoError, "Failed to load public key: #{e.message}" end |
.pad_data(data) ⇒ Object
PKCS7 padding
186 187 188 189 190 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 186 def pad_data(data) data = data.b if data.is_a?(String) pad_len = BLOCK_SIZE - (data.length % BLOCK_SIZE) data + (pad_len.chr * pad_len).b end |
.sign_ec(data, private_key) ⇒ Object
Generate ECDSA signature
214 215 216 217 218 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 214 def sign_ec(data, private_key) # Use SHA256 for ECDSA signature digest = OpenSSL::Digest.new('SHA256') private_key.sign(digest, data) end |
.unpad_data(data) ⇒ Object
Remove PKCS7 padding
193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 193 def unpad_data(data) return data if data.empty? pad_len = data[-1].ord # Validate padding if pad_len > 0 && pad_len <= BLOCK_SIZE && pad_len <= data.length # Check if all padding bytes are the same padding = data[-pad_len..] return data[0...-pad_len] if padding.bytes.all? { |b| b == pad_len } end data end |
.url_safe_str_to_bytes(str) ⇒ Object
Convert URL-safe base64 string to bytes
32 33 34 35 36 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 32 def url_safe_str_to_bytes(str) # Add padding if needed str += '=' * (4 - str.length % 4) if str.length % 4 != 0 Base64.urlsafe_decode64(str) end |
.verify_hmac(key, data, signature) ⇒ Object
Verify HMAC signature
221 222 223 224 225 226 227 228 229 230 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 221 def verify_hmac(key, data, signature) expected = generate_hmac(key, data) # Constant time comparison return false unless expected.bytesize == signature.bytesize result = 0 expected.bytes.zip(signature.bytes) { |a, b| result |= a ^ b } result == 0 end |