Class: BSV::Wallet::ProtoWallet

Inherits:
Object
  • Object
show all
Includes:
Interface
Defined in:
lib/bsv/wallet_interface/proto_wallet.rb

Overview

Cryptographic wallet implementing the 8 key/crypto BRC-100 methods.

ProtoWallet handles key derivation, encryption, decryption, HMAC, and signature operations using BRC-42/43 key derivation. Transaction, certificate, blockchain, and authentication methods raise UnsupportedActionError via the included Interface.

Examples:

Encrypt and decrypt a message

wallet = BSV::Wallet::ProtoWallet.new(BSV::Primitives::PrivateKey.generate)
args = { protocol_id: [0, 'hello world'], key_id: '1', counterparty: 'self' }
result = wallet.encrypt(args.merge(plaintext: [104, 101, 108, 108, 111]))
wallet.decrypt(args.merge(ciphertext: result[:ciphertext]))[:plaintext]

Direct Known Subclasses

WalletClient

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Interface

#abort_action, #acquire_certificate, #create_action, #discover_by_attributes, #discover_by_identity_key, #get_header_for_height, #get_height, #get_network, #get_version, #internalize_action, #is_authenticated, #list_actions, #list_certificates, #list_outputs, #prove_certificate, #relinquish_certificate, #relinquish_output, #sign_action, #wait_for_authentication

Constructor Details

#initialize(key) ⇒ ProtoWallet

Returns a new instance of ProtoWallet.

Parameters:

  • key (BSV::Primitives::PrivateKey, String, KeyDeriver)

    A private key, the string ‘anyone’, or a pre-built KeyDeriver



27
28
29
30
31
32
33
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 27

def initialize(key)
  @key_deriver = if key.is_a?(KeyDeriver)
                   key
                 else
                   KeyDeriver.new(key)
                 end
end

Instance Attribute Details

#key_deriverKeyDeriver (readonly)

Returns the underlying key deriver.

Returns:



23
24
25
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 23

def key_deriver
  @key_deriver
end

Instance Method Details

#create_hmac(args, originator: nil) ⇒ Hash

Creates an HMAC-SHA256 using a derived symmetric key.

Parameters:

  • args (Hash)
  • originator (String, nil) (defaults to: nil)

    FQDN of the originating application

Options Hash (args):

  • :data (Array<Integer>)

    byte array to authenticate

  • :protocol_id (Array)
    security_level, protocol_name
  • :key_id (String)

    key identifier

  • :counterparty (String)

    public key hex, ‘self’, or ‘anyone’

Returns:

  • (Hash)

    { hmac: Array<Integer> }



102
103
104
105
106
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 102

def create_hmac(args, originator: nil)
  sym_key = derive_sym_key(args)
  hmac = BSV::Primitives::Digest.hmac_sha256(sym_key.to_bytes, bytes_to_string(args[:data]))
  { hmac: string_to_bytes(hmac) }
end

#create_signature(args, originator: nil) ⇒ Hash

Creates an ECDSA signature using a derived private key.

Either :data or :hash_to_directly_sign must be provided. If :data is given it is SHA-256 hashed before signing. If :hash_to_directly_sign is given it is used as the 32-byte hash directly.

Parameters:

  • args (Hash)
  • originator (String, nil) (defaults to: nil)

    FQDN of the originating application

Options Hash (args):

  • :data (Array<Integer>)

    data to hash and sign

  • :hash_to_directly_sign (Array<Integer>)

    pre-computed 32-byte hash to sign

  • :protocol_id (Array)
    security_level, protocol_name
  • :key_id (String)

    key identifier

  • :counterparty (String)

    public key hex, ‘self’, or ‘anyone’

Returns:

  • (Hash)

    { signature: Array<Integer> } DER-encoded signature as byte array



143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 143

def create_signature(args, originator: nil)
  counterparty = args[:counterparty] || 'anyone'
  priv_key = @key_deriver.derive_private_key(args[:protocol_id], args[:key_id], counterparty)

  hash = if args[:hash_to_directly_sign]
           bytes_to_string(args[:hash_to_directly_sign])
         else
           BSV::Primitives::Digest.sha256(bytes_to_string(args[:data]))
         end

  sig = priv_key.sign(hash)
  { signature: string_to_bytes(sig.to_der) }
end

#decrypt(args, originator: nil) ⇒ Hash

Decrypts ciphertext using AES-256-GCM with a derived symmetric key.

Parameters:

  • args (Hash)
  • originator (String, nil) (defaults to: nil)

    FQDN of the originating application

Options Hash (args):

  • :ciphertext (Array<Integer>)

    byte array to decrypt

  • :protocol_id (Array)
    security_level, protocol_name
  • :key_id (String)

    key identifier

  • :counterparty (String)

    public key hex, ‘self’, or ‘anyone’

Returns:

  • (Hash)

    { plaintext: Array<Integer> }



