Class: BellaBaxter::E2EE::KeyPair
- Inherits:
-
Object
- Object
- BellaBaxter::E2EE::KeyPair
- Defined in:
- lib/bella_baxter/e2ee.rb
Instance Attribute Summary collapse
-
#public_key_b64 ⇒ Object
readonly
Base64-encoded DER/SPKI public key to send as X-E2E-Public-Key header.
Instance Method Summary collapse
-
#decrypt(payload) ⇒ Hash
Decrypt a response payload.
-
#decrypt_raw(payload) ⇒ Hash
Decrypt a response payload, returning the raw parsed JSON without transformation.
-
#initialize ⇒ KeyPair
constructor
A new instance of KeyPair.
Constructor Details
#initialize ⇒ KeyPair
Returns a new instance of KeyPair.
20 21 22 23 24 25 26 |
# File 'lib/bella_baxter/e2ee.rb', line 20 def initialize @ec = OpenSSL::PKey::EC.generate("prime256v1") # Export public key in SubjectPublicKeyInfo (SPKI/DER) format — what the server expects. # public_to_der is available in Ruby 3.0+ (openssl gem 3.x) via OpenSSL::PKey::PKey. @public_key_b64 = Base64.strict_encode64(@ec.public_to_der) end |
Instance Attribute Details
#public_key_b64 ⇒ Object (readonly)
Base64-encoded DER/SPKI public key to send as X-E2E-Public-Key header.
18 19 20 |
# File 'lib/bella_baxter/e2ee.rb', line 18 def public_key_b64 @public_key_b64 end |
Instance Method Details
#decrypt(payload) ⇒ Hash
Decrypt a response payload.
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 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 |
# File 'lib/bella_baxter/e2ee.rb', line 34 def decrypt(payload) return payload unless payload["encrypted"] == true server_key_der = Base64.strict_decode64(payload["serverPublicKey"]) nonce = Base64.strict_decode64(payload["nonce"]) tag = Base64.strict_decode64(payload["tag"]) ciphertext = Base64.strict_decode64(payload["ciphertext"]) # Load server public key from SPKI DER and compute ECDH shared secret. # OpenSSL::PKey.read handles SPKI DER for all key types (Ruby 4.0 / openssl gem 4.x). server_key = OpenSSL::PKey.read(server_key_der) shared_secret = @ec.derive(server_key) # HKDF-SHA256: extract + expand to 32-byte AES key. key = hkdf_sha256(shared_secret, 32) # AES-256-GCM decrypt. cipher = OpenSSL::Cipher.new("aes-256-gcm") cipher.decrypt cipher.key = key cipher.iv = nonce cipher.auth_tag = tag cipher.auth_data = "" plaintext = cipher.update(ciphertext) + cipher.final parsed = JSON.parse(plaintext) # Three possible server response shapes: # 1. Full AllEnvironmentSecretsResponse: {"environmentSlug":..., "secrets":{...}, ...} # 2. Array of SecretItem: [{"key":"K","value":"V"}, ...] # 3. Legacy flat dict: {"K":"V", ...} if parsed.is_a?(Hash) && parsed.key?("secrets") && parsed["secrets"].is_a?(Hash) parsed["secrets"].transform_values(&:to_s) elsif parsed.is_a?(Array) parsed.each_with_object({}) do |item, h| h[item["key"]] = item.fetch("value", "").to_s if item["key"] end else parsed end rescue OpenSSL::Cipher::CipherError => e raise BellaBaxter::DecryptionError, "AES-GCM decryption failed: #{e.}" rescue JSON::ParserError => e raise BellaBaxter::DecryptionError, "Decrypted payload is not valid JSON: #{e.}" end |
#decrypt_raw(payload) ⇒ Hash
Decrypt a response payload, returning the raw parsed JSON without transformation.
Unlike decrypt, this preserves the full server response shape (including environmentSlug, version, lastModified, etc.).
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/bella_baxter/e2ee.rb', line 88 def decrypt_raw(payload) return payload unless payload["encrypted"] == true server_key_der = Base64.strict_decode64(payload["serverPublicKey"]) nonce = Base64.strict_decode64(payload["nonce"]) tag = Base64.strict_decode64(payload["tag"]) ciphertext = Base64.strict_decode64(payload["ciphertext"]) server_key = OpenSSL::PKey.read(server_key_der) shared_secret = @ec.derive(server_key) key = hkdf_sha256(shared_secret, 32) cipher = OpenSSL::Cipher.new("aes-256-gcm") cipher.decrypt cipher.key = key cipher.iv = nonce cipher.auth_tag = tag cipher.auth_data = "" plaintext = cipher.update(ciphertext) + cipher.final JSON.parse(plaintext) rescue OpenSSL::Cipher::CipherError => e raise BellaBaxter::DecryptionError, "AES-GCM decryption failed: #{e.}" rescue JSON::ParserError => e raise BellaBaxter::DecryptionError, "Decrypted payload is not valid JSON: #{e.}" end |