Class: BSV::Primitives::SymmetricKey

Inherits:
Object
  • Object
show all
Defined in:
lib/bsv/primitives/symmetric_key.rb

Overview

AES-256-GCM symmetric encryption.

Provides authenticated encryption matching the interface used by the TS, Go, and Python reference SDKs. The wire format is:

|--- 32-byte IV ---|--- ciphertext ---|--- 16-byte auth tag ---|

All three reference SDKs use a 32-byte IV (non-standard but cross-SDK compatible) and 16-byte authentication tag.

Examples:

Round-trip encryption

key = BSV::Primitives::SymmetricKey.from_random
encrypted = key.encrypt('hello world')
key.decrypt(encrypted) #=> "hello world"

Constant Summary collapse

IV_SIZE =
32
TAG_SIZE =
16
KEY_SIZE =
32

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key_bytes) ⇒ SymmetricKey

Returns a new instance of SymmetricKey.

Parameters:

  • key_bytes (String)

    32-byte binary key (shorter keys are left-zero-padded)

Raises:

  • (ArgumentError)

    if key is empty or longer than 32 bytes



29
30
31
32
33
34
35
36
37
38
39
# File 'lib/bsv/primitives/symmetric_key.rb', line 29

def initialize(key_bytes)
  key_bytes = key_bytes.b
  raise ArgumentError, 'key must not be empty' if key_bytes.empty?
  raise ArgumentError, "key must be at most #{KEY_SIZE} bytes, got #{key_bytes.bytesize}" if key_bytes.bytesize > KEY_SIZE

  @key = if key_bytes.bytesize < KEY_SIZE
           ("\x00".b * (KEY_SIZE - key_bytes.bytesize)) + key_bytes
         else
           key_bytes
         end
end

Class Method Details

.from_ecdh(private_key, public_key) ⇒ SymmetricKey

Derive a symmetric key from an ECDH shared secret.

Computes the shared point between the two parties and uses the X-coordinate as the key material. The X-coordinate may be 31 or 32 bytes; shorter values are left-zero-padded automatically.

Examples:

Alice and Bob derive the same key

alice_key = SymmetricKey.from_ecdh(alice_priv, bob_pub)
bob_key   = SymmetricKey.from_ecdh(bob_priv, alice_pub)
alice_key.to_bytes == bob_key.to_bytes #=> true

Parameters:

  • private_key (PrivateKey)

    one party’s private key

  • public_key (PublicKey)

    the other party’s public key

Returns:



62
63
64
65
66
67
# File 'lib/bsv/primitives/symmetric_key.rb', line 62

def self.from_ecdh(private_key, public_key)
  shared = private_key.derive_shared_secret(public_key)
  # X-coordinate = bytes 1..32 of the compressed point (skip the 02/03 prefix)
  x_bytes = shared.compressed.byteslice(1, 32)
  new(x_bytes)
end

.from_randomSymmetricKey

Generate a random symmetric key.

Returns:



44
45
46
# File 'lib/bsv/primitives/symmetric_key.rb', line 44

def self.from_random
  new(SecureRandom.random_bytes(KEY_SIZE))
end

Instance Method Details

#decrypt(data) ⇒ String

Decrypt an AES-256-GCM encrypted message.

Expects the wire format: IV (32) + ciphertext + auth tag (16).

Parameters:

  • data (String)

    the encrypted message

Returns:

  • (String)

    the decrypted plaintext (binary)

Raises:

  • (ArgumentError)

    if the data is too short

  • (OpenSSL::Cipher::CipherError)

    if authentication fails (wrong key or tampered data)



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/bsv/primitives/symmetric_key.rb', line 101

def decrypt(data)
  data = data.b
  raise ArgumentError, "ciphertext too short: #{data.bytesize} bytes (minimum #{IV_SIZE + TAG_SIZE})" if data.bytesize < IV_SIZE + TAG_SIZE

  iv = data.byteslice(0, IV_SIZE)
  tag = data.byteslice(-TAG_SIZE, TAG_SIZE)
  ciphertext = data.byteslice(IV_SIZE, data.bytesize - IV_SIZE - TAG_SIZE)

  decipher = OpenSSL::Cipher.new('aes-256-gcm')
  decipher.decrypt
  decipher.key = @key
  decipher.iv_len = IV_SIZE
  decipher.iv = iv
  decipher.auth_tag = tag
  decipher.auth_data = ''.b

  ciphertext.empty? ? decipher.final : decipher.update(ciphertext) + decipher.final
end

#encrypt(plaintext) ⇒ String

Encrypt a message with AES-256-GCM.

Generates a random 32-byte IV per call. Returns the concatenation of IV, ciphertext, and 16-byte authentication tag.

Parameters:

  • plaintext (String)

    the message to encrypt

Returns:

  • (String)

    binary string: IV (32) + ciphertext + auth tag (16)



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/bsv/primitives/symmetric_key.rb', line 76

def encrypt(plaintext)
  iv = SecureRandom.random_bytes(IV_SIZE)

  cipher = OpenSSL::Cipher.new('aes-256-gcm')
  cipher.encrypt
  cipher.key = @key
  cipher.iv_len = IV_SIZE
  cipher.iv = iv
  cipher.auth_data = ''.b

  plaintext_bytes = plaintext.b
  ciphertext = plaintext_bytes.empty? ? cipher.final : cipher.update(plaintext_bytes) + cipher.final
  tag = cipher.auth_tag(TAG_SIZE)

  iv + ciphertext + tag
end

#to_bytesString

Return the raw key bytes.

Returns:

  • (String)

    32-byte binary key



123
124
125
# File 'lib/bsv/primitives/symmetric_key.rb', line 123

def to_bytes
  @key.dup
end