87
88
89
90
91
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 87

def decrypt(args, originator: nil)
  sym_key = derive_sym_key(args)
  plaintext = sym_key.decrypt(bytes_to_string(args[:ciphertext]))
  { plaintext: string_to_bytes(plaintext) }
end

#encrypt(args, originator: nil) ⇒ Hash

Encrypts plaintext using AES-256-GCM with a derived symmetric key.

Parameters:

  • args (Hash)
  • originator (String, nil) (defaults to: nil)

    FQDN of the originating application

Options Hash (args):

  • :plaintext (Array<Integer>)

    byte array to encrypt

  • :protocol_id (Array)
    security_level, protocol_name
  • :key_id (String)

    key identifier

  • :counterparty (String)

    public key hex, ‘self’, or ‘anyone’

Returns:

  • (Hash)

    { ciphertext: Array<Integer> }



72
73
74
75
76
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 72

def encrypt(args, originator: nil)
  sym_key = derive_sym_key(args)
  ciphertext = sym_key.encrypt(bytes_to_string(args[:plaintext]))
  { ciphertext: string_to_bytes(ciphertext) }
end

#get_public_key(args, originator: nil) ⇒ Hash

Returns a derived or identity public key.

When args[:identity_key] is true, returns the wallet’s identity key. Otherwise derives a key for the given protocol, key ID, and counterparty.

Parameters:

  • args (Hash)
  • originator (String, nil) (defaults to: nil)

    FQDN of the originating application

Options Hash (args):

  • :identity_key (Boolean)

    return the identity key instead of deriving

  • :protocol_id (Array)
    security_level, protocol_name
  • :key_id (String)

    key identifier

  • :counterparty (String)

    public key hex, ‘self’, or ‘anyone’

  • :for_self (Boolean)

    derive from own identity

Returns:

  • (Hash)

    { public_key: String } hex-encoded compressed public key



48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 48

def get_public_key(args, originator: nil)
  if args[:identity_key]
    { public_key: @key_deriver.identity_key }
  else
    counterparty = args[:counterparty] || 'self'
    pub = @key_deriver.derive_public_key(
      args[:protocol_id],
      args[:key_id],
      counterparty,
      for_self: args[:for_self] || false
    )
    { public_key: pub.to_hex }
  end
end

#reveal_counterparty_key_linkage(args, originator: nil) ⇒ Hash

Reveals counterparty key linkage to a verifier (BRC-69 Method 1).

Encrypts the ECDH shared secret between this wallet and the counterparty for the verifier using a BRC-72 protocol-derived key. Also generates a BRC-94 Schnorr zero-knowledge proof of the linkage and encrypts it for the verifier.

Parameters:

  • args (Hash)
  • originator (String, nil) (defaults to: nil)

    FQDN of the originating application

Options Hash (args):

  • :counterparty (String)

    counterparty public key hex (not ‘self’)

  • :verifier (String)

    verifier public key hex

Returns:

  • (Hash)

    with :prover, :verifier, :counterparty, :revelation_time, :encrypted_linkage, :encrypted_linkage_proof

Raises:



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 211

def reveal_counterparty_key_linkage(args, originator: nil)
  counterparty = args[:counterparty]
  verifier = args[:verifier]

  raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'

  Validators.validate_pub_key_hex!(verifier, 'verifier')

  linkage = @key_deriver.reveal_counterparty_secret(counterparty)
  revelation_time = Time.now.utc.iso8601

  encrypted_linkage_result = encrypt({
                                       plaintext: string_to_bytes(linkage),
                                       protocol_id: [2, 'counterparty linkage revelation'],
                                       key_id: revelation_time,
                                       counterparty: verifier
                                     })

  counterparty_pub = BSV::Primitives::PublicKey.from_hex(counterparty)
  linkage_point = BSV::Primitives::PublicKey.from_bytes(linkage)
  schnorr_proof = BSV::Primitives::Schnorr.generate_proof(
    @key_deriver.root_key,
    @key_deriver.root_key.public_key,
    counterparty_pub,
    linkage_point
  )

  z_bytes = schnorr_proof.z.to_s(2)
  z_bytes = ("\x00".b * (32 - z_bytes.length)) + z_bytes if z_bytes.length < 32
  proof_bin = schnorr_proof.r.compressed + schnorr_proof.s_prime.compressed + z_bytes

  encrypted_proof_result = encrypt({
                                     plaintext: string_to_bytes(proof_bin),
                                     protocol_id: [2, 'counterparty linkage revelation'],
                                     key_id: revelation_time,
                                     counterparty: verifier
                                   })

  {
    prover: @key_deriver.identity_key,
    verifier: verifier,
    counterparty: counterparty,
    revelation_time: revelation_time,
    encrypted_linkage: encrypted_linkage_result[:ciphertext],
    encrypted_linkage_proof: encrypted_proof_result[:ciphertext]
  }
