@majikah/majik-universal-id-client 0.0.1
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/core/contacts/majik-contact-directory.d.ts +37 -0
- package/dist/core/contacts/majik-contact-directory.js +191 -0
- package/dist/core/contacts/majik-contact.d.ts +89 -0
- package/dist/core/contacts/majik-contact.js +212 -0
- package/dist/core/crypto/constants.d.ts +56 -0
- package/dist/core/crypto/constants.js +51 -0
- package/dist/core/crypto/keystore.d.ts +228 -0
- package/dist/core/crypto/keystore.js +575 -0
- package/dist/core/identity.d.ts +63 -0
- package/dist/core/identity.js +177 -0
- package/dist/core/types.d.ts +86 -0
- package/dist/core/types.js +7 -0
- package/dist/core/utils/APITranscoder.d.ts +114 -0
- package/dist/core/utils/APITranscoder.js +305 -0
- package/dist/core/utils/idb-majik-system.d.ts +15 -0
- package/dist/core/utils/idb-majik-system.js +44 -0
- package/dist/core/utils/majik-file-utils.d.ts +16 -0
- package/dist/core/utils/majik-file-utils.js +153 -0
- package/dist/core/utils/utilities.d.ts +18 -0
- package/dist/core/utils/utilities.js +80 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/majik-universal-id-client.d.ts +757 -0
- package/dist/majik-universal-id-client.js +1618 -0
- package/package.json +55 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MajikKeyStore.ts
|
|
3
|
+
*
|
|
4
|
+
* IDB persistence + in-memory cache layer for MajikKey accounts.
|
|
5
|
+
* Replaces MajikKeyStore as the account storage backend for MajikMessage.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
import { MajikKey } from "@majikah/majik-key";
|
|
9
|
+
import { KDF_VERSION } from "./constants";
|
|
10
|
+
// ─── IDB Config ───────────────────────────────────────────────────────────────
|
|
11
|
+
const STORE_NAME = "majik-keys";
|
|
12
|
+
const LEGACY_STORE_NAME = "identities"; // MajikKeyStore's old store — for migration reads
|
|
13
|
+
const DB_VERSION = 2; // bump from MajikKeyStore's v1 to trigger onupgradeneeded
|
|
14
|
+
export class MajikKeyStoreError extends Error {
|
|
15
|
+
cause;
|
|
16
|
+
constructor(message, cause) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "MajikKeyStoreError";
|
|
19
|
+
this.cause = cause;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// ─── MajikKeyStore ────────────────────────────────────────────────────────────
|
|
23
|
+
export class MajikKeyStore {
|
|
24
|
+
static _deviceID = "default";
|
|
25
|
+
static _dbPromise = null;
|
|
26
|
+
/**
|
|
27
|
+
* In-memory cache of all loaded MajikKey instances (locked or unlocked).
|
|
28
|
+
* Keyed by account ID. Unlocked state lives inside each MajikKey instance.
|
|
29
|
+
*/
|
|
30
|
+
static _keys = new Map();
|
|
31
|
+
/**
|
|
32
|
+
* Optional callback invoked when UI needs to prompt for a passphrase.
|
|
33
|
+
* Should return the passphrase string or Promise<string>.
|
|
34
|
+
*/
|
|
35
|
+
static onUnlockRequested;
|
|
36
|
+
// ── Init ───────────────────────────────────────────────────────────────────
|
|
37
|
+
/**
|
|
38
|
+
* Initialize the store with a device/user ID.
|
|
39
|
+
* Must be called before any other method.
|
|
40
|
+
*/
|
|
41
|
+
static init(deviceID) {
|
|
42
|
+
this._deviceID = deviceID;
|
|
43
|
+
this._dbPromise = null; // reset DB connection on re-init
|
|
44
|
+
this._keys.clear();
|
|
45
|
+
}
|
|
46
|
+
// ── IDB ───────────────────────────────────────────────────────────────────
|
|
47
|
+
static async _getDB() {
|
|
48
|
+
if (this._dbPromise)
|
|
49
|
+
return this._dbPromise;
|
|
50
|
+
this._dbPromise = new Promise((resolve, reject) => {
|
|
51
|
+
const request = indexedDB.open(this._deviceID, DB_VERSION);
|
|
52
|
+
request.onupgradeneeded = (event) => {
|
|
53
|
+
const db = request.result;
|
|
54
|
+
const oldVersion = event.oldVersion;
|
|
55
|
+
// Create new store for MajikKeyJSON (v2+)
|
|
56
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
57
|
+
db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
58
|
+
}
|
|
59
|
+
// Keep legacy "identities" store intact for migration reads
|
|
60
|
+
// (do not delete it — old data may need to be migrated on demand)
|
|
61
|
+
if (oldVersion < 1 &&
|
|
62
|
+
!db.objectStoreNames.contains(LEGACY_STORE_NAME)) {
|
|
63
|
+
db.createObjectStore(LEGACY_STORE_NAME, { keyPath: "id" });
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
request.onsuccess = () => resolve(request.result);
|
|
67
|
+
request.onerror = () => reject(new MajikKeyStoreError("IDB open failed", request.error));
|
|
68
|
+
});
|
|
69
|
+
return this._dbPromise;
|
|
70
|
+
}
|
|
71
|
+
static async _put(json) {
|
|
72
|
+
const db = await this._getDB();
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
75
|
+
const store = tx.objectStore(STORE_NAME);
|
|
76
|
+
const req = store.put(json);
|
|
77
|
+
req.onsuccess = () => resolve();
|
|
78
|
+
req.onerror = () => reject(new MajikKeyStoreError("Failed to store MajikKey", req.error));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
static async _get(id) {
|
|
82
|
+
const db = await this._getDB();
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
85
|
+
const store = tx.objectStore(STORE_NAME);
|
|
86
|
+
const req = store.get(id);
|
|
87
|
+
req.onsuccess = () => resolve(req.result || null);
|
|
88
|
+
req.onerror = () => reject(new MajikKeyStoreError("Failed to read MajikKey", req.error));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
static async _getAll() {
|
|
92
|
+
const db = await this._getDB();
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
95
|
+
const store = tx.objectStore(STORE_NAME);
|
|
96
|
+
const req = store.getAll();
|
|
97
|
+
req.onsuccess = () => resolve(req.result || []);
|
|
98
|
+
req.onerror = () => reject(new MajikKeyStoreError("Failed to list MajikKeys", req.error));
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
static async _delete(id) {
|
|
102
|
+
const db = await this._getDB();
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
105
|
+
const store = tx.objectStore(STORE_NAME);
|
|
106
|
+
const req = store.delete(id);
|
|
107
|
+
req.onsuccess = () => resolve();
|
|
108
|
+
req.onerror = () => reject(new MajikKeyStoreError("Failed to delete MajikKey", req.error));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
// ── Legacy IDB (migration reads only) ─────────────────────────────────────
|
|
112
|
+
static async _getLegacy(id) {
|
|
113
|
+
try {
|
|
114
|
+
const db = await this._getDB();
|
|
115
|
+
if (!db.objectStoreNames.contains(LEGACY_STORE_NAME))
|
|
116
|
+
return null;
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
const tx = db.transaction(LEGACY_STORE_NAME, "readonly");
|
|
119
|
+
const store = tx.objectStore(LEGACY_STORE_NAME);
|
|
120
|
+
const req = store.get(id);
|
|
121
|
+
req.onsuccess = () => resolve(req.result || null);
|
|
122
|
+
req.onerror = () => resolve(null);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
static async _getAllLegacy() {
|
|
130
|
+
try {
|
|
131
|
+
const db = await this._getDB();
|
|
132
|
+
if (!db.objectStoreNames.contains(LEGACY_STORE_NAME))
|
|
133
|
+
return [];
|
|
134
|
+
return new Promise((resolve) => {
|
|
135
|
+
const tx = db.transaction(LEGACY_STORE_NAME, "readonly");
|
|
136
|
+
const store = tx.objectStore(LEGACY_STORE_NAME);
|
|
137
|
+
const req = store.getAll();
|
|
138
|
+
req.onsuccess = () => resolve(req.result || []);
|
|
139
|
+
req.onerror = () => resolve([]);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ── Core API ──────────────────────────────────────────────────────────────
|
|
147
|
+
/**
|
|
148
|
+
* Store a MajikKey in IDB and cache it in memory.
|
|
149
|
+
* The key must be unlocked (toKeyIdentity() is called to warm the memory cache).
|
|
150
|
+
* The full MajikKeyJSON is persisted — including ML-KEM keys and kdfVersion.
|
|
151
|
+
*/
|
|
152
|
+
static async save(key) {
|
|
153
|
+
await this._put(key.toJSON());
|
|
154
|
+
this._keys.set(key.id, key);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Load a MajikKey by ID. Checks memory cache first, then IDB, then legacy IDB.
|
|
158
|
+
* Returns null if not found anywhere.
|
|
159
|
+
*
|
|
160
|
+
* Loaded keys are LOCKED. Call unlock(id, passphrase) to unlock.
|
|
161
|
+
*/
|
|
162
|
+
static async load(id) {
|
|
163
|
+
// 1. Memory cache
|
|
164
|
+
const cached = this._keys.get(id);
|
|
165
|
+
if (cached)
|
|
166
|
+
return cached;
|
|
167
|
+
// 2. New IDB store (MajikKeyJSON)
|
|
168
|
+
const json = await this._get(id);
|
|
169
|
+
if (json) {
|
|
170
|
+
const key = MajikKey.fromJSON(json);
|
|
171
|
+
this._keys.set(id, key);
|
|
172
|
+
return key;
|
|
173
|
+
}
|
|
174
|
+
// 3. Legacy IDB store (MajikKeyStore format) — migrate on read
|
|
175
|
+
const legacy = await this._getLegacy(id);
|
|
176
|
+
if (legacy) {
|
|
177
|
+
const key = MajikKeyStore.fromLegacySerializedIdentity(legacy);
|
|
178
|
+
// Don't save yet — wait until the user unlocks and we can verify
|
|
179
|
+
this._keys.set(id, key);
|
|
180
|
+
return key;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Load all MajikKeys from IDB (new store + legacy store merged).
|
|
186
|
+
* Legacy accounts are included but NOT migrated until explicitly unlocked.
|
|
187
|
+
*/
|
|
188
|
+
static async loadAll() {
|
|
189
|
+
const results = new Map();
|
|
190
|
+
// New store first
|
|
191
|
+
const allNew = await this._getAll();
|
|
192
|
+
for (const json of allNew) {
|
|
193
|
+
const key = MajikKey.fromJSON(json);
|
|
194
|
+
results.set(key.id, key);
|
|
195
|
+
this._keys.set(key.id, key);
|
|
196
|
+
}
|
|
197
|
+
// Legacy store — add any not already in new store
|
|
198
|
+
const allLegacy = await this._getAllLegacy();
|
|
199
|
+
for (const legacy of allLegacy) {
|
|
200
|
+
if (!results.has(legacy.id)) {
|
|
201
|
+
const key = MajikKeyStore.fromLegacySerializedIdentity(legacy);
|
|
202
|
+
results.set(key.id, key);
|
|
203
|
+
this._keys.set(key.id, key);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return [...results.values()];
|
|
207
|
+
}
|
|
208
|
+
static async getAccount(id) {
|
|
209
|
+
const key = await this.load(id);
|
|
210
|
+
if (!key)
|
|
211
|
+
throw new MajikKeyStoreError(`Account not found: ${id}`);
|
|
212
|
+
return key;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Unlock a stored MajikKey with the given passphrase.
|
|
216
|
+
* Automatically dispatches to the correct KDF (PBKDF2 for old accounts, Argon2id for new).
|
|
217
|
+
* Updates the in-memory cache with the unlocked instance.
|
|
218
|
+
*/
|
|
219
|
+
static async unlock(id, passphrase) {
|
|
220
|
+
const key = await this.load(id);
|
|
221
|
+
if (!key)
|
|
222
|
+
throw new MajikKeyStoreError(`Account not found: ${id}`);
|
|
223
|
+
if (key.isUnlocked)
|
|
224
|
+
return key;
|
|
225
|
+
await key.unlock(passphrase);
|
|
226
|
+
this._keys.set(id, key);
|
|
227
|
+
return key;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Lock a MajikKey — clears private keys from memory.
|
|
231
|
+
*/
|
|
232
|
+
static lock(id) {
|
|
233
|
+
const key = this._keys.get(id);
|
|
234
|
+
if (key)
|
|
235
|
+
key.lock();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Lock all loaded accounts.
|
|
239
|
+
*/
|
|
240
|
+
static lockAll() {
|
|
241
|
+
for (const key of this._keys.values()) {
|
|
242
|
+
key.lock();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Get the private key of an unlocked account.
|
|
247
|
+
* Throws if not found or not unlocked — caller must call unlock() first.
|
|
248
|
+
*/
|
|
249
|
+
static getPrivateKey(idOrFingerprint) {
|
|
250
|
+
// Check by ID
|
|
251
|
+
const byId = this._keys.get(idOrFingerprint);
|
|
252
|
+
if (byId?.isUnlocked)
|
|
253
|
+
return byId.getPrivateKey();
|
|
254
|
+
// Check by fingerprint
|
|
255
|
+
for (const key of this._keys.values()) {
|
|
256
|
+
if (key.fingerprint === idOrFingerprint && key.isUnlocked) {
|
|
257
|
+
return key.getPrivateKey();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw new MajikKeyStoreError(`Account "${idOrFingerprint}" must be unlocked first via unlock()`);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Get the ML-KEM secret key of an unlocked account.
|
|
264
|
+
* Returns undefined if the account has no ML-KEM keys (pre-migration).
|
|
265
|
+
*/
|
|
266
|
+
static getMlKemSecretKey(idOrFingerprint) {
|
|
267
|
+
const key = this._findKey(idOrFingerprint);
|
|
268
|
+
if (!key?.isUnlocked)
|
|
269
|
+
return undefined;
|
|
270
|
+
try {
|
|
271
|
+
return key.getMlKemSecretKey();
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return undefined; // account exists but has no ML-KEM keys yet
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Get a loaded MajikKey by ID or fingerprint.
|
|
279
|
+
*/
|
|
280
|
+
static get(idOrFingerprint) {
|
|
281
|
+
return this._findKey(idOrFingerprint);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* List all currently loaded MajikKey instances (locked + unlocked).
|
|
285
|
+
*/
|
|
286
|
+
static list() {
|
|
287
|
+
return [...this._keys.values()];
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Check whether an account exists by ID or fingerprint.
|
|
291
|
+
* Checks memory cache first, then IDB.
|
|
292
|
+
*/
|
|
293
|
+
static async has(idOrFingerprint) {
|
|
294
|
+
// Memory cache
|
|
295
|
+
if (this._findKey(idOrFingerprint))
|
|
296
|
+
return true;
|
|
297
|
+
// IDB (new store)
|
|
298
|
+
const allNew = await this._getAll();
|
|
299
|
+
if (allNew.some((j) => j.id === idOrFingerprint || j.fingerprint === idOrFingerprint)) {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
// Legacy IDB
|
|
303
|
+
const allLegacy = await this._getAllLegacy();
|
|
304
|
+
return allLegacy.some((l) => l.id === idOrFingerprint || l.fingerprint === idOrFingerprint);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Validate whether a passphrase can decrypt the stored account.
|
|
308
|
+
* Does NOT unlock or mutate any state.
|
|
309
|
+
*/
|
|
310
|
+
static async isPassphraseValid(id, passphrase) {
|
|
311
|
+
const key = await this.load(id);
|
|
312
|
+
if (!key)
|
|
313
|
+
return false;
|
|
314
|
+
return key.verify(passphrase);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Delete an account from IDB and memory cache.
|
|
318
|
+
*/
|
|
319
|
+
static async delete(id) {
|
|
320
|
+
await this._delete(id);
|
|
321
|
+
this._keys.delete(id);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Ensure an account is unlocked, prompting for passphrase if needed.
|
|
325
|
+
* Drop-in replacement for MajikMessage.ensureIdentityUnlocked().
|
|
326
|
+
*
|
|
327
|
+
* @param id - Account ID
|
|
328
|
+
* @param promptFn - Optional passphrase prompt function
|
|
329
|
+
* @returns The unlocked account's X25519 private key
|
|
330
|
+
*/
|
|
331
|
+
static async ensureUnlocked(id, promptFn) {
|
|
332
|
+
// Try from memory first (already unlocked)
|
|
333
|
+
try {
|
|
334
|
+
return this.getPrivateKey(id);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
/* not unlocked */
|
|
338
|
+
}
|
|
339
|
+
// Ask for passphrase
|
|
340
|
+
let passphrase = null;
|
|
341
|
+
if (promptFn) {
|
|
342
|
+
const res = promptFn(id);
|
|
343
|
+
passphrase = typeof res === "string" ? res : await res;
|
|
344
|
+
}
|
|
345
|
+
else if (this.onUnlockRequested) {
|
|
346
|
+
const res = this.onUnlockRequested(id);
|
|
347
|
+
passphrase = typeof res === "string" ? res : await res;
|
|
348
|
+
}
|
|
349
|
+
else if (typeof window !== "undefined" && window.prompt) {
|
|
350
|
+
passphrase = window.prompt("Enter passphrase to unlock account:", "");
|
|
351
|
+
}
|
|
352
|
+
if (!passphrase)
|
|
353
|
+
throw new MajikKeyStoreError("Unlock cancelled");
|
|
354
|
+
await this.unlock(id, passphrase);
|
|
355
|
+
return this.getPrivateKey(id);
|
|
356
|
+
}
|
|
357
|
+
// ── Migration: fromLegacySerializedIdentity ────────────────────────────────
|
|
358
|
+
/**
|
|
359
|
+
* Reconstruct a locked MajikKey from a MajikKeyStore SerializedIdentity.
|
|
360
|
+
*
|
|
361
|
+
* The legacy format has:
|
|
362
|
+
* - id, publicKey (base64), fingerprint
|
|
363
|
+
* - encryptedPrivateKey (base64, PBKDF2-encrypted)
|
|
364
|
+
* - salt (base64, 16 bytes)
|
|
365
|
+
* - NO: kdfVersion (implied PBKDF2), NO: ML-KEM keys, NO: label, NO: backup
|
|
366
|
+
*
|
|
367
|
+
* The resulting MajikKey will:
|
|
368
|
+
* - Have kdfVersion: PBKDF2 (so unlock() uses the correct KDF)
|
|
369
|
+
* - Have hasMlKem: false (until importFromMnemonicBackup() is called)
|
|
370
|
+
* - Be locked (private key not in memory)
|
|
371
|
+
*
|
|
372
|
+
* After unlock(), MajikMessage can call key.migrate(passphrase) to upgrade
|
|
373
|
+
* the KDF to Argon2id. For full ML-KEM upgrade, importFromMnemonicBackup()
|
|
374
|
+
* is required (mnemonic needed).
|
|
375
|
+
*
|
|
376
|
+
* This method is also the answer to your question: it's how MajikKey
|
|
377
|
+
* accepts a SerializedIdentity from the existing MajikKeyStore IDB.
|
|
378
|
+
*/
|
|
379
|
+
static fromLegacySerializedIdentity(si) {
|
|
380
|
+
if (!si.id || !si.publicKey || !si.fingerprint) {
|
|
381
|
+
throw new MajikKeyStoreError("Invalid legacy SerializedIdentity: missing required fields");
|
|
382
|
+
}
|
|
383
|
+
// // SerializedIdentity may or may not have encryptedPrivateKey
|
|
384
|
+
// // (some records were stored without it — public-key-only contacts)
|
|
385
|
+
// const encryptedPrivateKey = si.encryptedPrivateKey
|
|
386
|
+
// ? base64ToArrayBuffer(si.encryptedPrivateKey)
|
|
387
|
+
// : new ArrayBuffer(0);
|
|
388
|
+
// Reconstruct as a minimal MajikKeyJSON (v1 PBKDF2, no ML-KEM)
|
|
389
|
+
const json = {
|
|
390
|
+
id: si.id,
|
|
391
|
+
label: "",
|
|
392
|
+
publicKey: si.publicKey,
|
|
393
|
+
fingerprint: si.fingerprint,
|
|
394
|
+
encryptedPrivateKey: si.encryptedPrivateKey || "",
|
|
395
|
+
salt: si.salt || "",
|
|
396
|
+
backup: "_LEGACY", // no backup available from legacy format
|
|
397
|
+
timestamp: new Date().toISOString(),
|
|
398
|
+
kdfVersion: KDF_VERSION.PBKDF2, // legacy MajikKeyStore always used PBKDF2
|
|
399
|
+
// mlKemPublicKey: absent — hasMlKem will be false
|
|
400
|
+
// encryptedMlKemSecretKey: absent
|
|
401
|
+
};
|
|
402
|
+
return MajikKey.fromJSON(json);
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Migrate all legacy MajikKeyStore accounts to the new MajikKeyJSON format.
|
|
406
|
+
*
|
|
407
|
+
* Reads all records from the old "identities" IDB store, reconstructs
|
|
408
|
+
* them as MajikKey instances, and writes them to the new "majik-keys" store.
|
|
409
|
+
* Does NOT upgrade KDF or add ML-KEM keys — that requires the passphrase/mnemonic.
|
|
410
|
+
*
|
|
411
|
+
* Call this once on app startup after MajikKeyStore → MajikKeyStore transition.
|
|
412
|
+
* Safe to call multiple times (already-migrated accounts are skipped).
|
|
413
|
+
*/
|
|
414
|
+
static async migrateAllLegacy() {
|
|
415
|
+
const legacyAll = await this._getAllLegacy();
|
|
416
|
+
let migrated = 0;
|
|
417
|
+
let skipped = 0;
|
|
418
|
+
for (const legacy of legacyAll) {
|
|
419
|
+
// Skip if already in new store
|
|
420
|
+
const existing = await this._get(legacy.id);
|
|
421
|
+
if (existing) {
|
|
422
|
+
skipped++;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
const key = MajikKeyStore.fromLegacySerializedIdentity(legacy);
|
|
427
|
+
await this._put(key.toJSON());
|
|
428
|
+
this._keys.set(key.id, key);
|
|
429
|
+
migrated++;
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
console.warn(`Failed to migrate legacy account ${legacy.id}:`, err);
|
|
433
|
+
skipped++;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
return { migrated, skipped };
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Migrate a legacy MajikKeyStore account to the new MajikKeyJSON format.
|
|
440
|
+
*
|
|
441
|
+
* Reconstructs them as MajikKey instances, and writes them to the new "majik-keys" store.
|
|
442
|
+
* Does NOT upgrade KDF or add ML-KEM keys — that requires the passphrase/mnemonic.
|
|
443
|
+
*
|
|
444
|
+
* Call this once on app startup after MajikKeyStore → MajikKeyStore transition.
|
|
445
|
+
* Safe to call multiple times (already-migrated accounts are skipped).
|
|
446
|
+
*/
|
|
447
|
+
static async migrate(identity) {
|
|
448
|
+
try {
|
|
449
|
+
const key = MajikKeyStore.fromLegacySerializedIdentity(identity);
|
|
450
|
+
await this._put(key.toJSON());
|
|
451
|
+
this._keys.set(key.id, key);
|
|
452
|
+
return {
|
|
453
|
+
success: true,
|
|
454
|
+
message: `Successfully migrated ${identity.id}`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
console.warn(`Failed to migrate legacy account ${identity.id}:`, err);
|
|
459
|
+
return {
|
|
460
|
+
success: false,
|
|
461
|
+
message: `Failed to migrate legacy account ${identity.id}: ${err}`,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// ── Drop-in replacements for MajikKeyStore methods used by MajikMessage ─────────
|
|
466
|
+
/**
|
|
467
|
+
* Drop-in for MajikKeyStore.addMajikKey().
|
|
468
|
+
* Saves the FULL MajikKeyJSON to IDB (not just 5 fields).
|
|
469
|
+
*/
|
|
470
|
+
static async addMajikKey(key) {
|
|
471
|
+
return this.save(key);
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Drop-in for MajikKeyStore.unlockIdentity().
|
|
475
|
+
*/
|
|
476
|
+
static async unlockIdentity(id, passphrase) {
|
|
477
|
+
return this.unlock(id, passphrase);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Drop-in for MajikKeyStore.lockIdentity().
|
|
481
|
+
*/
|
|
482
|
+
static lockIdentity(id) {
|
|
483
|
+
return this.lock(id);
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Drop-in for MajikKeyStore.hasIdentity().
|
|
487
|
+
*/
|
|
488
|
+
static async hasIdentity(fingerprint) {
|
|
489
|
+
return this.has(fingerprint);
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Drop-in for MajikKeyStore.isPassphraseValid().
|
|
493
|
+
*/
|
|
494
|
+
static async isPassphraseValidFor(id, passphrase) {
|
|
495
|
+
return this.isPassphraseValid(id, passphrase);
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Drop-in for MajikKeyStore.updatePassphrase().
|
|
499
|
+
* Correctly upgrades KDF to Argon2id on re-encryption (MajikKeyStore never did this).
|
|
500
|
+
*/
|
|
501
|
+
static async updatePassphrase(id, currentPassphrase, newPassphrase) {
|
|
502
|
+
const key = await this.load(id);
|
|
503
|
+
if (!key)
|
|
504
|
+
throw new MajikKeyStoreError(`Account not found: ${id}`);
|
|
505
|
+
await key.updatePassphrase(currentPassphrase, newPassphrase);
|
|
506
|
+
await this._put(key.toJSON()); // persist updated encrypted state
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Drop-in for MajikKeyStore.listStoredIdentities().
|
|
510
|
+
* Returns all stored MajikKey instances (loaded from IDB if needed).
|
|
511
|
+
*/
|
|
512
|
+
static async listStoredKeys() {
|
|
513
|
+
return this.loadAll();
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Drop-in for MajikKeyStore.deleteIdentity().
|
|
517
|
+
*/
|
|
518
|
+
static async deleteIdentity(id) {
|
|
519
|
+
return this.delete(id);
|
|
520
|
+
}
|
|
521
|
+
static async deleteAll() {
|
|
522
|
+
const db = await this._getDB();
|
|
523
|
+
const clearStore = (storeName) => new Promise((resolve, reject) => {
|
|
524
|
+
if (!db.objectStoreNames.contains(storeName))
|
|
525
|
+
return resolve();
|
|
526
|
+
const tx = db.transaction(storeName, "readwrite");
|
|
527
|
+
const req = tx.objectStore(storeName).clear();
|
|
528
|
+
req.onsuccess = () => resolve();
|
|
529
|
+
req.onerror = () => reject(new MajikKeyStoreError(`Failed to clear store: ${storeName}`, req.error));
|
|
530
|
+
});
|
|
531
|
+
await clearStore(STORE_NAME);
|
|
532
|
+
await clearStore(LEGACY_STORE_NAME);
|
|
533
|
+
this._keys.clear();
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Drop-in for MajikKeyStore.generateMnemonic().
|
|
537
|
+
*/
|
|
538
|
+
static generateMnemonic(strength = 128) {
|
|
539
|
+
return MajikKey.generateMnemonic(strength);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Drop-in for MajikKeyStore.exportIdentityMnemonicBackup().
|
|
543
|
+
* The account must be unlocked.
|
|
544
|
+
*/
|
|
545
|
+
static async exportMnemonicBackup(id, mnemonic) {
|
|
546
|
+
const key = this._keys.get(id);
|
|
547
|
+
if (!key)
|
|
548
|
+
throw new MajikKeyStoreError(`Account not found: ${id}`);
|
|
549
|
+
if (key.isLocked)
|
|
550
|
+
throw new MajikKeyStoreError("Account must be unlocked to export backup");
|
|
551
|
+
return key.exportMnemonicBackup(mnemonic);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Drop-in for MajikKeyStore.importIdentityFromMnemonicBackup().
|
|
555
|
+
* Fully upgrades the account: Argon2id KDF + ML-KEM keys in one step.
|
|
556
|
+
*/
|
|
557
|
+
static async importFromMnemonicBackup(backupBase64, mnemonic, passphrase, label) {
|
|
558
|
+
const key = await MajikKey.importFromMnemonicBackup(backupBase64, mnemonic, passphrase, label);
|
|
559
|
+
await this.save(key);
|
|
560
|
+
return key;
|
|
561
|
+
}
|
|
562
|
+
// ── Private helpers ────────────────────────────────────────────────────────
|
|
563
|
+
static _findKey(idOrFingerprint) {
|
|
564
|
+
// By ID
|
|
565
|
+
const byId = this._keys.get(idOrFingerprint);
|
|
566
|
+
if (byId)
|
|
567
|
+
return byId;
|
|
568
|
+
// By fingerprint
|
|
569
|
+
for (const key of this._keys.values()) {
|
|
570
|
+
if (key.fingerprint === idOrFingerprint)
|
|
571
|
+
return key;
|
|
572
|
+
}
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { MajikUser } from "@thezelijah/majik-user";
|
|
2
|
+
import { SerializedMajikContact } from "./contacts/majik-contact";
|
|
3
|
+
export interface MajikMessageIdentityJSON {
|
|
4
|
+
id: string;
|
|
5
|
+
user_id: string;
|
|
6
|
+
public_key: string;
|
|
7
|
+
ml_key: string;
|
|
8
|
+
phash: string;
|
|
9
|
+
label: string;
|
|
10
|
+
timestamp: string;
|
|
11
|
+
restricted: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* MajikMessageIdentity
|
|
15
|
+
* Immutable identity container with integrity verification
|
|
16
|
+
*/
|
|
17
|
+
export declare class MajikMessageIdentity {
|
|
18
|
+
private readonly _id;
|
|
19
|
+
private readonly _userId;
|
|
20
|
+
private readonly _publicKey;
|
|
21
|
+
private readonly _mlKey;
|
|
22
|
+
private readonly _phash;
|
|
23
|
+
private _label;
|
|
24
|
+
private readonly _timestamp;
|
|
25
|
+
private readonly _restricted;
|
|
26
|
+
/**
|
|
27
|
+
* Constructor is private to enforce controlled creation
|
|
28
|
+
*/
|
|
29
|
+
private constructor();
|
|
30
|
+
/**
|
|
31
|
+
* Create a new immutable identity from MajikUser
|
|
32
|
+
*/
|
|
33
|
+
static create(user: MajikUser, account: SerializedMajikContact, options?: {
|
|
34
|
+
label?: string;
|
|
35
|
+
restricted?: boolean;
|
|
36
|
+
}): MajikMessageIdentity;
|
|
37
|
+
get id(): string;
|
|
38
|
+
get userID(): string;
|
|
39
|
+
get publicKey(): string;
|
|
40
|
+
get phash(): string;
|
|
41
|
+
get label(): string;
|
|
42
|
+
get timestamp(): string;
|
|
43
|
+
get restricted(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Only mutable field
|
|
46
|
+
*/
|
|
47
|
+
set label(label: string);
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if identity is restricted
|
|
50
|
+
*/
|
|
51
|
+
isRestricted(): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Verify identity integrity
|
|
54
|
+
* Detects tampering of id/public_key
|
|
55
|
+
*/
|
|
56
|
+
validateIntegrity(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Explicit verification helper
|
|
59
|
+
*/
|
|
60
|
+
matches(userId: string, publicKey: string): boolean;
|
|
61
|
+
toJSON(): MajikMessageIdentityJSON;
|
|
62
|
+
static fromJSON(json: string | MajikMessageIdentityJSON): MajikMessageIdentity;
|
|
63
|
+
}
|