@thezelijah/majik-message 1.0.17 → 1.0.18

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.
@@ -54,6 +54,7 @@ export declare class MajikContact {
54
54
  isMajikahIdentityChecked(): boolean;
55
55
  isMajikahRegistered(): boolean;
56
56
  setMajikahStatus(status: boolean): this;
57
+ getDisplayName(): Promise<string>;
57
58
  /**
58
59
  * Support both CryptoKey and raw-key wrappers (fallbacks when WebCrypto X25519 unsupported)
59
60
  */
@@ -114,6 +114,9 @@ export class MajikContact {
114
114
  this.majikah_registered = status;
115
115
  return this;
116
116
  }
117
+ async getDisplayName() {
118
+ return this.meta.label || (await this.getPublicKeyBase64());
119
+ }
117
120
  /**
118
121
  * Support both CryptoKey and raw-key wrappers (fallbacks when WebCrypto X25519 unsupported)
119
122
  */
@@ -31,13 +31,13 @@ export declare class KeyStoreError extends Error {
31
31
  * ⚠️ Background-only. Never expose private keys to content scripts.
32
32
  */
33
33
  export declare class KeyStore {
34
- private static DB_NAME;
34
+ private static deviceID;
35
35
  private static STORE_NAME;
36
36
  private static DB_VERSION;
37
37
  private static dbPromise;
38
38
  private static unlockedIdentities;
39
39
  static onUnlockRequested?: (id: string) => string | Promise<string>;
40
- private static computeChecksum;
40
+ static init(deviceID: string): void;
41
41
  private static getDB;
42
42
  private static putSerializedIdentity;
43
43
  private static getSerializedIdentity;
@@ -28,7 +28,7 @@ export class KeyStoreError extends Error {
28
28
  * ⚠️ Background-only. Never expose private keys to content scripts.
29
29
  */
30
30
  export class KeyStore {
31
- static DB_NAME = "MajikMessageKeyStore";
31
+ static deviceID = "default";
32
32
  static STORE_NAME = "identities";
33
33
  static DB_VERSION = 1;
34
34
  static dbPromise = null;
@@ -37,22 +37,18 @@ export class KeyStore {
37
37
  // Optional callback: invoked when UI needs to request a passphrase to unlock an identity.
38
38
  // Should return the passphrase string or a Promise<string>.
39
39
  static onUnlockRequested;
40
- /* ================================
41
- * Chrome Storage Helpers
42
- * ================================ */
43
- static async computeChecksum(identities) {
44
- const json = JSON.stringify(identities);
45
- const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(json));
46
- return arrayBufferToBase64(hash);
47
- }
48
40
  /* ================================
49
41
  * IndexedDB Helpers
50
42
  * ================================ */
43
+ static init(deviceID) {
44
+ this.deviceID = deviceID;
45
+ }
51
46
  static async getDB() {
52
47
  if (this.dbPromise)
53
48
  return this.dbPromise;
49
+ const dbName = this.deviceID;
54
50
  this.dbPromise = new Promise((resolve, reject) => {
55
- const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);
51
+ const request = indexedDB.open(dbName, this.DB_VERSION);
56
52
  request.onupgradeneeded = () => {
57
53
  const db = request.result;
58
54
  if (!db.objectStoreNames.contains(this.STORE_NAME)) {
@@ -28,13 +28,14 @@ export interface EnvelopeCacheItem {
28
28
  message?: string;
29
29
  }
30
30
  export declare class EnvelopeCache {
31
+ private userProfile;
31
32
  private dbPromise;
32
33
  private dbName;
33
34
  private storeName;
34
35
  private maxEntries?;
35
36
  private memoryCache;
36
37
  private memoryCacheSize;
37
- constructor(config?: EnvelopeCacheConfig);
38
+ constructor(config?: EnvelopeCacheConfig, userProfile?: string);
38
39
  private initDB;
39
40
  private getEnvelopeId;
40
41
  set(envelope: MessageEnvelope, source?: string): Promise<void>;
@@ -15,6 +15,7 @@ export class EnvelopeCacheError extends Error {
15
15
  * EnvelopeCache
16
16
  * ------------------------------- */
17
17
  export class EnvelopeCache {
18
+ userProfile = "default";
18
19
  dbPromise;
19
20
  dbName;
20
21
  storeName;
@@ -22,12 +23,13 @@ export class EnvelopeCache {
22
23
  // In-memory cache
23
24
  memoryCache;
24
25
  memoryCacheSize;
25
- constructor(config) {
26
- this.dbName = config?.dbName || "MajikEnvelopeDB";
26
+ constructor(config, userProfile = "default") {
27
+ this.dbName = config?.dbName || `MajikEnvelopeDB_${userProfile}`;
27
28
  this.storeName = config?.storeName || "envelopes";
28
29
  this.maxEntries = config?.maxEntries;
29
30
  this.memoryCacheSize = config?.memoryCacheSize || 100;
30
31
  this.memoryCache = new Map();
32
+ this.userProfile = userProfile || "default";
31
33
  this.dbPromise = this.initDB();
32
34
  }
33
35
  /* -------------------------------
@@ -35,6 +37,7 @@ export class EnvelopeCache {
35
37
  * ------------------------------- */
36
38
  initDB() {
37
39
  return new Promise((resolve, reject) => {
40
+ console.log("DB: ", this.dbName);
38
41
  const request = indexedDB.open(this.dbName, 1);
39
42
  request.onupgradeneeded = (event) => {
40
43
  const db = event.target.result;
@@ -7,9 +7,9 @@ export interface MajikIDBSaveData {
7
7
  interface MajikAutosaveSchema {
8
8
  majikdata: MajikIDBSaveData;
9
9
  }
10
- export declare function initDB(): Promise<IDBPDatabase<MajikAutosaveSchema>>;
11
- export declare function idbSaveBlob(id: string, data: Blob): Promise<void>;
12
- export declare function idbLoadBlob(id: string): Promise<MajikIDBSaveData | undefined>;
10
+ export declare function initDB(name?: string): Promise<IDBPDatabase<MajikAutosaveSchema>>;
11
+ export declare function idbSaveBlob(id: string, data: Blob, name?: string): Promise<void>;
12
+ export declare function idbLoadBlob(id: string, name?: string): Promise<MajikIDBSaveData | undefined>;
13
13
  export declare function deleteBlob(id: string): Promise<void>;
14
14
  export declare function clearAllBlobs(): Promise<void>;
15
15
  export {};
@@ -1,9 +1,10 @@
1
1
  // lib/indexedDB.ts
2
2
  import { openDB } from "idb";
3
3
  let dbPromise;
4
- export function initDB() {
4
+ export function initDB(name = "default") {
5
5
  if (!dbPromise) {
6
- dbPromise = openDB("MajikAutosaveDB", 1, {
6
+ const dbName = `MajikAutosaveDB_${name}`;
7
+ dbPromise = openDB(dbName, 1, {
7
8
  upgrade(db) {
8
9
  if (!db.objectStoreNames.contains("majikdata")) {
9
10
  db.createObjectStore("majikdata", { keyPath: "id" });
@@ -13,13 +14,13 @@ export function initDB() {
13
14
  }
14
15
  return dbPromise;
15
16
  }
16
- export async function idbSaveBlob(id, data) {
17
- const db = await initDB();
17
+ export async function idbSaveBlob(id, data, name = "default") {
18
+ const db = await initDB(name);
18
19
  await db.put("majikdata", { id, data, savedAt: Date.now() });
19
20
  }
20
- export async function idbLoadBlob(id) {
21
+ export async function idbLoadBlob(id, name = "default") {
21
22
  try {
22
- const db = await initDB();
23
+ const db = await initDB(name);
23
24
  return await db.get("majikdata", id);
24
25
  }
25
26
  catch (err) {
@@ -27,6 +27,7 @@ export interface MajikMessageJSON {
27
27
  }
28
28
  type EventCallback = (...args: any[]) => void;
29
29
  export declare class MajikMessage {
30
+ private userProfile;
30
31
  private pinHash?;
31
32
  private id;
32
33
  private contactDirectory;
@@ -38,7 +39,8 @@ export declare class MajikMessage {
38
39
  private autosaveTimer;
39
40
  private autosaveIntervalMs;
40
41
  private autosaveDebounceMs;
41
- constructor(config: MajikMessageConfig, id?: string);
42
+ private unlocked;
43
+ constructor(config: MajikMessageConfig, id?: string, userProfile?: string);
42
44
  /**
43
45
  * Create a new account (generates identity via KeyStore) and add it as an own account.
44
46
  * Returns the created identity id and a backup blob (base64) that the user should store.
@@ -205,6 +207,7 @@ export declare class MajikMessage {
205
207
  ensureIdentityUnlocked(id: string, promptFn?: (identityId: string) => string | Promise<string>): Promise<CryptoKey | {
206
208
  raw: Uint8Array;
207
209
  }>;
210
+ isUnlocked(): boolean;
208
211
  isPassphraseValid(passphrase: string, id?: string): Promise<boolean>;
209
212
  toJSON(): Promise<MajikMessageJSON>;
210
213
  static fromJSON<T extends MajikMessage>(this: new (config: MajikMessageConfig, id?: string) => T, json: MajikMessageJSON): Promise<T>;
@@ -228,6 +231,6 @@ export declare class MajikMessage {
228
231
  /**
229
232
  * Try to load an existing state from IDB; if none exists, create a fresh instance and save it.
230
233
  */
231
- static loadOrCreate<T extends MajikMessage>(this: MajikMessageStatic<T>, config: MajikMessageConfig): Promise<T>;
234
+ static loadOrCreate<T extends MajikMessage>(this: MajikMessageStatic<T>, config: MajikMessageConfig, userProfile?: string): Promise<T>;
232
235
  }
233
236
  export {};
@@ -14,6 +14,7 @@ import { idbLoadBlob, idbSaveBlob } from "./core/utils/idb-majik-system";
14
14
  import { MajikMessageChat } from "./core/database/chat/majik-message-chat";
15
15
  import { MajikCompressor } from "./core/compressor/majik-compressor";
16
16
  export class MajikMessage {
17
+ userProfile = "default";
17
18
  // Optional PIN protection (hashed). If set, UI should prompt for PIN to unlock.
18
19
  pinHash = null;
19
20
  id;
@@ -26,11 +27,14 @@ export class MajikMessage {
26
27
  autosaveTimer = null;
27
28
  autosaveIntervalMs = 15000; // periodic backup interval
28
29
  autosaveDebounceMs = 500; // debounce for rapid changes
29
- constructor(config, id) {
30
+ unlocked = false;
31
+ constructor(config, id, userProfile = "default") {
32
+ this.userProfile = userProfile || "default";
30
33
  this.id = id || arrayToBase64(randomBytes(32));
31
34
  this.contactDirectory =
32
35
  config.contactDirectory || new MajikContactDirectory();
33
- this.envelopeCache = config.envelopeCache || new EnvelopeCache();
36
+ this.envelopeCache =
37
+ config.envelopeCache || new EnvelopeCache(undefined, userProfile);
34
38
  // Initialize scanner
35
39
  this.scanner = new ScannerEngine({
36
40
  contactDirectory: this.contactDirectory,
@@ -142,6 +146,10 @@ export class MajikMessage {
142
146
  if (!this.contactDirectory.hasContact(account.id)) {
143
147
  this.contactDirectory.addContact(account);
144
148
  }
149
+ if (!this.getActiveAccount()) {
150
+ this.setActiveAccount(account.id);
151
+ this.unlocked = true;
152
+ }
145
153
  }
146
154
  catch (e) {
147
155
  // ignore if contact can't be added
@@ -356,44 +364,51 @@ export class MajikMessage {
356
364
  * Will prompt to unlock identity if necessary.
357
365
  */
358
366
  async decryptEnvelope(envelope, bypassIdentity = false) {
359
- const fingerprint = envelope.extractFingerprint();
360
- const authorizedAccount = this.listContacts(true).find((a) => a.fingerprint === fingerprint);
361
- if (!authorizedAccount) {
362
- throw new Error("No matching own account to decrypt this envelope");
363
- }
364
- let privateKey;
365
- if (bypassIdentity) {
366
- const activeAccount = this.getActiveAccount();
367
- if (!activeAccount) {
368
- throw new Error("No active account available to bypass identity check");
369
- }
370
- privateKey = await KeyStore.getPrivateKey(activeAccount.id);
371
- }
372
- else {
373
- privateKey = await this.ensureIdentityUnlocked(authorizedAccount.id);
374
- }
375
- if (!privateKey) {
376
- throw new Error("No private key found for this fingerprint.");
377
- }
378
- let decrypted;
379
367
  if (envelope.isGroup()) {
380
- const recipientKey = envelope.getRecipientKey(fingerprint);
381
- if (!recipientKey) {
382
- throw new Error("No recipient key found for this fingerprint");
368
+ // Group message - try all own accounts
369
+ const ownAccounts = this.listOwnAccounts();
370
+ if (ownAccounts.length === 0) {
371
+ throw new Error("No own accounts available to decrypt group message");
372
+ }
373
+ for (const ownAccount of ownAccounts) {
374
+ try {
375
+ const privateKey = await this.ensureIdentityUnlocked(ownAccount.id);
376
+ const decrypted = await EncryptionEngine.decryptGroupMessage(envelope.extractEncryptedPayload(), privateKey, ownAccount.fingerprint);
377
+ // Decompress if needed
378
+ let plaintext = decrypted;
379
+ if (decrypted.startsWith("mjkcmp:")) {
380
+ plaintext = (await MajikCompressor.decompress("plaintext", decrypted));
381
+ }
382
+ await this.envelopeCache.set(envelope, typeof window !== "undefined" && window.location
383
+ ? window.location.hostname
384
+ : "extension");
385
+ return plaintext;
386
+ }
387
+ catch (err) {
388
+ // This account can't decrypt, try next
389
+ continue;
390
+ }
383
391
  }
384
- decrypted = await EncryptionEngine.decryptGroupMessage(envelope.extractEncryptedPayload(), privateKey, fingerprint);
392
+ throw new Error("None of your accounts can decrypt this group message");
385
393
  }
386
394
  else {
387
- decrypted = await EncryptionEngine.decryptSoloMessage(envelope.extractEncryptedPayload(), privateKey);
388
- }
389
- let plaintext = decrypted;
390
- if (decrypted.startsWith("mjkcmp:")) {
391
- plaintext = (await MajikCompressor.decompress("plaintext", decrypted));
395
+ // Solo message - original logic
396
+ const fingerprint = envelope.extractFingerprint();
397
+ const ownAccount = this.listOwnAccounts().find((a) => a.fingerprint === fingerprint);
398
+ if (!ownAccount) {
399
+ throw new Error("No matching account to decrypt this envelope");
400
+ }
401
+ const privateKey = await this.ensureIdentityUnlocked(ownAccount.id);
402
+ const decrypted = await EncryptionEngine.decryptSoloMessage(envelope.extractEncryptedPayload(), privateKey);
403
+ let plaintext = decrypted;
404
+ if (decrypted.startsWith("mjkcmp:")) {
405
+ plaintext = (await MajikCompressor.decompress("plaintext", decrypted));
406
+ }
407
+ await this.envelopeCache.set(envelope, typeof window !== "undefined" && window.location
408
+ ? window.location.hostname
409
+ : "extension");
410
+ return plaintext;
392
411
  }
393
- await this.envelopeCache.set(envelope, typeof window !== "undefined" && window.location
394
- ? window.location.hostname
395
- : "extension");
396
- return plaintext;
397
412
  }
398
413
  async importContactFromString(base64Str) {
399
414
  const jsonStr = base64ToUtf8(base64Str);
@@ -519,16 +534,20 @@ export class MajikMessage {
519
534
  const payloadBytes = new TextEncoder().encode(JSON.stringify(payload));
520
535
  // Envelope structure: [version byte][sender fingerprint][payload bytes]
521
536
  const versionByte = new Uint8Array([2]);
522
- // Use sender fingerprint for group envelope
523
- const activeAccount = this.getActiveAccount();
524
- if (!activeAccount)
525
- throw new Error("No active account to send from");
526
- const fingerprintBytes = new Uint8Array(base64ToArrayBuffer(activeAccount.fingerprint));
537
+ // // Use sender fingerprint for group envelope
538
+ // const activeAccount = this.getActiveAccount();
539
+ // if (!activeAccount) throw new Error("No active account to send from");
540
+ // const fingerprintBytes = new Uint8Array(
541
+ // base64ToArrayBuffer(activeAccount.fingerprint),
542
+ // );
543
+ // ✅ Use a special marker instead of a specific fingerprint
544
+ // Option 1: All zeros to indicate "multi-recipient"
545
+ const markerBytes = new Uint8Array(32).fill(0);
527
546
  // Combine all parts into a single Uint8Array
528
- const blob = new Uint8Array(versionByte.length + fingerprintBytes.length + payloadBytes.length);
547
+ const blob = new Uint8Array(versionByte.length + markerBytes.length + payloadBytes.length);
529
548
  blob.set(versionByte, 0);
530
- blob.set(fingerprintBytes, versionByte.length);
531
- blob.set(payloadBytes, versionByte.length + fingerprintBytes.length);
549
+ blob.set(markerBytes, versionByte.length);
550
+ blob.set(payloadBytes, versionByte.length + markerBytes.length);
532
551
  // Wrap as MessageEnvelope
533
552
  const envelope = new MessageEnvelope(blob.buffer);
534
553
  if (!!cache) {
@@ -755,38 +774,60 @@ export class MajikMessage {
755
774
  if (cached)
756
775
  return;
757
776
  const fingerprint = envelope.extractFingerprint();
758
- // contact is the directory entry (useful for metadata); ownAccount
759
- // must match fingerprint for this device to be able to decrypt.
760
- const contact = this.contactDirectory.getContactByFingerprint(fingerprint);
761
- const ownAccount = this.listOwnAccounts().find((a) => a.fingerprint === fingerprint);
762
- // If this envelope isn't addressed to one of our own accounts, mark as untrusted
763
- if (!ownAccount) {
764
- this.emit("untrusted", envelope);
765
- return;
766
- }
767
- try {
768
- // Ensure identity unlocked; try interactive prompt if needed
769
- const privateKey = await this.ensureIdentityUnlocked(ownAccount.id);
770
- let decrypted;
771
- if (envelope.isGroup()) {
772
- const recipientKey = envelope.getRecipientKey(fingerprint);
773
- if (!recipientKey) {
774
- throw new Error("No recipient key found for this fingerprint");
777
+ // Check if this is a group message (all zeros or special marker)
778
+ const isGroupMessage = envelope.isGroup();
779
+ if (isGroupMessage) {
780
+ // For group messages, try all own accounts until one works
781
+ const ownAccounts = this.listOwnAccounts();
782
+ if (ownAccounts.length === 0) {
783
+ this.emit("untrusted", envelope);
784
+ return;
785
+ }
786
+ let decrypted = null;
787
+ let successfulAccount = null;
788
+ for (const ownAccount of ownAccounts) {
789
+ try {
790
+ const privateKey = await this.ensureIdentityUnlocked(ownAccount.id);
791
+ // Try to decrypt with this account
792
+ decrypted = await EncryptionEngine.decryptGroupMessage(envelope.extractEncryptedPayload(), privateKey, ownAccount.fingerprint);
793
+ successfulAccount = ownAccount;
794
+ break; // Success! Stop trying other accounts
795
+ }
796
+ catch (err) {
797
+ // This account doesn't have access, try next one
798
+ continue;
775
799
  }
776
- decrypted = await EncryptionEngine.decryptGroupMessage(envelope.extractEncryptedPayload(), privateKey, fingerprint);
777
800
  }
778
- else {
779
- decrypted = await EncryptionEngine.decryptSoloMessage(envelope.extractEncryptedPayload(), privateKey);
801
+ if (!decrypted || !successfulAccount) {
802
+ this.emit("untrusted", envelope);
803
+ return;
780
804
  }
781
- // Cache envelope (record source as current hostname when available)
805
+ // Cache and emit
782
806
  await this.envelopeCache.set(envelope, typeof window !== "undefined" && window.location
783
807
  ? window.location.hostname
784
808
  : "extension");
785
809
  this.scheduleAutosave();
786
- this.emit("message", decrypted, envelope, contact);
810
+ this.emit("message", decrypted, envelope, successfulAccount);
787
811
  }
788
- catch (err) {
789
- this.emit("error", err, { envelope });
812
+ else {
813
+ // Solo message - original logic
814
+ const ownAccount = this.listOwnAccounts().find((a) => a.fingerprint === fingerprint);
815
+ if (!ownAccount) {
816
+ this.emit("untrusted", envelope);
817
+ return;
818
+ }
819
+ try {
820
+ const privateKey = await this.ensureIdentityUnlocked(ownAccount.id);
821
+ const decrypted = await EncryptionEngine.decryptSoloMessage(envelope.extractEncryptedPayload(), privateKey);
822
+ await this.envelopeCache.set(envelope, typeof window !== "undefined" && window.location
823
+ ? window.location.hostname
824
+ : "extension");
825
+ this.scheduleAutosave();
826
+ this.emit("message", decrypted, envelope, ownAccount);
827
+ }
828
+ catch (err) {
829
+ this.emit("error", err, { envelope });
830
+ }
790
831
  }
791
832
  }
792
833
  /**
@@ -817,13 +858,19 @@ export class MajikMessage {
817
858
  else if (typeof window !== "undefined" && window.prompt) {
818
859
  passphrase = window.prompt("Enter passphrase to unlock identity:", "");
819
860
  }
820
- if (!passphrase)
861
+ if (!passphrase) {
862
+ this.unlocked = false;
821
863
  throw new Error("Unlock cancelled");
864
+ }
822
865
  // Attempt to unlock
823
866
  await KeyStore.unlockIdentity(id, passphrase);
867
+ this.unlocked = true;
824
868
  return await KeyStore.getPrivateKey(id);
825
869
  }
826
870
  }
871
+ isUnlocked() {
872
+ return this.unlocked;
873
+ }
827
874
  async isPassphraseValid(passphrase, id) {
828
875
  const target = id ? this.getOwnAccountById(id) : this.getActiveAccount();
829
876
  if (!target)
@@ -1023,7 +1070,7 @@ export class MajikMessage {
1023
1070
  try {
1024
1071
  const jsonDocument = await this.toJSON();
1025
1072
  const autosaveBlob = autoSaveMajikFileData(jsonDocument);
1026
- await idbSaveBlob("majik-message-state", autosaveBlob);
1073
+ await idbSaveBlob("majik-message-state", autosaveBlob, this.userProfile);
1027
1074
  }
1028
1075
  catch (err) {
1029
1076
  console.error("Failed to save MajikMessage state:", err);
@@ -1032,7 +1079,7 @@ export class MajikMessage {
1032
1079
  /** Load state from IndexedDB and apply to this instance. */
1033
1080
  async loadState() {
1034
1081
  try {
1035
- const autosaveData = await idbLoadBlob("majik-message-state");
1082
+ const autosaveData = await idbLoadBlob("majik-message-state", this.userProfile);
1036
1083
  if (!autosaveData?.data)
1037
1084
  return;
1038
1085
  const blobFile = autosaveData.data;
@@ -1053,9 +1100,9 @@ export class MajikMessage {
1053
1100
  /**
1054
1101
  * Try to load an existing state from IDB; if none exists, create a fresh instance and save it.
1055
1102
  */
1056
- static async loadOrCreate(config) {
1103
+ static async loadOrCreate(config, userProfile = "default") {
1057
1104
  try {
1058
- const saved = await idbLoadBlob("majik-message-state");
1105
+ const saved = await idbLoadBlob("majik-message-state", userProfile);
1059
1106
  if (saved?.data) {
1060
1107
  const loaded = await loadSavedMajikFileData(saved.data);
1061
1108
  const parsedJSON = loaded.j;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@thezelijah/majik-message",
3
3
  "type": "module",
4
4
  "description": "Encrypt and decrypt messages on any website. Secure chats with keypairs and seed-based accounts. Open source.",
5
- "version": "1.0.17",
5
+ "version": "1.0.18",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",