end

#reveal_specific_key_linkage(args, originator: nil) ⇒ Hash

Reveals specific key linkage for a particular interaction (BRC-69 Method 2).

Encrypts the HMAC-derived key offset for the given protocol/key combination for the verifier using a BRC-72 protocol-derived key. Proof type 0 means no cryptographic proof is provided (consistent with the ts-sdk behaviour).

Parameters:

  • args (Hash)
  • originator (String, nil) (defaults to: nil)

    FQDN of the originating application

Options Hash (args):

  • :counterparty (String)

    counterparty public key hex

  • :verifier (String)

    verifier public key hex

  • :protocol_id (Array)
    security_level, protocol_name
  • :key_id (String)

    key identifier

Returns:

  • (Hash)

    with :prover, :verifier, :counterparty, :protocol_id, :key_id, :encrypted_linkage, :encrypted_linkage_proof, :proof_type

Raises:



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 273

def reveal_specific_key_linkage(args, originator: nil)
  counterparty = args[:counterparty]
  verifier = args[:verifier]
  protocol_id = args[:protocol_id]
  key_id = args[:key_id]

  raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'

  Validators.validate_pub_key_hex!(verifier, 'verifier')

  linkage = @key_deriver.reveal_specific_secret(counterparty, protocol_id, key_id)

  derived_protocol = "specific linkage revelation #{protocol_id[0]} #{protocol_id[1]}"

  encrypted_linkage_result = encrypt({
                                       plaintext: string_to_bytes(linkage),
                                       protocol_id: [2, derived_protocol],
                                       key_id: key_id,
                                       counterparty: verifier
                                     })

  encrypted_proof_result = encrypt({
                                     plaintext: [0],
                                     protocol_id: [2, derived_protocol],
                                     key_id: key_id,
                                     counterparty: verifier
                                   })

  {
    prover: @key_deriver.identity_key,
    verifier: verifier,
    counterparty: counterparty,
    protocol_id: protocol_id,
    key_id: key_id,
    encrypted_linkage: encrypted_linkage_result[:ciphertext],
    encrypted_linkage_proof: encrypted_proof_result[:ciphertext],
    proof_type: 0
  }
end

#verify_hmac(args, originator: nil) ⇒ Hash

Verifies an HMAC-SHA256 using a derived symmetric key.

Parameters:

  • args (Hash)
  • originator (String, nil) (defaults to: nil)

    FQDN of the originating application

Options Hash (args):

  • :data (Array<Integer>)

    byte array that was authenticated

  • :hmac (Array<Integer>)

    HMAC to verify

  • :protocol_id (Array)
    security_level, protocol_name
  • :key_id (String)

    key identifier

  • :counterparty (String)

    public key hex, ‘self’, or ‘anyone’

Returns:

  • (Hash)

    { valid: true }

Raises:



119
120
121
122
123
124
125
126
127
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 119

def verify_hmac(args, originator: nil)
  sym_key = derive_sym_key(args)
  expected = BSV::Primitives::Digest.hmac_sha256(sym_key.to_bytes, bytes_to_string(args[:data]))
  provided = bytes_to_string(args[:hmac])

  raise InvalidHmacError unless secure_compare(expected, provided)

  { valid: true }
end

#verify_signature(args, originator: nil) ⇒ Hash

Verifies an ECDSA signature using a derived public key.

Either :data or :hash_to_directly_verify must be provided. If :data is given it is SHA-256 hashed before verification.

Parameters:

  • args (Hash)
  • originator (String, nil) (defaults to: nil)

    FQDN of the originating application

Options Hash (args):

  • :data (Array<Integer>)

    original data that was signed

  • :hash_to_directly_verify (Array<Integer>)

    pre-computed 32-byte hash

  • :signature (Array<Integer>)

    DER-encoded signature as byte array

  • :protocol_id (Array)
    security_level, protocol_name
  • :key_id (String)

    key identifier

  • :counterparty (String)

    public key hex, ‘self’, or ‘anyone’

  • :for_self (Boolean)

    verify own derived key (default false)

Returns:

  • (Hash)

    { valid: true }

Raises:



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/bsv/wallet_interface/proto_wallet.rb', line 173

def verify_signature(args, originator: nil)
  counterparty = args[:counterparty] || 'self'
  for_self = args[:for_self] || false

  pub_key = @key_deriver.derive_public_key(
    args[:protocol_id],
    args[:key_id],
    counterparty,
    for_self: for_self
  )

  hash = if args[:hash_to_directly_verify]
           bytes_to_string(args[:hash_to_directly_verify])
         else
           BSV::Primitives::Digest.sha256(bytes_to_string(args[:data]))
         end

  sig = BSV::Primitives::Signature.from_der(bytes_to_string(args[:signature]))
  valid = pub_key.verify(hash, sig)

  raise InvalidSignatureError unless valid

  { valid: true }
end