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_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 47 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 44 def base64_to_bytes(str) raise CryptoError, 'base64_to_bytes: received nil' if str.nil? 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
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 159 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.}" end |
.decrypt_aes_gcm(encrypted_data, key) ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 120 def decrypt_aes_gcm(encrypted_data, key) 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') decrypt_aes_cbc(encrypted_data, key) else raise e end rescue OpenSSL::Cipher::CipherError => e raise DecryptionError, "Failed to decrypt data: #{e.}" end |
.decrypt_ec(encrypted_data, private_key) ⇒ Object
Decrypt with EC private key
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 272 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)
143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 143 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
94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 94 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)
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 249 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
239 240 241 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 239 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
244 245 246 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 244 def export_public_key_der(ec_key) ec_key.public_key.to_der end |
.generate_ecc_keys ⇒ Object
Generate ECC key pair
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 91 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 50 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
201 202 203 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 201 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
225 226 227 228 229 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 225 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.}" end |
.load_public_key_der(der_bytes) ⇒ Object
Load public key from DER format
232 233 234 235 236 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 232 def load_public_key_der(der_bytes) OpenSSL::PKey.read(der_bytes) rescue StandardError => e raise CryptoError, "Failed to load public key: #{e.}" end |
.pad_data(data) ⇒ Object
PKCS7 padding
178 179 180 181 182 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 178 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
206 207 208 209 210 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 206 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
185 186 187 188 189 190 191 192 193 194 195 196 197 198 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 185 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) raise CryptoError, 'url_safe_str_to_bytes: received nil' if str.nil? str += '=' * (4 - str.length % 4) if str.length % 4 != 0 Base64.urlsafe_decode64(str) end |
.verify_hmac(key, data, signature) ⇒ Object
Verify HMAC signature
213 214 215 216 217 218 219 220 221 222 |
# File 'lib/keeper_secrets_manager/crypto.rb', line 213 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 |