@phantom/indexed-db-stamper 0.1.4 → 1.0.0-beta.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/dist/index.d.ts +19 -6
- package/dist/index.js +99 -55
- package/dist/index.mjs +99 -55
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -25,8 +25,8 @@ declare class IndexedDbStamper implements StamperWithKeyManagement {
|
|
|
25
25
|
private storeName;
|
|
26
26
|
private keyName;
|
|
27
27
|
private db;
|
|
28
|
-
private
|
|
29
|
-
private
|
|
28
|
+
private activeKeyPairRecord;
|
|
29
|
+
private pendingKeyPairRecord;
|
|
30
30
|
algorithm: Algorithm;
|
|
31
31
|
type: "PKI" | "OIDC";
|
|
32
32
|
idToken?: string;
|
|
@@ -66,10 +66,23 @@ declare class IndexedDbStamper implements StamperWithKeyManagement {
|
|
|
66
66
|
clear(): Promise<void>;
|
|
67
67
|
private clearStoredKeys;
|
|
68
68
|
private openDB;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Generate a new keypair for rotation without making it active
|
|
71
|
+
*/
|
|
72
|
+
rotateKeyPair(): Promise<StamperKeyInfo>;
|
|
73
|
+
/**
|
|
74
|
+
* Switch to the pending keypair, making it active and cleaning up the old one
|
|
75
|
+
*/
|
|
76
|
+
commitRotation(authenticatorId: string): Promise<void>;
|
|
77
|
+
/**
|
|
78
|
+
* Discard the pending keypair on rotation failure
|
|
79
|
+
*/
|
|
80
|
+
rollbackRotation(): Promise<void>;
|
|
81
|
+
private generateAndStoreNewKeyPair;
|
|
82
|
+
private storeKeyPairRecord;
|
|
83
|
+
private loadActiveKeyPairRecord;
|
|
84
|
+
private loadPendingKeyPairRecord;
|
|
85
|
+
private removeKeyPairRecord;
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
export { IndexedDbStamper, IndexedDbStamperConfig };
|
package/dist/index.js
CHANGED
|
@@ -40,8 +40,8 @@ var IndexedDbStamper = class {
|
|
|
40
40
|
// Optional for PKI, required for OIDC
|
|
41
41
|
constructor(config = {}) {
|
|
42
42
|
this.db = null;
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
43
|
+
this.activeKeyPairRecord = null;
|
|
44
|
+
this.pendingKeyPairRecord = null;
|
|
45
45
|
this.algorithm = import_sdk_types.Algorithm.ed25519;
|
|
46
46
|
// Use Ed25519 for maximum security and performance
|
|
47
47
|
// The type of stamper, can be changed at any time
|
|
@@ -61,29 +61,27 @@ var IndexedDbStamper = class {
|
|
|
61
61
|
*/
|
|
62
62
|
async init() {
|
|
63
63
|
await this.openDB();
|
|
64
|
-
|
|
65
|
-
if (!
|
|
66
|
-
|
|
67
|
-
} else {
|
|
68
|
-
await this.loadKeyPair();
|
|
64
|
+
this.activeKeyPairRecord = await this.loadActiveKeyPairRecord();
|
|
65
|
+
if (!this.activeKeyPairRecord) {
|
|
66
|
+
this.activeKeyPairRecord = await this.generateAndStoreNewKeyPair("active");
|
|
69
67
|
}
|
|
70
|
-
this.
|
|
71
|
-
return keyInfo;
|
|
68
|
+
this.pendingKeyPairRecord = await this.loadPendingKeyPairRecord();
|
|
69
|
+
return this.activeKeyPairRecord.keyInfo;
|
|
72
70
|
}
|
|
73
71
|
/**
|
|
74
72
|
* Get the public key information
|
|
75
73
|
*/
|
|
76
74
|
getKeyInfo() {
|
|
77
|
-
return this.keyInfo;
|
|
75
|
+
return this.activeKeyPairRecord?.keyInfo || null;
|
|
78
76
|
}
|
|
79
77
|
/**
|
|
80
78
|
* Reset the key pair by generating a new one
|
|
81
79
|
*/
|
|
82
80
|
async resetKeyPair() {
|
|
83
81
|
await this.clearStoredKeys();
|
|
84
|
-
|
|
85
|
-
this.
|
|
86
|
-
return keyInfo;
|
|
82
|
+
this.activeKeyPairRecord = await this.generateAndStoreNewKeyPair("active");
|
|
83
|
+
this.pendingKeyPairRecord = null;
|
|
84
|
+
return this.activeKeyPairRecord.keyInfo;
|
|
87
85
|
}
|
|
88
86
|
/**
|
|
89
87
|
* Create X-Phantom-Stamp header value using stored private key
|
|
@@ -92,7 +90,7 @@ var IndexedDbStamper = class {
|
|
|
92
90
|
*/
|
|
93
91
|
async stamp(params) {
|
|
94
92
|
const { data } = params;
|
|
95
|
-
if (!this.
|
|
93
|
+
if (!this.activeKeyPairRecord) {
|
|
96
94
|
throw new Error("Stamper not initialized. Call init() first.");
|
|
97
95
|
}
|
|
98
96
|
const dataBytes = new Uint8Array(data);
|
|
@@ -101,7 +99,7 @@ var IndexedDbStamper = class {
|
|
|
101
99
|
name: this.algorithm,
|
|
102
100
|
hash: "SHA-256"
|
|
103
101
|
},
|
|
104
|
-
this.
|
|
102
|
+
this.activeKeyPairRecord.keyPair.privateKey,
|
|
105
103
|
dataBytes
|
|
106
104
|
);
|
|
107
105
|
const signatureBase64url = (0, import_base64url.base64urlEncode)(new Uint8Array(signature));
|
|
@@ -110,14 +108,14 @@ var IndexedDbStamper = class {
|
|
|
110
108
|
const salt = params.type === "OIDC" ? params.salt : this.salt;
|
|
111
109
|
const stampData = stampType === "PKI" ? {
|
|
112
110
|
// Decode base58 public key to bytes, then encode as base64url (consistent with ApiKeyStamper)
|
|
113
|
-
publicKey: (0, import_base64url.base64urlEncode)(import_bs58.default.decode(this.keyInfo.publicKey)),
|
|
111
|
+
publicKey: (0, import_base64url.base64urlEncode)(import_bs58.default.decode(this.activeKeyPairRecord.keyInfo.publicKey)),
|
|
114
112
|
signature: signatureBase64url,
|
|
115
113
|
kind: "PKI",
|
|
116
114
|
algorithm: this.algorithm
|
|
117
115
|
} : {
|
|
118
116
|
kind: "OIDC",
|
|
119
117
|
idToken,
|
|
120
|
-
publicKey: (0, import_base64url.base64urlEncode)(import_bs58.default.decode(this.keyInfo.publicKey)),
|
|
118
|
+
publicKey: (0, import_base64url.base64urlEncode)(import_bs58.default.decode(this.activeKeyPairRecord.keyInfo.publicKey)),
|
|
121
119
|
salt,
|
|
122
120
|
algorithm: this.algorithm,
|
|
123
121
|
signature: signatureBase64url
|
|
@@ -130,8 +128,8 @@ var IndexedDbStamper = class {
|
|
|
130
128
|
*/
|
|
131
129
|
async clear() {
|
|
132
130
|
await this.clearStoredKeys();
|
|
133
|
-
this.
|
|
134
|
-
this.
|
|
131
|
+
this.activeKeyPairRecord = null;
|
|
132
|
+
this.pendingKeyPairRecord = null;
|
|
135
133
|
}
|
|
136
134
|
async clearStoredKeys() {
|
|
137
135
|
if (!this.db) {
|
|
@@ -140,8 +138,8 @@ var IndexedDbStamper = class {
|
|
|
140
138
|
return new Promise((resolve, reject) => {
|
|
141
139
|
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
142
140
|
const store = transaction.objectStore(this.storeName);
|
|
143
|
-
const
|
|
144
|
-
const
|
|
141
|
+
const deleteActiveKeyPair = store.delete(`${this.keyName}-active`);
|
|
142
|
+
const deletePendingKeyPair = store.delete(`${this.keyName}-pending`);
|
|
145
143
|
let completed = 0;
|
|
146
144
|
const total = 2;
|
|
147
145
|
const checkComplete = () => {
|
|
@@ -150,10 +148,10 @@ var IndexedDbStamper = class {
|
|
|
150
148
|
resolve();
|
|
151
149
|
}
|
|
152
150
|
};
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
151
|
+
deleteActiveKeyPair.onsuccess = checkComplete;
|
|
152
|
+
deletePendingKeyPair.onsuccess = checkComplete;
|
|
153
|
+
deleteActiveKeyPair.onerror = () => reject(deleteActiveKeyPair.error);
|
|
154
|
+
deletePendingKeyPair.onerror = () => reject(deletePendingKeyPair.error);
|
|
157
155
|
});
|
|
158
156
|
}
|
|
159
157
|
async openDB() {
|
|
@@ -172,8 +170,46 @@ var IndexedDbStamper = class {
|
|
|
172
170
|
};
|
|
173
171
|
});
|
|
174
172
|
}
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Generate a new keypair for rotation without making it active
|
|
175
|
+
*/
|
|
176
|
+
async rotateKeyPair() {
|
|
177
|
+
if (!this.db) {
|
|
178
|
+
await this.openDB();
|
|
179
|
+
}
|
|
180
|
+
this.pendingKeyPairRecord = await this.generateAndStoreNewKeyPair("pending");
|
|
181
|
+
return this.pendingKeyPairRecord.keyInfo;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Switch to the pending keypair, making it active and cleaning up the old one
|
|
185
|
+
*/
|
|
186
|
+
async commitRotation(authenticatorId) {
|
|
187
|
+
if (!this.pendingKeyPairRecord) {
|
|
188
|
+
throw new Error("No pending keypair to commit");
|
|
189
|
+
}
|
|
190
|
+
if (this.activeKeyPairRecord) {
|
|
191
|
+
await this.removeKeyPairRecord("active");
|
|
192
|
+
}
|
|
193
|
+
this.pendingKeyPairRecord.status = "active";
|
|
194
|
+
this.pendingKeyPairRecord.authenticatorId = authenticatorId;
|
|
195
|
+
this.pendingKeyPairRecord.keyInfo.authenticatorId = authenticatorId;
|
|
196
|
+
this.activeKeyPairRecord = this.pendingKeyPairRecord;
|
|
197
|
+
this.pendingKeyPairRecord = null;
|
|
198
|
+
await this.storeKeyPairRecord(this.activeKeyPairRecord, "active");
|
|
199
|
+
await this.removeKeyPairRecord("pending");
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Discard the pending keypair on rotation failure
|
|
203
|
+
*/
|
|
204
|
+
async rollbackRotation() {
|
|
205
|
+
if (!this.pendingKeyPairRecord) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
await this.removeKeyPairRecord("pending");
|
|
209
|
+
this.pendingKeyPairRecord = null;
|
|
210
|
+
}
|
|
211
|
+
async generateAndStoreNewKeyPair(type) {
|
|
212
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
177
213
|
{
|
|
178
214
|
name: "Ed25519"
|
|
179
215
|
},
|
|
@@ -181,65 +217,73 @@ var IndexedDbStamper = class {
|
|
|
181
217
|
// non-extractable - private key can never be exported
|
|
182
218
|
["sign", "verify"]
|
|
183
219
|
);
|
|
184
|
-
const rawPublicKeyBuffer = await crypto.subtle.exportKey("raw",
|
|
220
|
+
const rawPublicKeyBuffer = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
|
185
221
|
const publicKeyBase58 = import_bs58.default.encode(new Uint8Array(rawPublicKeyBuffer));
|
|
186
222
|
const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKeyBuffer);
|
|
187
223
|
const keyId = (0, import_base64url.base64urlEncode)(new Uint8Array(keyIdBuffer)).substring(0, 16);
|
|
224
|
+
const now = Date.now();
|
|
188
225
|
const keyInfo = {
|
|
189
226
|
keyId,
|
|
190
|
-
publicKey: publicKeyBase58
|
|
227
|
+
publicKey: publicKeyBase58,
|
|
228
|
+
createdAt: now
|
|
191
229
|
};
|
|
192
|
-
|
|
193
|
-
|
|
230
|
+
const record = {
|
|
231
|
+
keyPair,
|
|
232
|
+
keyInfo,
|
|
233
|
+
createdAt: now,
|
|
234
|
+
expiresAt: 0,
|
|
235
|
+
// Not used anymore, kept for backward compatibility
|
|
236
|
+
status: type
|
|
237
|
+
};
|
|
238
|
+
await this.storeKeyPairRecord(record, type);
|
|
239
|
+
return record;
|
|
194
240
|
}
|
|
195
|
-
async
|
|
241
|
+
async storeKeyPairRecord(record, type) {
|
|
196
242
|
if (!this.db) {
|
|
197
243
|
throw new Error("Database not initialized");
|
|
198
244
|
}
|
|
199
245
|
return new Promise((resolve, reject) => {
|
|
200
246
|
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
201
247
|
const store = transaction.objectStore(this.storeName);
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const total = 2;
|
|
206
|
-
const checkComplete = () => {
|
|
207
|
-
completed++;
|
|
208
|
-
if (completed === total) {
|
|
209
|
-
resolve();
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
keyPairRequest.onsuccess = checkComplete;
|
|
213
|
-
keyInfoRequest.onsuccess = checkComplete;
|
|
214
|
-
keyPairRequest.onerror = () => reject(keyPairRequest.error);
|
|
215
|
-
keyInfoRequest.onerror = () => reject(keyInfoRequest.error);
|
|
248
|
+
const request = store.put(record, `${this.keyName}-${type}`);
|
|
249
|
+
request.onsuccess = () => resolve();
|
|
250
|
+
request.onerror = () => reject(request.error);
|
|
216
251
|
});
|
|
217
252
|
}
|
|
218
|
-
async
|
|
253
|
+
async loadActiveKeyPairRecord() {
|
|
219
254
|
if (!this.db) {
|
|
220
|
-
return;
|
|
255
|
+
return null;
|
|
221
256
|
}
|
|
222
257
|
return new Promise((resolve, reject) => {
|
|
223
258
|
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
224
259
|
const store = transaction.objectStore(this.storeName);
|
|
225
|
-
const request = store.get(`${this.keyName}-
|
|
226
|
-
request.onsuccess = () =>
|
|
227
|
-
this.cryptoKeyPair = request.result || null;
|
|
228
|
-
resolve();
|
|
229
|
-
};
|
|
260
|
+
const request = store.get(`${this.keyName}-active`);
|
|
261
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
230
262
|
request.onerror = () => reject(request.error);
|
|
231
263
|
});
|
|
232
264
|
}
|
|
233
|
-
async
|
|
265
|
+
async loadPendingKeyPairRecord() {
|
|
234
266
|
if (!this.db) {
|
|
235
267
|
return null;
|
|
236
268
|
}
|
|
237
269
|
return new Promise((resolve, reject) => {
|
|
238
270
|
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
239
271
|
const store = transaction.objectStore(this.storeName);
|
|
240
|
-
const request = store.get(`${this.keyName}-
|
|
272
|
+
const request = store.get(`${this.keyName}-pending`);
|
|
241
273
|
request.onsuccess = () => resolve(request.result || null);
|
|
242
274
|
request.onerror = () => reject(request.error);
|
|
243
275
|
});
|
|
244
276
|
}
|
|
277
|
+
async removeKeyPairRecord(type) {
|
|
278
|
+
if (!this.db) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
return new Promise((resolve, reject) => {
|
|
282
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
283
|
+
const store = transaction.objectStore(this.storeName);
|
|
284
|
+
const request = store.delete(`${this.keyName}-${type}`);
|
|
285
|
+
request.onsuccess = () => resolve();
|
|
286
|
+
request.onerror = () => reject(request.error);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
245
289
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -6,8 +6,8 @@ var IndexedDbStamper = class {
|
|
|
6
6
|
// Optional for PKI, required for OIDC
|
|
7
7
|
constructor(config = {}) {
|
|
8
8
|
this.db = null;
|
|
9
|
-
this.
|
|
10
|
-
this.
|
|
9
|
+
this.activeKeyPairRecord = null;
|
|
10
|
+
this.pendingKeyPairRecord = null;
|
|
11
11
|
this.algorithm = Algorithm.ed25519;
|
|
12
12
|
// Use Ed25519 for maximum security and performance
|
|
13
13
|
// The type of stamper, can be changed at any time
|
|
@@ -27,29 +27,27 @@ var IndexedDbStamper = class {
|
|
|
27
27
|
*/
|
|
28
28
|
async init() {
|
|
29
29
|
await this.openDB();
|
|
30
|
-
|
|
31
|
-
if (!
|
|
32
|
-
|
|
33
|
-
} else {
|
|
34
|
-
await this.loadKeyPair();
|
|
30
|
+
this.activeKeyPairRecord = await this.loadActiveKeyPairRecord();
|
|
31
|
+
if (!this.activeKeyPairRecord) {
|
|
32
|
+
this.activeKeyPairRecord = await this.generateAndStoreNewKeyPair("active");
|
|
35
33
|
}
|
|
36
|
-
this.
|
|
37
|
-
return keyInfo;
|
|
34
|
+
this.pendingKeyPairRecord = await this.loadPendingKeyPairRecord();
|
|
35
|
+
return this.activeKeyPairRecord.keyInfo;
|
|
38
36
|
}
|
|
39
37
|
/**
|
|
40
38
|
* Get the public key information
|
|
41
39
|
*/
|
|
42
40
|
getKeyInfo() {
|
|
43
|
-
return this.keyInfo;
|
|
41
|
+
return this.activeKeyPairRecord?.keyInfo || null;
|
|
44
42
|
}
|
|
45
43
|
/**
|
|
46
44
|
* Reset the key pair by generating a new one
|
|
47
45
|
*/
|
|
48
46
|
async resetKeyPair() {
|
|
49
47
|
await this.clearStoredKeys();
|
|
50
|
-
|
|
51
|
-
this.
|
|
52
|
-
return keyInfo;
|
|
48
|
+
this.activeKeyPairRecord = await this.generateAndStoreNewKeyPair("active");
|
|
49
|
+
this.pendingKeyPairRecord = null;
|
|
50
|
+
return this.activeKeyPairRecord.keyInfo;
|
|
53
51
|
}
|
|
54
52
|
/**
|
|
55
53
|
* Create X-Phantom-Stamp header value using stored private key
|
|
@@ -58,7 +56,7 @@ var IndexedDbStamper = class {
|
|
|
58
56
|
*/
|
|
59
57
|
async stamp(params) {
|
|
60
58
|
const { data } = params;
|
|
61
|
-
if (!this.
|
|
59
|
+
if (!this.activeKeyPairRecord) {
|
|
62
60
|
throw new Error("Stamper not initialized. Call init() first.");
|
|
63
61
|
}
|
|
64
62
|
const dataBytes = new Uint8Array(data);
|
|
@@ -67,7 +65,7 @@ var IndexedDbStamper = class {
|
|
|
67
65
|
name: this.algorithm,
|
|
68
66
|
hash: "SHA-256"
|
|
69
67
|
},
|
|
70
|
-
this.
|
|
68
|
+
this.activeKeyPairRecord.keyPair.privateKey,
|
|
71
69
|
dataBytes
|
|
72
70
|
);
|
|
73
71
|
const signatureBase64url = base64urlEncode(new Uint8Array(signature));
|
|
@@ -76,14 +74,14 @@ var IndexedDbStamper = class {
|
|
|
76
74
|
const salt = params.type === "OIDC" ? params.salt : this.salt;
|
|
77
75
|
const stampData = stampType === "PKI" ? {
|
|
78
76
|
// Decode base58 public key to bytes, then encode as base64url (consistent with ApiKeyStamper)
|
|
79
|
-
publicKey: base64urlEncode(bs58.decode(this.keyInfo.publicKey)),
|
|
77
|
+
publicKey: base64urlEncode(bs58.decode(this.activeKeyPairRecord.keyInfo.publicKey)),
|
|
80
78
|
signature: signatureBase64url,
|
|
81
79
|
kind: "PKI",
|
|
82
80
|
algorithm: this.algorithm
|
|
83
81
|
} : {
|
|
84
82
|
kind: "OIDC",
|
|
85
83
|
idToken,
|
|
86
|
-
publicKey: base64urlEncode(bs58.decode(this.keyInfo.publicKey)),
|
|
84
|
+
publicKey: base64urlEncode(bs58.decode(this.activeKeyPairRecord.keyInfo.publicKey)),
|
|
87
85
|
salt,
|
|
88
86
|
algorithm: this.algorithm,
|
|
89
87
|
signature: signatureBase64url
|
|
@@ -96,8 +94,8 @@ var IndexedDbStamper = class {
|
|
|
96
94
|
*/
|
|
97
95
|
async clear() {
|
|
98
96
|
await this.clearStoredKeys();
|
|
99
|
-
this.
|
|
100
|
-
this.
|
|
97
|
+
this.activeKeyPairRecord = null;
|
|
98
|
+
this.pendingKeyPairRecord = null;
|
|
101
99
|
}
|
|
102
100
|
async clearStoredKeys() {
|
|
103
101
|
if (!this.db) {
|
|
@@ -106,8 +104,8 @@ var IndexedDbStamper = class {
|
|
|
106
104
|
return new Promise((resolve, reject) => {
|
|
107
105
|
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
108
106
|
const store = transaction.objectStore(this.storeName);
|
|
109
|
-
const
|
|
110
|
-
const
|
|
107
|
+
const deleteActiveKeyPair = store.delete(`${this.keyName}-active`);
|
|
108
|
+
const deletePendingKeyPair = store.delete(`${this.keyName}-pending`);
|
|
111
109
|
let completed = 0;
|
|
112
110
|
const total = 2;
|
|
113
111
|
const checkComplete = () => {
|
|
@@ -116,10 +114,10 @@ var IndexedDbStamper = class {
|
|
|
116
114
|
resolve();
|
|
117
115
|
}
|
|
118
116
|
};
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
deleteActiveKeyPair.onsuccess = checkComplete;
|
|
118
|
+
deletePendingKeyPair.onsuccess = checkComplete;
|
|
119
|
+
deleteActiveKeyPair.onerror = () => reject(deleteActiveKeyPair.error);
|
|
120
|
+
deletePendingKeyPair.onerror = () => reject(deletePendingKeyPair.error);
|
|
123
121
|
});
|
|
124
122
|
}
|
|
125
123
|
async openDB() {
|
|
@@ -138,8 +136,46 @@ var IndexedDbStamper = class {
|
|
|
138
136
|
};
|
|
139
137
|
});
|
|
140
138
|
}
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Generate a new keypair for rotation without making it active
|
|
141
|
+
*/
|
|
142
|
+
async rotateKeyPair() {
|
|
143
|
+
if (!this.db) {
|
|
144
|
+
await this.openDB();
|
|
145
|
+
}
|
|
146
|
+
this.pendingKeyPairRecord = await this.generateAndStoreNewKeyPair("pending");
|
|
147
|
+
return this.pendingKeyPairRecord.keyInfo;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Switch to the pending keypair, making it active and cleaning up the old one
|
|
151
|
+
*/
|
|
152
|
+
async commitRotation(authenticatorId) {
|
|
153
|
+
if (!this.pendingKeyPairRecord) {
|
|
154
|
+
throw new Error("No pending keypair to commit");
|
|
155
|
+
}
|
|
156
|
+
if (this.activeKeyPairRecord) {
|
|
157
|
+
await this.removeKeyPairRecord("active");
|
|
158
|
+
}
|
|
159
|
+
this.pendingKeyPairRecord.status = "active";
|
|
160
|
+
this.pendingKeyPairRecord.authenticatorId = authenticatorId;
|
|
161
|
+
this.pendingKeyPairRecord.keyInfo.authenticatorId = authenticatorId;
|
|
162
|
+
this.activeKeyPairRecord = this.pendingKeyPairRecord;
|
|
163
|
+
this.pendingKeyPairRecord = null;
|
|
164
|
+
await this.storeKeyPairRecord(this.activeKeyPairRecord, "active");
|
|
165
|
+
await this.removeKeyPairRecord("pending");
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Discard the pending keypair on rotation failure
|
|
169
|
+
*/
|
|
170
|
+
async rollbackRotation() {
|
|
171
|
+
if (!this.pendingKeyPairRecord) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await this.removeKeyPairRecord("pending");
|
|
175
|
+
this.pendingKeyPairRecord = null;
|
|
176
|
+
}
|
|
177
|
+
async generateAndStoreNewKeyPair(type) {
|
|
178
|
+
const keyPair = await crypto.subtle.generateKey(
|
|
143
179
|
{
|
|
144
180
|
name: "Ed25519"
|
|
145
181
|
},
|
|
@@ -147,67 +183,75 @@ var IndexedDbStamper = class {
|
|
|
147
183
|
// non-extractable - private key can never be exported
|
|
148
184
|
["sign", "verify"]
|
|
149
185
|
);
|
|
150
|
-
const rawPublicKeyBuffer = await crypto.subtle.exportKey("raw",
|
|
186
|
+
const rawPublicKeyBuffer = await crypto.subtle.exportKey("raw", keyPair.publicKey);
|
|
151
187
|
const publicKeyBase58 = bs58.encode(new Uint8Array(rawPublicKeyBuffer));
|
|
152
188
|
const keyIdBuffer = await crypto.subtle.digest("SHA-256", rawPublicKeyBuffer);
|
|
153
189
|
const keyId = base64urlEncode(new Uint8Array(keyIdBuffer)).substring(0, 16);
|
|
190
|
+
const now = Date.now();
|
|
154
191
|
const keyInfo = {
|
|
155
192
|
keyId,
|
|
156
|
-
publicKey: publicKeyBase58
|
|
193
|
+
publicKey: publicKeyBase58,
|
|
194
|
+
createdAt: now
|
|
157
195
|
};
|
|
158
|
-
|
|
159
|
-
|
|
196
|
+
const record = {
|
|
197
|
+
keyPair,
|
|
198
|
+
keyInfo,
|
|
199
|
+
createdAt: now,
|
|
200
|
+
expiresAt: 0,
|
|
201
|
+
// Not used anymore, kept for backward compatibility
|
|
202
|
+
status: type
|
|
203
|
+
};
|
|
204
|
+
await this.storeKeyPairRecord(record, type);
|
|
205
|
+
return record;
|
|
160
206
|
}
|
|
161
|
-
async
|
|
207
|
+
async storeKeyPairRecord(record, type) {
|
|
162
208
|
if (!this.db) {
|
|
163
209
|
throw new Error("Database not initialized");
|
|
164
210
|
}
|
|
165
211
|
return new Promise((resolve, reject) => {
|
|
166
212
|
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
167
213
|
const store = transaction.objectStore(this.storeName);
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const total = 2;
|
|
172
|
-
const checkComplete = () => {
|
|
173
|
-
completed++;
|
|
174
|
-
if (completed === total) {
|
|
175
|
-
resolve();
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
keyPairRequest.onsuccess = checkComplete;
|
|
179
|
-
keyInfoRequest.onsuccess = checkComplete;
|
|
180
|
-
keyPairRequest.onerror = () => reject(keyPairRequest.error);
|
|
181
|
-
keyInfoRequest.onerror = () => reject(keyInfoRequest.error);
|
|
214
|
+
const request = store.put(record, `${this.keyName}-${type}`);
|
|
215
|
+
request.onsuccess = () => resolve();
|
|
216
|
+
request.onerror = () => reject(request.error);
|
|
182
217
|
});
|
|
183
218
|
}
|
|
184
|
-
async
|
|
219
|
+
async loadActiveKeyPairRecord() {
|
|
185
220
|
if (!this.db) {
|
|
186
|
-
return;
|
|
221
|
+
return null;
|
|
187
222
|
}
|
|
188
223
|
return new Promise((resolve, reject) => {
|
|
189
224
|
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
190
225
|
const store = transaction.objectStore(this.storeName);
|
|
191
|
-
const request = store.get(`${this.keyName}-
|
|
192
|
-
request.onsuccess = () =>
|
|
193
|
-
this.cryptoKeyPair = request.result || null;
|
|
194
|
-
resolve();
|
|
195
|
-
};
|
|
226
|
+
const request = store.get(`${this.keyName}-active`);
|
|
227
|
+
request.onsuccess = () => resolve(request.result || null);
|
|
196
228
|
request.onerror = () => reject(request.error);
|
|
197
229
|
});
|
|
198
230
|
}
|
|
199
|
-
async
|
|
231
|
+
async loadPendingKeyPairRecord() {
|
|
200
232
|
if (!this.db) {
|
|
201
233
|
return null;
|
|
202
234
|
}
|
|
203
235
|
return new Promise((resolve, reject) => {
|
|
204
236
|
const transaction = this.db.transaction([this.storeName], "readonly");
|
|
205
237
|
const store = transaction.objectStore(this.storeName);
|
|
206
|
-
const request = store.get(`${this.keyName}-
|
|
238
|
+
const request = store.get(`${this.keyName}-pending`);
|
|
207
239
|
request.onsuccess = () => resolve(request.result || null);
|
|
208
240
|
request.onerror = () => reject(request.error);
|
|
209
241
|
});
|
|
210
242
|
}
|
|
243
|
+
async removeKeyPairRecord(type) {
|
|
244
|
+
if (!this.db) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
const transaction = this.db.transaction([this.storeName], "readwrite");
|
|
249
|
+
const store = transaction.objectStore(this.storeName);
|
|
250
|
+
const request = store.delete(`${this.keyName}-${type}`);
|
|
251
|
+
request.onsuccess = () => resolve();
|
|
252
|
+
request.onerror = () => reject(request.error);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
211
255
|
};
|
|
212
256
|
export {
|
|
213
257
|
IndexedDbStamper
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phantom/indexed-db-stamper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-beta.0",
|
|
4
4
|
"description": "IndexedDB stamper for Phantom Wallet SDK with non-extractable key storage",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"typescript": "^5.0.4"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@phantom/base64url": "^0.
|
|
43
|
-
"@phantom/crypto": "^0.
|
|
44
|
-
"@phantom/embedded-provider-core": "^0.
|
|
45
|
-
"@phantom/sdk-types": "^0.
|
|
42
|
+
"@phantom/base64url": "^1.0.0-beta.0",
|
|
43
|
+
"@phantom/crypto": "^1.0.0-beta.0",
|
|
44
|
+
"@phantom/embedded-provider-core": "^1.0.0-beta.0",
|
|
45
|
+
"@phantom/sdk-types": "^1.0.0-beta.0",
|
|
46
46
|
"bs58": "^6.0.0",
|
|
47
47
|
"buffer": "^6.0.3"
|
|
48
48
|
},
|