@did-btcr2/kms 0.2.0 → 0.4.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.
package/src/kms.ts CHANGED
@@ -1,324 +1,272 @@
1
- import { Bytes, HashBytes, KeyBytes, KeyManagerError, SignatureBytes } from '@did-btcr2/common';
1
+ import {
2
+ Bytes,
3
+ HashBytes,
4
+ KeyBytes,
5
+ KeyManagerError,
6
+ SignatureBytes
7
+ } from '@did-btcr2/common';
2
8
  import { SchnorrKeyPair } from '@did-btcr2/keypair';
3
9
  import { sha256 } from '@noble/hashes/sha2.js';
4
- import { KeyIdentifier, KeyManager } from './interface.js';
10
+ import {
11
+ GenerateKeyOptions,
12
+ ImportKeyOptions,
13
+ KeyEntry,
14
+ KeyIdentifier,
15
+ KeyManager,
16
+ SignOptions,
17
+ } from './interface.js';
5
18
  import { KeyValueStore, MemoryStore } from './store.js';
6
19
 
7
20
  /**
8
- * Class for managing cryptographic keys for the BTCR2 DID method.
9
- * @class Kms
10
- * @type {Kms}
21
+ * Key Management System for the did:btcr2 DID method.
22
+ *
23
+ * Implements the {@link KeyManager} interface with a pluggable
24
+ * {@link KeyValueStore} (defaults to {@link MemoryStore}).
25
+ *
26
+ * Supports both signing (secret key present) and watch-only
27
+ * (public-key-only) key entries, and both Schnorr and ECDSA
28
+ * signature schemes.
29
+ *
11
30
  */
