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.
Class Method Summary collapse
-
.from_pem(pem) ⇒ Object
Create a KeyPair from a PKCS#8 PEM private key (ZKE persistent device key).
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 |
Class Method Details
.from_pem(pem) ⇒ Object
Create a KeyPair from a PKCS#8 PEM private key (ZKE persistent device key). Use this instead of KeyPair.new for ZKE mode. Obtain a key with: bella auth setup
31 32 33 34 35 36 37 38 |
# File 'lib/bella_baxter/e2ee.rb', line 31 def self.from_pem(pem) private_key = OpenSSL::PKey::EC.new(pem) public_key_b64 = Base64.strict_encode64(private_key.public_to_der) instance = allocate instance.send(:initialize_from_key, private_key, public_key_b64) instance end |
Instance Method Details
#decrypt(payload) ⇒ Hash
Decrypt a response payload.
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 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/bella_baxter/e2ee.rb', line 46 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.).
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/bella_baxter/e2ee.rb', line 100 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 |