forked from SatoshiPortal/bullbitcoin-mobile
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnostr_keychain_handle.dart
More file actions
185 lines (167 loc) · 5.65 KB
/
Copy pathnostr_keychain_handle.dart
File metadata and controls
185 lines (167 loc) · 5.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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
91
92
93
94
95
96
97
98
99
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import 'package:bip85_entropy/bip85_entropy.dart' as bip85;
import 'package:bech32/bech32.dart';
import 'package:bitcoin_base/bitcoin_base.dart';
import 'package:convert/convert.dart';
/// BIP85 application number for direct Nostr key derivation.
///
/// Path suffix: `128002'/{identity}'/{account_index}'`.
const int nostrBip85Application = 128002;
/// In-memory handle for a Nostr signing key.
///
/// Do not store this type in DTOs or persistence models. It exposes public-key
/// access and hash signing, but it does not expose raw secret-key material.
final class NostrKeychainHandle {
final ECPrivate _key;
NostrKeychainHandle._(this._key);
factory NostrKeychainHandle._fromSecretKeyHex(String secretKeyHex) {
return NostrKeychainHandle._(ECPrivate.fromHex(secretKeyHex));
}
factory NostrKeychainHandle.deriveFromBip85Path({
required String xprvBase58,
required String hardenedPath,
}) {
_validateNostrBip85Path(hardenedPath);
final path = bip85.Bip85HardenedPath(hardenedPath);
final entropyHex = bip85.Bip85Entropy.deriveFromHardenedPath(
xprvBase58: xprvBase58,
path: path,
);
return NostrKeychainHandle._fromSecretKeyHex(entropyHex.substring(0, 64));
}
String get publicKeyHex => _key.getPublic().toXOnlyHex();
/// NIP-19 `npub` display encoding of [publicKeyHex].
String get npub => NostrPublicKeyEncoding.npubFromPublicKeyHex(publicKeyHex);
String signHashHex(String messageHashHex) {
final digest = hex.decode(messageHashHex);
if (digest.length != 32) {
throw ArgumentError.value(
messageHashHex,
'messageHashHex',
'Nostr signing requires a 32-byte hash hex value',
);
}
return _key.signBip340(digest, tweak: false);
}
@override
String toString() => 'NostrKeychainHandle(publicKeyHex: $publicKeyHex)';
}
void _validateNostrBip85Path(String hardenedPath) {
final match = RegExp(r"^(\d+)'/(\d+)'/(\d+)'$").firstMatch(hardenedPath);
if (match == null) {
throw ArgumentError.value(
hardenedPath,
'hardenedPath',
"Expected a Nostr BIP85 path shaped as 128002'/identity'/account'",
);
}
final application = int.parse(match.group(1)!);
final identity = int.parse(match.group(2)!);
final account = int.parse(match.group(3)!);
const maxHardenedChild = 0x7fffffff;
if (application > maxHardenedChild ||
identity > maxHardenedChild ||
account > maxHardenedChild) {
throw ArgumentError.value(
hardenedPath,
'hardenedPath',
'Nostr BIP85 path components exceed the hardened child range',
);
}
if (application != nostrBip85Application) {
throw ArgumentError.value(
hardenedPath,
'hardenedPath',
'Expected Nostr BIP85 application $nostrBip85Application',
);
}
if (identity == 0) {
throw ArgumentError.value(
hardenedPath,
'hardenedPath',
'Nostr identity zero is reserved by BIP85',
);
}
if (account == 0) {
throw ArgumentError.value(
hardenedPath,
'hardenedPath',
'Nostr account zero is reserved by BIP85',
);
}
}
/// NIP-19 bech32 display encoding for Nostr public keys.
///
/// A stored 32-byte x-only public key is never shown as hex in the UI: `npub`
/// is the only representation users see, so every surface encodes through here
/// instead of formatting the hex itself. Shares the bech32 encoder and bit
/// converter used for `nsec`, so the two encodings cannot drift apart.
final class NostrPublicKeyEncoding {
const NostrPublicKeyEncoding._();
static final _publicKeyHexPattern = RegExp(r'^[0-9a-fA-F]{64}$');
static String npubFromPublicKeyHex(String publicKeyHex) {
final normalized = publicKeyHex.trim().toLowerCase();
if (!_publicKeyHexPattern.hasMatch(normalized)) {
throw ArgumentError.value(
publicKeyHex,
'publicKeyHex',
'Nostr public key must be 32-byte hex',
);
}
return bech32.encode(
Bech32('npub', _convertBits(hex.decode(normalized), 8, 5, true)),
);
}
}
/// Secret materialization is deliberately a one-shot operation. Callers must
/// not persist the returned value or place it in application state.
final class NostrKeychainSecretMaterializer {
const NostrKeychainSecretMaterializer._();
static String deriveSecretKeyHex({
required String xprvBase58,
required String hardenedPath,
}) {
_validateNostrBip85Path(hardenedPath);
final path = bip85.Bip85HardenedPath(hardenedPath);
final entropyHex = bip85.Bip85Entropy.deriveFromHardenedPath(
xprvBase58: xprvBase58,
path: path,
);
return entropyHex.substring(0, 64);
}
static String deriveNsec({
required String xprvBase58,
required String hardenedPath,
}) {
final secretKeyHex = deriveSecretKeyHex(
xprvBase58: xprvBase58,
hardenedPath: hardenedPath,
);
return bech32.encode(
Bech32('nsec', _convertBits(hex.decode(secretKeyHex), 8, 5, true)),
);
}
}
List<int> _convertBits(List<int> data, int from, int to, bool pad) {
var accumulator = 0;
var bits = 0;
final result = <int>[];
final maxValue = (1 << to) - 1;
final maxAccumulator = (1 << (from + to - 1)) - 1;
for (final value in data) {
if (value < 0 || value >> from != 0) {
throw ArgumentError.value(value, 'data');
}
accumulator = ((accumulator << from) | value) & maxAccumulator;
bits += from;
while (bits >= to) {
bits -= to;
result.add((accumulator >> bits) & maxValue);
}
}
if (pad) {
if (bits > 0) result.add((accumulator << (to - bits)) & maxValue);
} else if (bits >= from || ((accumulator << (to - bits)) & maxValue) != 0) {
throw ArgumentError('Invalid bit conversion padding');
}
return result;
}