12
31
  export class Kms implements KeyManager {
13
- /**
14
- * Singleton instance of the Kms.
15
- * @private
16
- * @type {KeyManager}
17
- */
18
- static #instance?: Kms;
19
-
20
- /**
21
- * The `store` is a private variable in `KeyManager`. It is a `KeyValueStore` instance used for
22
- * storing and managing cryptographic keys. It allows the `KeyManager` class to save,
23
- * retrieve, and handle keys efficiently within the local Key Management System (KMS) context.
24
- * This variable can be configured to use different storage backends, like in-memory storage or
25
- * persistent storage, providing flexibility in key management according to the application's
26
- * requirements.
27
- * @private
28
- * @type {KeyValueStore<KeyIdentifier, KeyBytes>} The key store for managing cryptographic keys.
29
- */
30
- #store: KeyValueStore<KeyIdentifier, KeyBytes>;
31
-
32
-
33
- /**
34
- * The `#activeKeyId` property is a string that points to the currently active key.
35
- * It is used to identify the key that will be used for signing and verifying operations.
36
- * This property is optional and can be set to a specific key ID when initializing the
37
- * `KeyManager` instance. If not set, the key manager will use the default key id.
38
- * @private
39
- * @type {KeyIdentifier}
40
- */
32
+ #store: KeyValueStore<KeyIdentifier, KeyEntry>;
41
33
  #activeKeyId?: KeyIdentifier;
42
34
 
43
35
  /**
44
- * Creates an instance of KeyManager.
45
- * @param {KeyValueStore<KeyIdentifier, KeyBytes>} store An optional property to specify a custom
46
- * `KeyValueStore` instance for key management. If not provided, {@link KeyManager} uses a default `MemoryStore`
47
- * instance. This store is responsible for managing cryptographic keys, allowing them to be retrieved, stored, and
48
- * managed during cryptographic operations.
36
+ * Create a new KMS instance.
37
+ *
38
+ * @param {KeyValueStore<KeyIdentifier, KeyEntry>} [store] Optional key-value store.
39
+ * Defaults to in-memory store if not provided.
49
40
  */
50
- constructor(store?: KeyValueStore<KeyIdentifier, KeyBytes>) {
51
- // Set the default key store to a MemoryStore instance
52
- this.#store = store ?? new MemoryStore<KeyIdentifier, KeyBytes>();
41
+ constructor(store?: KeyValueStore<KeyIdentifier, KeyEntry>) {
42
+ this.#store = store ?? new MemoryStore<KeyIdentifier, KeyEntry>();
53
43
  }
54
44
 
55
45
  /**
56
- * Gets the ID of the active key.
57
- * @returns {KeyIdentifier | undefined} The ID of the active key.
46
+ * Get the active key identifier.
47
+ *
48
+ * @returns {KeyIdentifier | undefined} The active key identifier, or undefined if none is set.
58
49
  */
59
50
  get activeKeyId(): KeyIdentifier | undefined {
60
51
  return this.#activeKeyId;
61
52
  }
62
53
 
63
54
  /**
64
- * Gets the key pair associated with the given ID or the active key if no ID is provided.
65
- * @param {KeyIdentifier} [id] The ID of the key to get.
66
- * @returns {KeyBytes} A promise resolving to the key pair.
67
- * @throws {KeyManagerError} If the key is not found or no active key is set.
68
- */
69
- #getKeyOrThrow(id?: KeyIdentifier): SchnorrKeyPair {
70
- // Get the key id
71
- const keyId = id ?? this.#activeKeyId;
72
- // Throw an error if no active key is set
73
- if (!keyId) {
74
- throw new KeyManagerError('No active key set', 'ACTIVE_KEY_URI_NOT_SET');
75
- }
76
-
77
- // Get the secret key from the store, throw an error if not found
78
- const _secretKey = this.#store.get(keyId);
79
- if (!_secretKey) {
80
- throw new KeyManagerError(`Key not found: ${keyId}`, 'KEY_NOT_FOUND');
81
- }
82
-
83
- // Create a key pair from the secret key
84
- const kp = new SchnorrKeyPair({ secretKey: _secretKey });
85
-
86
- // Return the secret key
87
- return kp;
88
- }
89
-
90
- /**
91
- * Checks if a key with the given ID exists in the key store.
92
- * @param {KeyIdentifier} id The ID of the key to check.
93
- * @returns {boolean} A promise resolving to a boolean indicating if the key exists.
55
+ * Generate a URN-style key identifier from compressed public key bytes.
56
+ * Format: `urn:kms:secp256k1:<fingerprint>` where fingerprint is the
57
+ * first 8 bytes of SHA-256(publicKey), hex-encoded.
58
+ *
59
+ * @param {KeyBytes} publicKeyBytes Compressed secp256k1 public key bytes.
60
+ * @returns {KeyIdentifier} The generated key identifier.
94
61
  */
95
- #exists(id: KeyIdentifier): boolean {
96
- const key = this.#store.get(id);
97
- return !!key;
62
+ #generateUrn(publicKeyBytes: KeyBytes): KeyIdentifier {
63
+ const hash = sha256(publicKeyBytes);
64
+ const fingerprint = Array.from(hash.slice(0, 8))
65
+ .map(b => b.toString(16).padStart(2, '0'))
66
+ .join('');
67
+ return `urn:kms:secp256k1:${fingerprint}`;
98
68
  }
99
69
 
100
70
  /**
101
- * Removes a key from the key store.
102
- * @param {KeyIdentifier} id The key identifier of the key to remove.
103
- * @param {{ force?: boolean }} options The options for removing the key.
104
- * @param {boolean} [options.force] Whether to force the removal of the key.
105
- * @returns {void} A promise that resolves when the key is removed.
106
- * @throws {KeyManagerError} If attempting to remove the active key without force.
71
+ * Retrieve a key entry or throw if not found / no active key set.
72
+ *
73
+ * @param {KeyIdentifier} [id] Key identifier. Uses active key if omitted.
74
+ * @returns {KeyEntry} The retrieved key entry.
75
+ * @throws {KeyManagerError} If key not found or no active key set.
107
76
  */
