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.
- checksums.yaml +7 -0
- data/LICENSE +86 -0
- data/lib/bsv/wallet_interface/errors/invalid_hmac_error.rb +11 -0
- data/lib/bsv/wallet_interface/errors/invalid_parameter_error.rb +14 -0
- data/lib/bsv/wallet_interface/errors/invalid_signature_error.rb +11 -0
- data/lib/bsv/wallet_interface/errors/unsupported_action_error.rb +11 -0
- data/lib/bsv/wallet_interface/errors/wallet_error.rb +14 -0
- data/lib/bsv/wallet_interface/interface.rb +384 -0
- data/lib/bsv/wallet_interface/key_deriver.rb +142 -0
- data/lib/bsv/wallet_interface/memory_store.rb +115 -0
- data/lib/bsv/wallet_interface/proto_wallet.rb +361 -0
- data/lib/bsv/wallet_interface/storage_adapter.rb +51 -0
- data/lib/bsv/wallet_interface/validators.rb +126 -0
- data/lib/bsv/wallet_interface/version.rb +7 -0
- data/lib/bsv/wallet_interface/wallet_client.rb +486 -0
- data/lib/bsv/wallet_interface.rb +25 -0
- data/lib/bsv-wallet.rb +4 -0
- metadata +87 -0
|
@@ -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
|