bsv-wallet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # BRC-42/43 key derivation for the wallet interface.
6
+ #
7
+ # Derives child keys from a root private key using BKDS (BSV Key Derivation
8
+ # Scheme). Supports protocol IDs, key IDs, counterparties, and security
9
+ # levels as defined in BRC-43.
10
+ class KeyDeriver
11
+ ANYONE_BN = OpenSSL::BN.new(1)
12
+
13
+ attr_reader :root_key
14
+
15
+ # @param root_key [BSV::Primitives::PrivateKey, String] a private key or 'anyone'
16
+ def initialize(root_key)
17
+ @root_key = if root_key == 'anyone'
18
+ BSV::Primitives::PrivateKey.new(ANYONE_BN)
19
+ else
20
+ root_key
21
+ end
22
+ end
23
+
24
+ # Returns the identity public key as a hex string.
25
+ # @return [String] 66-character compressed public key hex
26
+ def identity_key
27
+ @root_key.public_key.to_hex
28
+ end
29
+
30
+ # Derives a public key using BRC-42 key derivation.
31
+ #
32
+ # @param protocol_id [Array] [security_level, protocol_name]
33
+ # @param key_id [String] key identifier
34
+ # @param counterparty [String] public key hex, 'self', or 'anyone'
35
+ # @param for_self [Boolean] derive from own identity rather than counterparty's
36
+ # @return [BSV::Primitives::PublicKey]
37
+ def derive_public_key(protocol_id, key_id, counterparty, for_self: false)
38
+ Validators.validate_protocol_id!(protocol_id)
39
+ Validators.validate_key_id!(key_id)
40
+ invoice = compute_invoice_number(protocol_id, key_id)
41
+ counterparty_pub = resolve_counterparty(counterparty)
42
+
43
+ if for_self
44
+ @root_key.derive_child(counterparty_pub, invoice).public_key
45
+ else
46
+ counterparty_pub.derive_child(@root_key, invoice)
47
+ end
48
+ end
49
+
50
+ # Derives a private key using BRC-42 key derivation.
51
+ #
52
+ # @param protocol_id [Array] [security_level, protocol_name]
53
+ # @param key_id [String] key identifier
54
+ # @param counterparty [String] public key hex, 'self', or 'anyone'
55
+ # @return [BSV::Primitives::PrivateKey]
56
+ def derive_private_key(protocol_id, key_id, counterparty)
57
+ Validators.validate_protocol_id!(protocol_id)
58
+ Validators.validate_key_id!(key_id)
59
+ invoice = compute_invoice_number(protocol_id, key_id)
60
+ counterparty_pub = resolve_counterparty(counterparty)
61
+ @root_key.derive_child(counterparty_pub, invoice)
62
+ end
63
+
64
+ # Derives a symmetric key for encryption/HMAC operations.
65
+ #
66
+ # Uses ECDH between the derived private and public child keys to
67
+ # produce a shared secret, then uses the X-coordinate as the key.
68
+ #
69
+ # @param protocol_id [Array] [security_level, protocol_name]
70
+ # @param key_id [String] key identifier
71
+ # @param counterparty [String] public key hex, 'self', or 'anyone'
72
+ # @return [BSV::Primitives::SymmetricKey]
73
+ def derive_symmetric_key(protocol_id, key_id, counterparty)
74
+ Validators.validate_protocol_id!(protocol_id)
75
+ Validators.validate_key_id!(key_id)
76
+ invoice = compute_invoice_number(protocol_id, key_id)
77
+ counterparty_pub = resolve_counterparty(counterparty)
78
+
79
+ derived_private = @root_key.derive_child(counterparty_pub, invoice)
80
+ derived_public = counterparty_pub.derive_child(@root_key, invoice)
81
+
82
+ BSV::Primitives::SymmetricKey.from_ecdh(derived_private, derived_public)
83
+ end
84
+
85
+ # Reveals the ECDH shared secret between this wallet and a counterparty.
86
+ # Used for BRC-69 Method 1 (counterparty key linkage).
87
+ #
88
+ # @param counterparty [String] public key hex (not 'self')
89
+ # @return [String] compressed shared secret bytes
90
+ def reveal_counterparty_secret(counterparty)
91
+ raise InvalidParameterError.new('counterparty', 'not "self" for key linkage revelation') if counterparty == 'self'
92
+
93
+ counterparty_pub = resolve_counterparty(counterparty)
94
+ @root_key.derive_shared_secret(counterparty_pub).compressed
95
+ end
96
+
97
+ # Reveals the specific key offset for a particular derived key.
98
+ # Used for BRC-69 Method 2 (specific key linkage).
99
+ #
100
+ # @param counterparty [String] public key hex
101
+ # @param protocol_id [Array] [security_level, protocol_name]
102
+ # @param key_id [String] key identifier
103
+ # @return [String] HMAC-SHA256 bytes (the key offset)
104
+ def reveal_specific_secret(counterparty, protocol_id, key_id)
105
+ Validators.validate_protocol_id!(protocol_id)
106
+ Validators.validate_key_id!(key_id)
107
+ counterparty_pub = resolve_counterparty(counterparty)
108
+ shared = @root_key.derive_shared_secret(counterparty_pub)
109
+ invoice = compute_invoice_number(protocol_id, key_id)
110
+ BSV::Primitives::Digest.hmac_sha256(shared.compressed, invoice.encode('UTF-8'))
111
+ end
112
+
113
+ private
114
+
115
+ # Resolves a counterparty identifier to a PublicKey.
116
+ #
117
+ # @param counterparty [String] 'self', 'anyone', or a hex public key
118
+ # @return [BSV::Primitives::PublicKey]
119
+ def resolve_counterparty(counterparty)
120
+ case counterparty
121
+ when 'self'
122
+ @root_key.public_key
123
+ when 'anyone'
124
+ BSV::Primitives::PrivateKey.new(ANYONE_BN).public_key
125
+ else
126
+ Validators.validate_counterparty!(counterparty)
127
+ BSV::Primitives::PublicKey.from_hex(counterparty)
128
+ end
129
+ end
130
+
131
+ # Computes the invoice number from a protocol ID and key ID.
132
+ # Format: "#{security_level}-#{protocol_name}-#{key_id}"
133
+ #
134
+ # @param protocol_id [Array] [security_level, protocol_name]
135
+ # @param key_id [String]
136
+ # @return [String]
137
+ def compute_invoice_number(protocol_id, key_id)
138
+ "#{protocol_id[0]}-#{protocol_id[1]}-#{key_id}"
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # In-memory storage adapter for testing.
6
+ #
7
+ # Stores actions, outputs, and certificates in plain Ruby arrays.
8
+ # Not thread-safe; intended for test use only.
9
+ class MemoryStore
10
+ include StorageAdapter
11
+
12
+ def initialize
13
+ @actions = []
14
+ @outputs = []
15
+ @certificates = []
16
+ end
17
+
18
+ def store_action(action_data)
19
+ @actions << action_data
20
+ action_data
21
+ end
22
+
23
+ def find_actions(query)
24
+ apply_pagination(filter_actions(query), query)
25
+ end
26
+
27
+ def count_actions(query)
28
+ filter_actions(query).length
29
+ end
30
+
31
+ def store_output(output_data)
32
+ @outputs << output_data
33
+ output_data
34
+ end
35
+
36
+ def find_outputs(query)
37
+ apply_pagination(filter_outputs(query), query)
38
+ end
39
+
40
+ def count_outputs(query)
41
+ filter_outputs(query).length
42
+ end
43
+
44
+ def delete_output(outpoint)
45
+ idx = @outputs.index { |o| o[:outpoint] == outpoint }
46
+ return false unless idx
47
+
48
+ @outputs.delete_at(idx)
49
+ true
50
+ end
51
+
52
+ def store_certificate(cert_data)
53
+ @certificates << cert_data
54
+ cert_data
55
+ end
56
+
57
+ def find_certificates(query)
58
+ results = @certificates
59
+ results = results.select { |c| query[:certifiers].include?(c[:certifier]) } if query[:certifiers]
60
+ results = results.select { |c| query[:types].include?(c[:type]) } if query[:types]
61
+ apply_pagination(results, query)
62
+ end
63
+
64
+ def delete_certificate(type:, serial_number:, certifier:)
65
+ idx = @certificates.index do |c|
66
+ c[:type] == type && c[:serial_number] == serial_number && c[:certifier] == certifier
67
+ end
68
+ return false unless idx
69
+
70
+ @certificates.delete_at(idx)
71
+ true
72
+ end
73
+
74
+ private
75
+
76
+ def filter_actions(query)
77
+ results = @actions
78
+ return results unless query[:labels]
79
+
80
+ mode = query[:label_query_mode] || 'any'
81
+ results.select do |a|
82
+ action_labels = a[:labels] || []
83
+ if mode == 'all'
84
+ (query[:labels] - action_labels).empty?
85
+ else
86
+ (query[:labels] & action_labels).any?
87
+ end
88
+ end
89
+ end
90
+
91
+ def filter_outputs(query)
92
+ results = @outputs
93
+ results = results.select { |o| o[:basket] == query[:basket] } if query[:basket]
94
+ if query[:tags]
95
+ mode = query[:tag_query_mode] || 'any'
96
+ results = results.select do |o|
97
+ output_tags = o[:tags] || []
98
+ if mode == 'all'
99
+ (query[:tags] - output_tags).empty?
100
+ else
101
+ (query[:tags] & output_tags).any?
102
+ end
103
+ end
104
+ end
105
+ query[:include_spent] ? results : results.reject { |o| o[:spendable] == false }
106
+ end
107
+
108
+ def apply_pagination(results, query)
109
+ offset = query[:offset] || 0
110
+ limit = query[:limit] || 10
111
+ results[offset, limit] || []
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module BSV
6
+ module Wallet
7
+ # Cryptographic wallet implementing the 8 key/crypto BRC-100 methods.
8
+ #
9
+ # ProtoWallet handles key derivation, encryption, decryption, HMAC,
10
+ # and signature operations using BRC-42/43 key derivation. Transaction,
11
+ # certificate, blockchain, and authentication methods raise
12
+ # {UnsupportedActionError} via the included {Interface}.
13
+ #
14
+ # @example Encrypt and decrypt a message
15
+ # wallet = BSV::Wallet::ProtoWallet.new(BSV::Primitives::PrivateKey.generate)
16
+ # args = { protocol_id: [0, 'hello world'], key_id: '1', counterparty: 'self' }
17
+ # result = wallet.encrypt(args.merge(plaintext: [104, 101, 108, 108, 111]))
18
+ # wallet.decrypt(args.merge(ciphertext: result[:ciphertext]))[:plaintext]
19
+ class ProtoWallet
20
+ include Interface
21
+
22
+ # @return [KeyDeriver] the underlying key deriver
23
+ attr_reader :key_deriver
24
+
25
+ # @param key [BSV::Primitives::PrivateKey, String, KeyDeriver]
26
+ # A private key, the string +'anyone'+, or a pre-built {KeyDeriver}
27
+ def initialize(key)
28
+ @key_deriver = if key.is_a?(KeyDeriver)
29
+ key
30
+ else
31
+ KeyDeriver.new(key)
32
+ end
33
+ end
34
+
35
+ # Returns a derived or identity public key.
36
+ #
37
+ # When +args[:identity_key]+ is true, returns the wallet's identity key.
38
+ # Otherwise derives a key for the given protocol, key ID, and counterparty.
39
+ #
40
+ # @param args [Hash]
41
+ # @option args [Boolean] :identity_key return the identity key instead of deriving
42
+ # @option args [Array] :protocol_id [security_level, protocol_name]
43
+ # @option args [String] :key_id key identifier
44
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
45
+ # @option args [Boolean] :for_self derive from own identity
46
+ # @param originator [String, nil] FQDN of the originating application
47
+ # @return [Hash] { public_key: String } hex-encoded compressed public key
48
+ def get_public_key(args, _originator: nil)
49
+ if args[:identity_key]
50
+ { public_key: @key_deriver.identity_key }
51
+ else
52
+ counterparty = args[:counterparty] || 'self'
53
+ pub = @key_deriver.derive_public_key(
54
+ args[:protocol_id],
55
+ args[:key_id],
56
+ counterparty,
57
+ for_self: args[:for_self] || false
58
+ )
59
+ { public_key: pub.to_hex }
60
+ end
61
+ end
62
+
63
+ # Encrypts plaintext using AES-256-GCM with a derived symmetric key.
64
+ #
65
+ # @param args [Hash]
66
+ # @option args [Array<Integer>] :plaintext byte array to encrypt
67
+ # @option args [Array] :protocol_id [security_level, protocol_name]
68
+ # @option args [String] :key_id key identifier
69
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
70
+ # @param originator [String, nil] FQDN of the originating application
71
+ # @return [Hash] { ciphertext: Array<Integer> }
72
+ def encrypt(args, _originator: nil)
73
+ sym_key = derive_sym_key(args)
74
+ ciphertext = sym_key.encrypt(bytes_to_string(args[:plaintext]))
75
+ { ciphertext: string_to_bytes(ciphertext) }
76
+ end
77
+
78
+ # Decrypts ciphertext using AES-256-GCM with a derived symmetric key.
79
+ #
80
+ # @param args [Hash]
81
+ # @option args [Array<Integer>] :ciphertext byte array to decrypt
82
+ # @option args [Array] :protocol_id [security_level, protocol_name]
83
+ # @option args [String] :key_id key identifier
84
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
85
+ # @param originator [String, nil] FQDN of the originating application
86
+ # @return [Hash] { plaintext: Array<Integer> }
87
+ def decrypt(args, _originator: nil)
88
+ sym_key = derive_sym_key(args)
89
+ plaintext = sym_key.decrypt(bytes_to_string(args[:ciphertext]))
90
+ { plaintext: string_to_bytes(plaintext) }
91
+ end
92
+
93
+ # Creates an HMAC-SHA256 using a derived symmetric key.
94
+ #
95
+ # @param args [Hash]
96
+ # @option args [Array<Integer>] :data byte array to authenticate
97
+ # @option args [Array] :protocol_id [security_level, protocol_name]
98
+ # @option args [String] :key_id key identifier
99
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
100
+ # @param originator [String, nil] FQDN of the originating application
101
+ # @return [Hash] { hmac: Array<Integer> }
102
+ def create_hmac(args, _originator: nil)
103
+ sym_key = derive_sym_key(args)
104
+ hmac = BSV::Primitives::Digest.hmac_sha256(sym_key.to_bytes, bytes_to_string(args[:data]))
105
+ { hmac: string_to_bytes(hmac) }
106
+ end
107
+
108
+ # Verifies an HMAC-SHA256 using a derived symmetric key.
109
+ #
110
+ # @param args [Hash]
111
+ # @option args [Array<Integer>] :data byte array that was authenticated
112
+ # @option args [Array<Integer>] :hmac HMAC to verify
113
+ # @option args [Array] :protocol_id [security_level, protocol_name]
114
+ # @option args [String] :key_id key identifier
115
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
116
+ # @param originator [String, nil] FQDN of the originating application
117
+ # @return [Hash] { valid: true }
118
+ # @raise [InvalidHmacError] if the HMAC does not match
119
+ def verify_hmac(args, _originator: nil)
120
+ sym_key = derive_sym_key(args)
121
+ expected = BSV::Primitives::Digest.hmac_sha256(sym_key.to_bytes, bytes_to_string(args[:data]))
122
+ provided = bytes_to_string(args[:hmac])
123
+
124
+ raise InvalidHmacError unless secure_compare(expected, provided)
125
+
126
+ { valid: true }
127
+ end
128
+
129
+ # Creates an ECDSA signature using a derived private key.
130
+ #
131
+ # Either +:data+ or +:hash_to_directly_sign+ must be provided.
132
+ # If +:data+ is given it is SHA-256 hashed before signing.
133
+ # If +:hash_to_directly_sign+ is given it is used as the 32-byte hash directly.
134
+ #
135
+ # @param args [Hash]
136
+ # @option args [Array<Integer>] :data data to hash and sign
137
+ # @option args [Array<Integer>] :hash_to_directly_sign pre-computed 32-byte hash to sign
138
+ # @option args [Array] :protocol_id [security_level, protocol_name]
139
+ # @option args [String] :key_id key identifier
140
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
141
+ # @param originator [String, nil] FQDN of the originating application
142
+ # @return [Hash] { signature: Array<Integer> } DER-encoded signature as byte array
143
+ def create_signature(args, _originator: nil)
144
+ counterparty = args[:counterparty] || 'self'
145
+ priv_key = @key_deriver.derive_private_key(args[:protocol_id], args[:key_id], counterparty)
146
+
147
+ hash = if args[:hash_to_directly_sign]
148
+ bytes_to_string(args[:hash_to_directly_sign])
149
+ else
150
+ BSV::Primitives::Digest.sha256(bytes_to_string(args[:data]))
151
+ end
152
+
153
+ sig = priv_key.sign(hash)
154
+ { signature: string_to_bytes(sig.to_der) }
155
+ end
156
+
157
+ # Verifies an ECDSA signature using a derived public key.
158
+ #
159
+ # Either +:data+ or +:hash_to_directly_verify+ must be provided.
160
+ # If +:data+ is given it is SHA-256 hashed before verification.
161
+ #
162
+ # @param args [Hash]
163
+ # @option args [Array<Integer>] :data original data that was signed
164
+ # @option args [Array<Integer>] :hash_to_directly_verify pre-computed 32-byte hash
165
+ # @option args [Array<Integer>] :signature DER-encoded signature as byte array
166
+ # @option args [Array] :protocol_id [security_level, protocol_name]
167
+ # @option args [String] :key_id key identifier
168
+ # @option args [String] :counterparty public key hex, 'self', or 'anyone'
169
+ # @option args [Boolean] :for_self verify own derived key (default false)
170
+ # @param originator [String, nil] FQDN of the originating application
171
+ # @return [Hash] { valid: true }
172
+ # @raise [InvalidSignatureError] if the signature does not verify
173
+ def verify_signature(args, _originator: nil)
174
+ counterparty = args[:counterparty] || 'self'
175
+ for_self = args[:for_self] || false
176
+
177
+ pub_key = @key_deriver.derive_public_key(
178
+ args[:protocol_id],
179
+ args[:key_id],
180
+ counterparty,
181
+ for_self: for_self
182
+ )
183
+
184
+ hash = if args[:hash_to_directly_verify]
185
+ bytes_to_string(args[:hash_to_directly_verify])
186
+ else
187
+ BSV::Primitives::Digest.sha256(bytes_to_string(args[:data]))
188
+ end
189
+
190
+ sig = BSV::Primitives::Signature.from_der(bytes_to_string(args[:signature]))
191
+ valid = pub_key.verify(hash, sig)
192
+
193
+ raise InvalidSignatureError unless valid
194
+
195
+ { valid: true }
196
+ end
197
+
198
+ # Reveals counterparty key linkage to a verifier (BRC-69 Method 1).
199
+ #
200
+ # Encrypts the ECDH shared secret between this wallet and the counterparty
201
+ # for the verifier using a BRC-72 protocol-derived key. Also generates a
202
+ # BRC-94 Schnorr zero-knowledge proof of the linkage and encrypts it for
203
+ # the verifier.
204
+ #
205
+ # @param args [Hash]
206
+ # @option args [String] :counterparty counterparty public key hex (not 'self')
207
+ # @option args [String] :verifier verifier public key hex
208
+ # @param originator [String, nil] FQDN of the originating application
209
+ # @return [Hash] with :prover, :verifier, :counterparty, :revelation_time,
210
+ # :encrypted_linkage, :encrypted_linkage_proof
211
+ def reveal_counterparty_key_linkage(args, _originator: nil)
212
+ counterparty = args[:counterparty]
213
+ verifier = args[:verifier]
214
+
215
+ raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'
216
+
217
+ Validators.validate_pub_key_hex!(verifier, 'verifier')
218
+
219
+ linkage = @key_deriver.reveal_counterparty_secret(counterparty)
220
+ revelation_time = Time.now.utc.iso8601
221
+
222
+ encrypted_linkage_result = encrypt({
223
+ plaintext: string_to_bytes(linkage),
224
+ protocol_id: [2, 'counterparty linkage revelation'],
225
+ key_id: revelation_time,
226
+ counterparty: verifier
227
+ })
228
+
229
+ counterparty_pub = BSV::Primitives::PublicKey.from_hex(counterparty)
230
+ linkage_point = BSV::Primitives::PublicKey.from_bytes(linkage)
231
+ schnorr_proof = BSV::Primitives::Schnorr.generate_proof(
232
+ @key_deriver.root_key,
233
+ @key_deriver.root_key.public_key,
234
+ counterparty_pub,
235
+ linkage_point
236
+ )
237
+
238
+ z_bytes = schnorr_proof.z.to_s(2)
239
+ z_bytes = ("\x00".b * (32 - z_bytes.length)) + z_bytes if z_bytes.length < 32
240
+ proof_bin = schnorr_proof.r.compressed + schnorr_proof.s_prime.compressed + z_bytes
241
+
242
+ encrypted_proof_result = encrypt({
243
+ plaintext: string_to_bytes(proof_bin),
244
+ protocol_id: [2, 'counterparty linkage revelation'],
245
+ key_id: revelation_time,
246
+ counterparty: verifier
247
+ })
248
+
249
+ {
250
+ prover: @key_deriver.identity_key,
251
+ verifier: verifier,
252
+ counterparty: counterparty,
253
+ revelation_time: revelation_time,
254
+ encrypted_linkage: encrypted_linkage_result[:ciphertext],
255
+ encrypted_linkage_proof: encrypted_proof_result[:ciphertext]
256
+ }
257
+ end
258
+
259
+ # Reveals specific key linkage for a particular interaction (BRC-69 Method 2).
260
+ #
261
+ # Encrypts the HMAC-derived key offset for the given protocol/key combination
262
+ # for the verifier using a BRC-72 protocol-derived key. Proof type 0 means
263
+ # no cryptographic proof is provided (consistent with the ts-sdk behaviour).
264
+ #
265
+ # @param args [Hash]
266
+ # @option args [String] :counterparty counterparty public key hex
267
+ # @option args [String] :verifier verifier public key hex
268
+ # @option args [Array] :protocol_id [security_level, protocol_name]
269
+ # @option args [String] :key_id key identifier
270
+ # @param originator [String, nil] FQDN of the originating application
271
+ # @return [Hash] with :prover, :verifier, :counterparty, :protocol_id, :key_id,
272
+ # :encrypted_linkage, :encrypted_linkage_proof, :proof_type
273
+ def reveal_specific_key_linkage(args, _originator: nil)
274
+ counterparty = args[:counterparty]
275
+ verifier = args[:verifier]
276
+ protocol_id = args[:protocol_id]
277
+ key_id = args[:key_id]
278
+
279
+ raise InvalidParameterError.new('counterparty', 'a specific public key hex, not "anyone"') if counterparty == 'anyone'
280
+
281
+ Validators.validate_pub_key_hex!(verifier, 'verifier')
282
+
283
+ linkage = @key_deriver.reveal_specific_secret(counterparty, protocol_id, key_id)
284
+
285
+ derived_protocol = "specific linkage revelation #{protocol_id[0]} #{protocol_id[1]}"
286
+
287
+ encrypted_linkage_result = encrypt({
288
+ plaintext: string_to_bytes(linkage),
289
+ protocol_id: [2, derived_protocol],
290
+ key_id: key_id,
291
+ counterparty: verifier
292
+ })
293
+
294
+ encrypted_proof_result = encrypt({
295
+ plaintext: [0],
296
+ protocol_id: [2, derived_protocol],
297
+ key_id: key_id,
298
+ counterparty: verifier
299
+ })
300
+
301
+ {
302
+ prover: @key_deriver.identity_key,
303
+ verifier: verifier,
304
+ counterparty: counterparty,
305
+ protocol_id: protocol_id,
306
+ key_id: key_id,
307
+ encrypted_linkage: encrypted_linkage_result[:ciphertext],
308
+ encrypted_linkage_proof: encrypted_proof_result[:ciphertext],
309
+ proof_type: 0
310
+ }
311
+ end
312
+
313
+ private
314
+
315
+ # Derives a symmetric key from the args hash.
316
+ #
317
+ # @param args [Hash] must contain :protocol_id, :key_id; :counterparty defaults to 'self'
318
+ # @return [BSV::Primitives::SymmetricKey]
319
+ def derive_sym_key(args)
320
+ counterparty = args[:counterparty] || 'self'
321
+ @key_deriver.derive_symmetric_key(args[:protocol_id], args[:key_id], counterparty)
322
+ end
323
+
324
+ # Converts a byte array (Array of Integers 0..255) to a binary string.
325
+ #
326
+ # @param bytes [Array<Integer>] byte array
327
+ # @return [String] binary string
328
+ def bytes_to_string(bytes)
329
+ bytes.pack('C*')
330
+ end
331
+
332
+ # Converts a binary string to a byte array (Array of Integers 0..255).
333
+ #
334
+ # @param str [String] binary string
335
+ # @return [Array<Integer>] byte array
336
+ def string_to_bytes(str)
337
+ str.unpack('C*')
338
+ end
339
+
340
+ # Constant-time string comparison to prevent timing attacks.
341
+ #
342
+ # Falls back to a manual XOR loop on platforms where
343
+ # +OpenSSL.fixed_length_secure_compare+ is unavailable.
344
+ #
345
+ # @param a [String] first binary string
346
+ # @param b [String] second binary string
347
+ # @return [Boolean]
348
+ def secure_compare(a, b)
349
+ return false unless a.bytesize == b.bytesize
350
+
351
+ if OpenSSL.respond_to?(:fixed_length_secure_compare)
352
+ OpenSSL.fixed_length_secure_compare(a, b)
353
+ else
354
+ result = 0
355
+ a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
356
+ result.zero?
357
+ end
358
+ end
359
+ end
360
+ end
361
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BSV
4
+ module Wallet
5
+ # Duck-typed storage interface for wallet persistence.
6
+ #
7
+ # Include this module in storage adapters and override all methods.
8
+ # The default implementations raise NotImplementedError.
9
+ module StorageAdapter
10
+ def store_action(_action_data)
11
+ raise NotImplementedError, "#{self.class}#store_action not implemented"
12
+ end
13
+
14
+ def find_actions(_query)
15
+ raise NotImplementedError, "#{self.class}#find_actions not implemented"
16
+ end
17
+
18
+ def store_output(_output_data)
19
+ raise NotImplementedError, "#{self.class}#store_output not implemented"
20
+ end
21
+
22
+ def find_outputs(_query)
23
+ raise NotImplementedError, "#{self.class}#find_outputs not implemented"
24
+ end
25
+
26
+ def delete_output(_outpoint)
27
+ raise NotImplementedError, "#{self.class}#delete_output not implemented"
28
+ end
29
+
30
+ def store_certificate(_cert_data)
31
+ raise NotImplementedError, "#{self.class}#store_certificate not implemented"
32
+ end
33
+
34
+ def find_certificates(_query)
35
+ raise NotImplementedError, "#{self.class}#find_certificates not implemented"
36
+ end
37
+
38
+ def delete_certificate(type:, serial_number:, certifier:)
39
+ raise NotImplementedError, "#{self.class}#delete_certificate not implemented"
40
+ end
41
+
42
+ def count_actions(_query)
43
+ raise NotImplementedError, "#{self.class}#count_actions not implemented"
44
+ end
45
+
46
+ def count_outputs(_query)
47
+ raise NotImplementedError, "#{self.class}#count_outputs not implemented"
48
+ end
49
+ end
50
+ end
51
+ end