108
- removeKey(id: KeyIdentifier, options: { force?: boolean } = {}): void {
109
- // Check if trying to remove the active key without force
110
- if (this.#activeKeyId === id && !options.force) {
111
- throw new KeyManagerError('Cannot remove active key (use "force": true or switch active key)', 'ACTIVE_KEY_DELETE');
112
- }
113
-
114
- // Check if the key exists, if not throw an error
115
- if (!this.#exists(id)) {
116
- throw new KeyManagerError(`Key not found: ${id}`, 'KEY_NOT_FOUND');
77
+ #getEntryOrThrow(id?: KeyIdentifier): KeyEntry {
78
+ const keyId = id ?? this.#activeKeyId;
79
+ if (!keyId) {
80
+ throw new KeyManagerError('No active key set', 'ACTIVE_KEY_NOT_SET');
117
81
  }
118
-
119
- // Remove the key from the store
120
- this.#store.delete(id);
121
-
122
- // Clear the active key if it was the one removed
123
- if (this.#activeKeyId === id) {
124
- this.#activeKeyId = undefined;
82
+ const entry = this.#store.get(keyId);
83
+ if (!entry) {
84
+ throw new KeyManagerError(`Key not found: ${keyId}`, 'KEY_NOT_FOUND');
125
85
  }
86
+ return entry;
126
87
  }
127
88
 
128
89
  /**
129
- * Lists all key identifiers in the key store.
130
- * @returns {Promise<KeyIdentifier[]>} A promise that resolves to an array of key identifiers.
131
- */
132
- listKeys(): KeyIdentifier[] {
133
- return this.#store.entries().flatMap(([k, _]) => [k as KeyIdentifier]);
134
- }
135
-
136
- /**
137
- * Sets the active key to the key associated with the given ID.
138
- * @param {KeyIdentifier} id The ID of the key to set as active.
139
- * @returns {Promise<void>} A promise that resolves when the active key is set.
90
+ * Set the active key.
91
+ *
92
+ * @param id The key identifier to set as active.
140
93
  * @throws {KeyManagerError} If the key is not found.
141
94
  */
142
95
  setActiveKey(id: KeyIdentifier): void {
143
- // Check if the key exists, if not throw an error
144
- this.#getKeyOrThrow(id);
145
-
146
- // Set the active key ID
96
+ this.#getEntryOrThrow(id);
147
97
  this.#activeKeyId = id;
148
98
  }
149
99
 
150
100
  /**
151
- * Gets the public key associated with the given ID or the active key if no ID is provided.
152
- * @param {KeyIdentifier} [id] The ID of the key to get the public key for.
153
- * @returns {Promise<KeyBytes>} A promise resolving to the public key bytes.
101
+ * Get the compressed public key bytes for a key.
102
+ *
103
+ * @param id Key identifier. Uses active key if omitted.
104
+ * @returns Compressed secp256k1 public key bytes.
105
+ * @throws {KeyManagerError} If key not found or no active key set.
154
106
  */
155
107
  getPublicKey(id?: KeyIdentifier): KeyBytes {
156
- // Get the key pair from the store
157
- const { publicKey } = this.#getKeyOrThrow(id);
158
-
159
- // Return the public key bytes
160
- return publicKey.compressed;
108
+ return this.#getEntryOrThrow(id).publicKey;
161
109
  }
162
110
 
163
111
  /**
164
- * Signs the given data using the key associated with the key ID.
112
+ * Sign data using the specified key.
113
+ *
165
114
  * @param {Bytes} data The data to sign.
166
- * @param {KeyIdentifier} [id] The ID of the key to sign the data with.
167
- * @returns {Promise<SignatureBytes>} A promise resolving to the signature of the data.
115
+ * @param {KeyIdentifier} [id] Key identifier. Uses active key if omitted.
116
+ * @param {SignOptions} [options] Signing options (scheme defaults to 'schnorr').
117
+ * @returns {SignatureBytes} The signature bytes.
118
+ * @throws {KeyManagerError} If key not found, no active key, or key cannot sign.
168
119
  */
169
- sign(data: Bytes, id?: KeyIdentifier): SignatureBytes {
170
- // Get the key from the store
171
- const { secretKey } = this.#getKeyOrThrow(id);
172
-
173
- // Check if the key can sign
174
- if(!secretKey) {
175
- throw new KeyManagerError(`Key ID ${id} is not a signer`, 'KEY_NOT_SIGNER');
120
+ sign(data: Bytes, id?: KeyIdentifier, options: SignOptions = {}): SignatureBytes {
121
+ const entry = this.#getEntryOrThrow(id);
122
+ if (!entry.secretKey) {
123
+ const keyId = id ?? this.#activeKeyId;
124
+ throw new KeyManagerError(`Key is not a signing key: ${keyId}`, 'KEY_NOT_SIGNER');
176
125
  }
177
-
178
- // Sign the data using the key and return the signature
179
- return secretKey.sign(data);
126
+ const kp = new SchnorrKeyPair({ secretKey: entry.secretKey });
127
+ return kp.secretKey.sign(data, { scheme: options.scheme ?? 'schnorr' });
180
128
  }
181
129
 
182
130
  /**
183
- * Verifies a signature using the key associated with the key ID.
184
- * @param {KeyIdentifier} id The ID of the key to verify the signature with.
185
- * @param {SignatureBytes} signature The signature to verify.
186
- * @param {Hex} data The data to verify the signature with.
187
- * @returns {Promise<boolean>} A promise resolving to a boolean indicating the verification result.
131
+ * Verify a signature using the specified key.
132
+ *
133
+ * @param {SignatureBytes} signature The signature bytes to verify.
134
+ * @param {Bytes} data The data that was signed.
135
+ * @param {KeyIdentifier} [id] Key identifier. Uses active key if omitted.
136
+ * @param {SignOptions} [options] Verification options (scheme defaults to 'schnorr').
137
+ * @returns {boolean} True if the signature is valid, false otherwise.
138
+ * @throws {KeyManagerError} If key not found or no active key set.
188
139
  */
189
- verify(signature: SignatureBytes, data: Bytes, id?: KeyIdentifier): boolean {
190
- // Get the key from the store
191
- const { publicKey } = this.#getKeyOrThrow(id);
192
-
193
- // Verify the signature using the multikey
194
- return publicKey.verify(signature, data);
140
+ verify(signature: SignatureBytes, data: Bytes, id?: KeyIdentifier, options: SignOptions = {}): boolean {
141
+ const entry = this.#getEntryOrThrow(id);
142
+ const kp = new SchnorrKeyPair({ publicKey: entry.publicKey });
143
+ return kp.publicKey.verify(signature, data, { scheme: options.scheme ?? 'schnorr' });
195
144
  }
196
145
 
197
146
  /**
198
- * Imports a key pair into the key store.
147
+ * Import a key pair into the KMS.
148
+ *
199
149
  * @param {SchnorrKeyPair} keyPair The key pair to import.
200
- * @param {{ id?: KeyIdentifier; setActive?: boolean }} options The options for importing the key pair.
201
- * @param {KeyIdentifier} [options.id] The ID of the key to import (optional).
202
- * @param {boolean} [options.setActive] Whether to set the key as active (optional, default: true).
203
- * @returns {Promise<KeyIdentifier>} A promise resolving to the ID of the imported key.
150
+ * @param {ImportKeyOptions} [options] Import options (id, tags, setActive).
151
+ * @returns {KeyIdentifier} The identifier of the imported key.
152
+ * @throws {KeyManagerError} If a key with the same identifier already exists.
204
153
  */
205
- importKey(
206
- keyPair: SchnorrKeyPair,
207
- options: {
208
- id?: KeyIdentifier;
209
- setActive?: boolean
210
- } = {}
211
- ): KeyIdentifier {
212
- // Ensure the key pair has a public key
213
- if(!keyPair.publicKey) {
214
- keyPair.publicKey = keyPair.secretKey.computePublicKey();
215
- }
154
+ importKey(keyPair: SchnorrKeyPair, options: ImportKeyOptions = {}): KeyIdentifier {
155
+ const id = options.id ?? this.#generateUrn(keyPair.publicKey.compressed);
216
156
 
217
- // Determine the key ID
218
- const id = options.id ?? (keyPair.publicKey.hex as string);
219
-
220
- // Check if the key already exists
221
- if (this.#exists(id)) {
157
+ if (this.#store.has(id)) {
222
158
  throw new KeyManagerError(`Key already exists: ${id}`, 'KEY_FOUND');
223
159
  }
224
160
 
225
- // Store the key pair in the key store
226
- this.#store.set(id, keyPair.secretKey.bytes);
161
+ // Build key entry secret key may not be available for watch-only pairs
162
+ const entry: KeyEntry = {
163
+ publicKey : keyPair.publicKey.compressed,
164
+ ...(options.tags && { tags: options.tags }),
165
+ };
166
+
167
+ try {
168
+ if (keyPair.secretKey) {
169
+ entry.secretKey = keyPair.secretKey.bytes;
170
+ }
171
+ } catch {
172
+ // Public-key-only key pair — secretKey getter throws
173
+ }
227
174
 
228
- // Determine whether to set the key as active, defaulting to true
229
- const setActive = options.setActive ?? true;
175
+ this.#store.set(id, entry);
230
176
 
231
- // Set the active key if specified
232
- if (setActive) {
177
+ if (options.setActive) {
233
178
  this.#activeKeyId = id;
234
179
  }
235
180
 
236
- // Return the key ID
237
181
  return id;
238
182
  }
239
183
 
240
184
  /**
241
- * Computes the hash of the given data.
242
- * @param {Uint8Array} data The data to hash.
243
- * @returns {HashBytes} The hash of the data.
244
- */
245
- digest(data: Uint8Array): HashBytes {
246
- return sha256(data);
247
- }
248
-
249
- /**
250
- * Generates a new key pair and stores it in the key store.
251
- * @returns {KeyIdentifier} The key identifier of the generated key.
185
+ * Remove a key from the KMS.
186
+ *
187
+ * @param {KeyIdentifier} id The key identifier to remove.
188
+ * @param {Object} [options] Removal options.
189
+ * @param {boolean} [options.force=false] Force removal of active key.
190
+ * @throws {KeyManagerError} If key not found or attempting to remove active key without force.
252
191
  */
253
- generateKey(): KeyIdentifier {
254
- // Generate a new Schnorr key pair
255
- const kp = SchnorrKeyPair.generate();
192
+ removeKey(id: KeyIdentifier, options: { force?: boolean } = {}): void {
193
+ if (this.#activeKeyId === id && !options.force) {
194
+ throw new KeyManagerError(
195
+ 'Cannot remove active key (use "force": true or switch active key)',
196
+ 'ACTIVE_KEY_DELETE'
197
+ );
198
+ }
256
199
 
257
- // Store the key pair in the key store
258
- const id = kp.publicKey.hex;
259
- this.#store.set(id, kp.secretKey.bytes);
200
+ if (!this.#store.has(id)) {
201
+ throw new KeyManagerError(`Key not found: ${id}`, 'KEY_NOT_FOUND');
202
+ }
260
203
 
261
- // Set the active key to the newly generated key
262
- this.#activeKeyId = id;
204
+ this.#store.delete(id);
263
205
 
264
- // Return the key ID
265
- return id;
206
+ if (this.#activeKeyId === id) {
207
+ this.#activeKeyId = undefined;
208
+ }
266
209
  }
267
210
 
268
211
  /**
269
- * Initializes a singleton KeyManager instance.
270
- * @param {SchnorrKeyPair} keyPair The secret key to import.
271
- * @param {string} id The ID to set as the active key.
272
- * @returns {void}
212
+ * List all key identifiers in the KMS.
213
+ *
214
+ * @returns {KeyIdentifier[]} Array of key identifiers.
273
215
  */
274
- static initialize(keyPair: SchnorrKeyPair, id: string): Kms {
275
- // Check if the KeyManager instance is already initialized
276
- if (Kms.#instance) {
277
- console.warn('WARNING: Kms global instance is already initialized.');
278
- return Kms.#instance;
279
- }
280
-
281
- // Check if the keypair is provided
282
- if(!keyPair) {
283
- // Log a warning message if not provided
284
- console.warn('WARNING: secretKey not provided, generating new SchnorrKeyPair ...');
285
- }
216
+ listKeys(): KeyIdentifier[] {
217
+ return this.#store.entries().map(([k]) => k);
218
+ }
286
219
 
287
- // Generate a new keypair if not provided
288
- keyPair ??= SchnorrKeyPair.generate();
220
+ /**
221
+ * Compute the SHA-256 digest of the given data.
222
+ *
223
+ * @param {Uint8Array} data The data to digest.
224
+ * @returns {HashBytes} The SHA-256 hash of the data.
225
+ */
226
+ digest(data: Uint8Array): HashBytes {
227
+ return sha256(data);
228
+ }
289
229
 
290
- // Initialize the singleton key manager with the keypair
291
- Kms.#instance = new Kms();
230
+ /**
231
+ * Generate a new secp256k1 key pair and store it in the KMS.
232
+ *
233
+ * @param {GenerateKeyOptions} [options] Generation options (tags, setActive).
234
+ * @returns {KeyIdentifier} The identifier of the generated key.
235
+ */
236
+ generateKey(options: GenerateKeyOptions = {}): KeyIdentifier {
237
+ const kp = SchnorrKeyPair.generate();
238
+ const id = this.#generateUrn(kp.publicKey.compressed);
292
239
 
293
- // Import the keypair into the key store
294
- Kms.#instance.importKey(keyPair, { setActive: true, id });
240
+ const entry: KeyEntry = {
241
+ secretKey : kp.secretKey.bytes,
242
+ publicKey : kp.publicKey.compressed,
243
+ ...(options.tags && { tags: options.tags }),
244
+ };
295
245
 
296
- // Set the active key URI7
297
- Kms.#instance.#activeKeyId = id;
246
+ this.#store.set(id, entry);
298
247
 
299
- // Log the active key ID
300
- console.info(`Kms initialized with Active Key ID: ${Kms.#instance.#activeKeyId}`);
248
+ if (options.setActive) {
249
+ this.#activeKeyId = id;
250
+ }
301
251
 
302
- // Return the singleton instance
303
- return Kms.#instance;
252
+ return id;
304
253
  }
305
254
 
306
255
  /**
307
- * Retrieves a keypair from the key store using the provided key ID.
308
- * @public
309
- * @param {KeyIdentifier} id The ID of the keypair to retrieve.
310
- * @returns {Promise<SchnorrKeyPair | undefined>} The retrieved keypair, or undefined if not found.
256
+ * Export the key pair for a stored key.
257
+ *
258
+ * Only available on the concrete {@link Kms} class, not on the
259
+ * {@link KeyManager} interface. HSM or hardware-backed implementations
260
+ * may not support key export.
261
+ *
262
+ * @param {KeyIdentifier} id The key identifier to export.
263
+ * @returns {SchnorrKeyPair} The reconstructed SchnorrKeyPair.
311
264
  */
312
- static getKey(id?: KeyIdentifier): SchnorrKeyPair | undefined {
313
- // Ensure the Kms instance is initialized
314
- if(!Kms.#instance) {
315
- throw new KeyManagerError('Kms instance not initialized', 'KMS_NOT_INITIALIZED');
265
+ exportKey(id: KeyIdentifier): SchnorrKeyPair {
266
+ const entry = this.#getEntryOrThrow(id);
267
+ if (entry.secretKey) {
268
+ return new SchnorrKeyPair({ secretKey: entry.secretKey });
316
269
  }
317
-
318
- // Use the active key ID if not provided
319
- id ??= Kms.#instance.activeKeyId;
320
-
321
- // Instantiate a new Kms with the default key store
322
- return Kms.#instance.#getKeyOrThrow(id);
270
+ return new SchnorrKeyPair({ publicKey: entry.publicKey });
323
271
  }
324
- }
272
+ }