@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,37 @@
|
|
|
1
|
+
import { MAJIK_API_RESPONSE } from "../types";
|
|
2
|
+
import { MajikContact, MajikContactData, SerializedMajikContact } from "./majik-contact";
|
|
3
|
+
export interface MajikContactDirectoryData {
|
|
4
|
+
contacts: SerializedMajikContact[];
|
|
5
|
+
}
|
|
6
|
+
export declare class MajikContactDirectoryError extends Error {
|
|
7
|
+
cause?: unknown;
|
|
8
|
+
constructor(message: string, cause?: unknown);
|
|
9
|
+
}
|
|
10
|
+
export declare class MajikContactDirectory {
|
|
11
|
+
private contacts;
|
|
12
|
+
private fingerprintMap;
|
|
13
|
+
constructor(initialContacts?: MajikContact[]);
|
|
14
|
+
addContact(contact: MajikContact): this;
|
|
15
|
+
addContacts(contacts: MajikContact[]): this;
|
|
16
|
+
removeContact(id: string): MAJIK_API_RESPONSE;
|
|
17
|
+
updateContactMeta(id: string, meta: Partial<MajikContactData["meta"]>): MajikContact;
|
|
18
|
+
getContact(id: string): MajikContact | undefined;
|
|
19
|
+
getContactByFingerprint(fingerprint: string): MajikContact | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Get contact by public key (base64)
|
|
22
|
+
* Uses MajikContact.getPublicKeyBase64() for canonical comparison
|
|
23
|
+
*/
|
|
24
|
+
getContactByPublicKeyBase64(publicKeyBase64: string): Promise<MajikContact | undefined>;
|
|
25
|
+
hasFingerprint(fingerprint: string): boolean;
|
|
26
|
+
listContacts(sortedByLabel?: boolean, majikahOnly?: boolean): MajikContact[];
|
|
27
|
+
blockContact(id: string): MajikContact;
|
|
28
|
+
unblockContact(id: string): MajikContact;
|
|
29
|
+
hasContact(id: string): boolean;
|
|
30
|
+
clear(): this;
|
|
31
|
+
setMajikahStatus(id: string, status: boolean): MajikContact;
|
|
32
|
+
isMajikahIdentityChecked(id: string): boolean;
|
|
33
|
+
isMajikahRegistered(id: string): boolean;
|
|
34
|
+
toJSON(): Promise<MajikContactDirectoryData>;
|
|
35
|
+
fromJSON(data: MajikContactDirectoryData): Promise<this>;
|
|
36
|
+
private assertId;
|
|
37
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { KEY_ALGO } from "../crypto/constants";
|
|
2
|
+
import { base64ToArrayBuffer } from "../utils/utilities";
|
|
3
|
+
import { MajikContact, } from "./majik-contact";
|
|
4
|
+
/* -------------------------------
|
|
5
|
+
* Errors
|
|
6
|
+
* ------------------------------- */
|
|
7
|
+
export class MajikContactDirectoryError extends Error {
|
|
8
|
+
cause;
|
|
9
|
+
constructor(message, cause) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "MajikContactDirectoryError";
|
|
12
|
+
this.cause = cause;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/* -------------------------------
|
|
16
|
+
* MajikContactDirectory Class
|
|
17
|
+
* ------------------------------- */
|
|
18
|
+
export class MajikContactDirectory {
|
|
19
|
+
contacts = new Map();
|
|
20
|
+
fingerprintMap = new Map(); // fingerprint → contact id
|
|
21
|
+
constructor(initialContacts) {
|
|
22
|
+
if (initialContacts?.length) {
|
|
23
|
+
initialContacts.forEach((c) => this.addContact(c));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/* ================================
|
|
27
|
+
* Contact Management
|
|
28
|
+
* ================================ */
|
|
29
|
+
addContact(contact) {
|
|
30
|
+
if (!(contact instanceof MajikContact)) {
|
|
31
|
+
throw new MajikContactDirectoryError("Invalid contact instance");
|
|
32
|
+
}
|
|
33
|
+
if (this.contacts.has(contact.id)) {
|
|
34
|
+
throw new MajikContactDirectoryError(`Contact with id "${contact.id}" already exists`);
|
|
35
|
+
}
|
|
36
|
+
this.contacts.set(contact.id, contact);
|
|
37
|
+
this.fingerprintMap.set(contact.fingerprint, contact.id);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
addContacts(contacts) {
|
|
41
|
+
contacts.forEach((c) => this.addContact(c));
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
removeContact(id) {
|
|
45
|
+
this.assertId(id);
|
|
46
|
+
const contact = this.contacts.get(id);
|
|
47
|
+
if (contact) {
|
|
48
|
+
this.fingerprintMap.delete(contact.fingerprint);
|
|
49
|
+
this.contacts.delete(id);
|
|
50
|
+
return {
|
|
51
|
+
message: "Contact removed successfully",
|
|
52
|
+
success: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
return {
|
|
57
|
+
message: "Contact not found",
|
|
58
|
+
success: false,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
updateContactMeta(id, meta) {
|
|
63
|
+
const contact = this.getContact(id);
|
|
64
|
+
if (!contact)
|
|
65
|
+
throw new MajikContactDirectoryError("Contact not found");
|
|
66
|
+
if (meta) {
|
|
67
|
+
meta.label && contact.updateLabel(meta.label);
|
|
68
|
+
meta.notes && contact.updateNotes(meta.notes);
|
|
69
|
+
meta.blocked !== undefined && contact.setBlocked(meta.blocked);
|
|
70
|
+
}
|
|
71
|
+
return contact;
|
|
72
|
+
}
|
|
73
|
+
getContact(id) {
|
|
74
|
+
this.assertId(id);
|
|
75
|
+
return this.contacts.get(id);
|
|
76
|
+
}
|
|
77
|
+
getContactByFingerprint(fingerprint) {
|
|
78
|
+
if (!fingerprint) {
|
|
79
|
+
throw new MajikContactDirectoryError("Fingerprint must be a non-empty string");
|
|
80
|
+
}
|
|
81
|
+
const contactId = this.fingerprintMap.get(fingerprint);
|
|
82
|
+
return contactId ? this.contacts.get(contactId) : undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get contact by public key (base64)
|
|
86
|
+
* Uses MajikContact.getPublicKeyBase64() for canonical comparison
|
|
87
|
+
*/
|
|
88
|
+
async getContactByPublicKeyBase64(publicKeyBase64) {
|
|
89
|
+
if (!publicKeyBase64 || typeof publicKeyBase64 !== "string") {
|
|
90
|
+
throw new MajikContactDirectoryError("Public key must be a non-empty base64 string");
|
|
91
|
+
}
|
|
92
|
+
for (const contact of this.contacts.values()) {
|
|
93
|
+
const contactKey = await contact.getPublicKeyBase64();
|
|
94
|
+
if (contactKey === publicKeyBase64) {
|
|
95
|
+
return contact;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
hasFingerprint(fingerprint) {
|
|
101
|
+
return this.fingerprintMap.has(fingerprint);
|
|
102
|
+
}
|
|
103
|
+
listContacts(sortedByLabel = false, majikahOnly = false) {
|
|
104
|
+
let contacts = [...this.contacts.values()];
|
|
105
|
+
if (majikahOnly) {
|
|
106
|
+
contacts = contacts.filter((c) => c.isMajikahRegistered());
|
|
107
|
+
}
|
|
108
|
+
if (sortedByLabel) {
|
|
109
|
+
contacts.sort((a, b) => (a.meta.label || "").localeCompare(b.meta.label || ""));
|
|
110
|
+
}
|
|
111
|
+
return contacts;
|
|
112
|
+
}
|
|
113
|
+
blockContact(id) {
|
|
114
|
+
const contact = this.getContact(id);
|
|
115
|
+
if (!contact)
|
|
116
|
+
throw new MajikContactDirectoryError(`Contact with id "${id}" not found for block`);
|
|
117
|
+
return contact.block();
|
|
118
|
+
}
|
|
119
|
+
unblockContact(id) {
|
|
120
|
+
const contact = this.getContact(id);
|
|
121
|
+
if (!contact)
|
|
122
|
+
throw new MajikContactDirectoryError(`Contact with id "${id}" not found for unblock`);
|
|
123
|
+
return contact.unblock();
|
|
124
|
+
}
|
|
125
|
+
hasContact(id) {
|
|
126
|
+
return this.contacts.has(id);
|
|
127
|
+
}
|
|
128
|
+
clear() {
|
|
129
|
+
this.contacts.clear();
|
|
130
|
+
this.fingerprintMap.clear();
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
setMajikahStatus(id, status) {
|
|
134
|
+
const contact = this.getContact(id);
|
|
135
|
+
if (!contact)
|
|
136
|
+
throw new MajikContactDirectoryError("Contact not found");
|
|
137
|
+
contact.setMajikahStatus(status);
|
|
138
|
+
return contact;
|
|
139
|
+
}
|
|
140
|
+
isMajikahIdentityChecked(id) {
|
|
141
|
+
const contact = this.getContact(id);
|
|
142
|
+
if (!contact)
|
|
143
|
+
throw new MajikContactDirectoryError("Contact not found");
|
|
144
|
+
return contact.isMajikahIdentityChecked();
|
|
145
|
+
}
|
|
146
|
+
isMajikahRegistered(id) {
|
|
147
|
+
const contact = this.getContact(id);
|
|
148
|
+
if (!contact)
|
|
149
|
+
throw new MajikContactDirectoryError("Contact not found");
|
|
150
|
+
return contact.isMajikahRegistered();
|
|
151
|
+
}
|
|
152
|
+
/* ================================
|
|
153
|
+
* Serialization / Persistence
|
|
154
|
+
* ================================ */
|
|
155
|
+
async toJSON() {
|
|
156
|
+
const contactsData = [];
|
|
157
|
+
for (const contact of this.contacts.values()) {
|
|
158
|
+
contactsData.push(await contact.toJSON());
|
|
159
|
+
}
|
|
160
|
+
return { contacts: contactsData };
|
|
161
|
+
}
|
|
162
|
+
async fromJSON(data) {
|
|
163
|
+
if (!data?.contacts) {
|
|
164
|
+
throw new MajikContactDirectoryError("Invalid serialized data");
|
|
165
|
+
}
|
|
166
|
+
this.clear();
|
|
167
|
+
for (const item of data.contacts) {
|
|
168
|
+
const raw = base64ToArrayBuffer(item.publicKeyBase64);
|
|
169
|
+
let publicKey;
|
|
170
|
+
try {
|
|
171
|
+
publicKey = await crypto.subtle.importKey("raw", raw, KEY_ALGO, true, []);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
// Fallback: create a raw-key wrapper when the browser does not support the namedCurve
|
|
175
|
+
publicKey = { raw: new Uint8Array(raw) };
|
|
176
|
+
}
|
|
177
|
+
const contact = MajikContact.create(item.id, publicKey, item.mlKey, item.fingerprint, item.meta);
|
|
178
|
+
this.contacts.set(contact.id, contact);
|
|
179
|
+
this.fingerprintMap.set(contact.fingerprint, contact.id);
|
|
180
|
+
}
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
/* ================================
|
|
184
|
+
* Validation Helpers
|
|
185
|
+
* ================================ */
|
|
186
|
+
assertId(id) {
|
|
187
|
+
if (!id || typeof id !== "string") {
|
|
188
|
+
throw new MajikContactDirectoryError("Contact ID must be a non-empty string");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ISODateString } from "../../core/types";
|
|
2
|
+
import { MajikMessageIdentityJSON } from "../identity";
|
|
3
|
+
export type SerializedMajikContact = {
|
|
4
|
+
id: string;
|
|
5
|
+
fingerprint: string;
|
|
6
|
+
meta?: MajikContactMeta;
|
|
7
|
+
publicKeyBase64: string;
|
|
8
|
+
mlKey: string;
|
|
9
|
+
majikah_registered?: boolean;
|
|
10
|
+
edPublicKeyBase64?: string;
|
|
11
|
+
mlDsaPublicKeyBase64?: string;
|
|
12
|
+
};
|
|
13
|
+
export interface MajikContactMeta {
|
|
14
|
+
label?: string;
|
|
15
|
+
notes?: string;
|
|
16
|
+
blocked?: boolean;
|
|
17
|
+
createdAt?: ISODateString;
|
|
18
|
+
updatedAt?: ISODateString;
|
|
19
|
+
}
|
|
20
|
+
export interface MajikContactData {
|
|
21
|
+
id: string;
|
|
22
|
+
publicKey: CryptoKey | {
|
|
23
|
+
raw: Uint8Array;
|
|
24
|
+
};
|
|
25
|
+
fingerprint: string;
|
|
26
|
+
mlKey: string;
|
|
27
|
+
meta?: MajikContactMeta;
|
|
28
|
+
majikah_registered?: boolean;
|
|
29
|
+
edPublicKeyBase64?: string;
|
|
30
|
+
mlDsaPublicKeyBase64?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface MajikContactCard {
|
|
33
|
+
id: string;
|
|
34
|
+
publicKey: string;
|
|
35
|
+
fingerprint: string;
|
|
36
|
+
label: string;
|
|
37
|
+
mlKey: string;
|
|
38
|
+
edPublicKeyBase64?: string;
|
|
39
|
+
mlDsaPublicKeyBase64?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare class MajikContactError extends Error {
|
|
42
|
+
cause?: unknown;
|
|
43
|
+
constructor(message: string, cause?: unknown);
|
|
44
|
+
}
|
|
45
|
+
export declare class MajikContact {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly publicKey: CryptoKey | {
|
|
48
|
+
raw: Uint8Array;
|
|
49
|
+
};
|
|
50
|
+
readonly fingerprint: string;
|
|
51
|
+
readonly mlKey: string;
|
|
52
|
+
readonly edPublicKeyBase64: string;
|
|
53
|
+
readonly mlDsaPublicKeyBase64: string;
|
|
54
|
+
meta: MajikContactMeta;
|
|
55
|
+
private majikah_registered?;
|
|
56
|
+
constructor(data: MajikContactData);
|
|
57
|
+
static create(id: string, publicKey: CryptoKey | {
|
|
58
|
+
raw: Uint8Array;
|
|
59
|
+
}, mlKey: string, fingerprint: string, meta?: Partial<MajikContactMeta>): MajikContact;
|
|
60
|
+
private assertId;
|
|
61
|
+
private assertMLKey;
|
|
62
|
+
private assertPublicKey;
|
|
63
|
+
private assertFingerprint;
|
|
64
|
+
private updateTimestamp;
|
|
65
|
+
updateLabel(label: string): this;
|
|
66
|
+
updateNotes(notes: string): this;
|
|
67
|
+
isBlocked(): boolean;
|
|
68
|
+
setBlocked(blocked: boolean): this;
|
|
69
|
+
block(): this;
|
|
70
|
+
unblock(): this;
|
|
71
|
+
isMajikahIdentityChecked(): boolean;
|
|
72
|
+
isMajikahRegistered(): boolean;
|
|
73
|
+
setMajikahStatus(status: boolean): this;
|
|
74
|
+
getDisplayName(): Promise<string>;
|
|
75
|
+
/**
|
|
76
|
+
* Support both CryptoKey and raw-key wrappers (fallbacks when WebCrypto X25519 unsupported)
|
|
77
|
+
*/
|
|
78
|
+
getPublicKeyBase64(): Promise<string>;
|
|
79
|
+
toJSON(): Promise<SerializedMajikContact>;
|
|
80
|
+
/**
|
|
81
|
+
* Reconstruct a MajikContact from its serialized form
|
|
82
|
+
*/
|
|
83
|
+
static fromJSON(serialized: SerializedMajikContact): MajikContact;
|
|
84
|
+
/**
|
|
85
|
+
* Create a new MajikContact from a MajikMessageIdentityJSON
|
|
86
|
+
*/
|
|
87
|
+
static fromIdentityJSON(identityJSON: MajikMessageIdentityJSON): Promise<MajikContact>;
|
|
88
|
+
static isBlocked(contact: MajikContact): boolean;
|
|
89
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { arrayBufferToBase64, base64ToArrayBuffer } from "../utils/utilities";
|
|
2
|
+
/* -------------------------------
|
|
3
|
+
* Errors
|
|
4
|
+
* ------------------------------- */
|
|
5
|
+
export class MajikContactError extends Error {
|
|
6
|
+
cause;
|
|
7
|
+
constructor(message, cause) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "MajikContactError";
|
|
10
|
+
this.cause = cause;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/* -------------------------------
|
|
14
|
+
* MajikContact Class
|
|
15
|
+
* ------------------------------- */
|
|
16
|
+
export class MajikContact {
|
|
17
|
+
id;
|
|
18
|
+
publicKey;
|
|
19
|
+
fingerprint;
|
|
20
|
+
mlKey;
|
|
21
|
+
edPublicKeyBase64;
|
|
22
|
+
mlDsaPublicKeyBase64;
|
|
23
|
+
meta;
|
|
24
|
+
majikah_registered;
|
|
25
|
+
constructor(data) {
|
|
26
|
+
this.assertId(data.id);
|
|
27
|
+
this.assertPublicKey(data.publicKey);
|
|
28
|
+
this.assertMLKey(data.mlKey);
|
|
29
|
+
this.assertFingerprint(data.fingerprint);
|
|
30
|
+
this.id = data.id;
|
|
31
|
+
this.publicKey = data.publicKey;
|
|
32
|
+
this.fingerprint = data.fingerprint;
|
|
33
|
+
this.mlKey = data.mlKey;
|
|
34
|
+
this.edPublicKeyBase64 = data.edPublicKeyBase64 || "";
|
|
35
|
+
this.mlDsaPublicKeyBase64 = data.mlDsaPublicKeyBase64 || "";
|
|
36
|
+
this.meta = {
|
|
37
|
+
label: data.meta?.label || "",
|
|
38
|
+
notes: data.meta?.notes || "",
|
|
39
|
+
blocked: data.meta?.blocked || false,
|
|
40
|
+
createdAt: data.meta?.createdAt || new Date().toISOString(),
|
|
41
|
+
updatedAt: data.meta?.updatedAt || new Date().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
static create(id, publicKey, mlKey, fingerprint, meta) {
|
|
45
|
+
return new MajikContact({
|
|
46
|
+
id,
|
|
47
|
+
publicKey,
|
|
48
|
+
fingerprint,
|
|
49
|
+
meta,
|
|
50
|
+
mlKey,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
assertId(id) {
|
|
54
|
+
if (!id || typeof id !== "string") {
|
|
55
|
+
throw new MajikContactError("Contact ID must be a non-empty string");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
assertMLKey(key) {
|
|
59
|
+
if (!key || typeof key !== "string") {
|
|
60
|
+
throw new MajikContactError("ML Key must be a non-empty string");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
assertPublicKey(key) {
|
|
64
|
+
// Accept either a WebCrypto CryptoKey (with .type === 'public')
|
|
65
|
+
// or a raw-key wrapper object that contains a Uint8Array `raw` field.
|
|
66
|
+
if (!key)
|
|
67
|
+
throw new MajikContactError("Invalid public key");
|
|
68
|
+
const anyKey = key;
|
|
69
|
+
if (anyKey && typeof anyKey === "object") {
|
|
70
|
+
if (anyKey.type === "public")
|
|
71
|
+
return;
|
|
72
|
+
if (anyKey.raw instanceof Uint8Array)
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
throw new MajikContactError("Invalid public key");
|
|
76
|
+
}
|
|
77
|
+
assertFingerprint(fingerprint) {
|
|
78
|
+
if (!fingerprint || typeof fingerprint !== "string") {
|
|
79
|
+
throw new MajikContactError("Fingerprint must be a non-empty string");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
updateTimestamp() {
|
|
83
|
+
this.meta.updatedAt = new Date().toISOString();
|
|
84
|
+
}
|
|
85
|
+
updateLabel(label) {
|
|
86
|
+
if (typeof label !== "string")
|
|
87
|
+
throw new MajikContactError("Label must be a string");
|
|
88
|
+
this.meta.label = label;
|
|
89
|
+
this.updateTimestamp();
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
updateNotes(notes) {
|
|
93
|
+
if (typeof notes !== "string")
|
|
94
|
+
throw new MajikContactError("Notes must be a string");
|
|
95
|
+
this.meta.notes = notes;
|
|
96
|
+
this.updateTimestamp();
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
isBlocked() {
|
|
100
|
+
return this.meta.blocked || false;
|
|
101
|
+
}
|
|
102
|
+
setBlocked(blocked) {
|
|
103
|
+
if (typeof blocked !== "boolean")
|
|
104
|
+
throw new MajikContactError("Blocked must be boolean");
|
|
105
|
+
this.meta.blocked = blocked;
|
|
106
|
+
this.updateTimestamp();
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
// Idempotent block/unblock for safe scanning
|
|
110
|
+
block() {
|
|
111
|
+
if (!this.isBlocked())
|
|
112
|
+
this.setBlocked(true);
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
unblock() {
|
|
116
|
+
if (this.isBlocked())
|
|
117
|
+
this.setBlocked(false);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
isMajikahIdentityChecked() {
|
|
121
|
+
return this.majikah_registered !== undefined;
|
|
122
|
+
}
|
|
123
|
+
isMajikahRegistered() {
|
|
124
|
+
return this.majikah_registered || false;
|
|
125
|
+
}
|
|
126
|
+
setMajikahStatus(status) {
|
|
127
|
+
this.majikah_registered = status;
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
async getDisplayName() {
|
|
131
|
+
return this.meta.label || (await this.getPublicKeyBase64());
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Support both CryptoKey and raw-key wrappers (fallbacks when WebCrypto X25519 unsupported)
|
|
135
|
+
*/
|
|
136
|
+
async getPublicKeyBase64() {
|
|
137
|
+
try {
|
|
138
|
+
// If it's a CryptoKey, export with SubtleCrypto
|
|
139
|
+
const raw = await crypto.subtle.exportKey("raw", this.publicKey);
|
|
140
|
+
return arrayBufferToBase64(raw);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
// Fallback: publicKey may be a wrapper with `raw` Uint8Array
|
|
144
|
+
const maybe = this.publicKey;
|
|
145
|
+
if (maybe && maybe.raw instanceof Uint8Array) {
|
|
146
|
+
return arrayBufferToBase64(maybe.raw.buffer);
|
|
147
|
+
}
|
|
148
|
+
throw e;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async toJSON() {
|
|
152
|
+
return {
|
|
153
|
+
id: this.id,
|
|
154
|
+
fingerprint: this.fingerprint,
|
|
155
|
+
meta: { ...this.meta },
|
|
156
|
+
publicKeyBase64: await this.getPublicKeyBase64(),
|
|
157
|
+
majikah_registered: this.majikah_registered,
|
|
158
|
+
mlKey: this.mlKey,
|
|
159
|
+
edPublicKeyBase64: this.edPublicKeyBase64,
|
|
160
|
+
mlDsaPublicKeyBase64: this.mlDsaPublicKeyBase64,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Reconstruct a MajikContact from its serialized form
|
|
165
|
+
*/
|
|
166
|
+
static fromJSON(serialized) {
|
|
167
|
+
try {
|
|
168
|
+
const publicKeyRaw = new Uint8Array(base64ToArrayBuffer(serialized.publicKeyBase64));
|
|
169
|
+
return new MajikContact({
|
|
170
|
+
id: serialized.id,
|
|
171
|
+
fingerprint: serialized.fingerprint,
|
|
172
|
+
meta: serialized.meta,
|
|
173
|
+
publicKey: { raw: publicKeyRaw },
|
|
174
|
+
majikah_registered: serialized.majikah_registered,
|
|
175
|
+
mlKey: serialized.mlKey,
|
|
176
|
+
edPublicKeyBase64: serialized.edPublicKeyBase64,
|
|
177
|
+
mlDsaPublicKeyBase64: serialized.mlDsaPublicKeyBase64,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
throw new MajikContactError("Failed to deserialize MajikContact", err);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Create a new MajikContact from a MajikMessageIdentityJSON
|
|
186
|
+
*/
|
|
187
|
+
static async fromIdentityJSON(identityJSON) {
|
|
188
|
+
try {
|
|
189
|
+
const publicKeyRaw = new Uint8Array(base64ToArrayBuffer(identityJSON.public_key));
|
|
190
|
+
const contactData = {
|
|
191
|
+
id: identityJSON.id,
|
|
192
|
+
publicKey: { raw: publicKeyRaw },
|
|
193
|
+
fingerprint: identityJSON.id,
|
|
194
|
+
meta: {
|
|
195
|
+
label: identityJSON.label,
|
|
196
|
+
createdAt: identityJSON.timestamp,
|
|
197
|
+
updatedAt: identityJSON.timestamp,
|
|
198
|
+
blocked: identityJSON.restricted,
|
|
199
|
+
},
|
|
200
|
+
majikah_registered: true,
|
|
201
|
+
mlKey: identityJSON.ml_key,
|
|
202
|
+
};
|
|
203
|
+
return new MajikContact(contactData);
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
throw new MajikContactError("Failed to create MajikContact from MajikMessageIdentityJSON", err);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
static isBlocked(contact) {
|
|
210
|
+
return !!contact.meta.blocked;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export declare const KEY_ALGO: {
|
|
2
|
+
readonly name: "ECDH";
|
|
3
|
+
readonly namedCurve: "X25519";
|
|
4
|
+
};
|
|
5
|
+
export declare const MAJIK_SALT = "MajikMessageSalt";
|
|
6
|
+
export declare const MAJIK_MNEMONIC_SALT = "MajikMessageMnemonicSalt";
|
|
7
|
+
/**
|
|
8
|
+
* KDF version identifiers.
|
|
9
|
+
* Stored alongside every encrypted private key blob so the correct
|
|
10
|
+
* derivation function is always used on decryption.
|
|
11
|
+
*/
|
|
12
|
+
export declare const KDF_VERSION: {
|
|
13
|
+
readonly PBKDF2: 1;
|
|
14
|
+
readonly ARGON2ID: 2;
|
|
15
|
+
};
|
|
16
|
+
export type KDF_VERSION = (typeof KDF_VERSION)[keyof typeof KDF_VERSION];
|
|
17
|
+
/**
|
|
18
|
+
* Argon2id parameters.
|
|
19
|
+
*
|
|
20
|
+
* PASSPHRASE (protecting the private key at rest):
|
|
21
|
+
* m=131072 (128 MB) — double OWASP "high security" tier (64 MB)
|
|
22
|
+
* t=4 — 4 passes
|
|
23
|
+
* p=4 — 4 parallel lanes
|
|
24
|
+
*
|
|
25
|
+
* Benchmark targets (approximate):
|
|
26
|
+
* Modern laptop (2020+): ~600–900ms ✓
|
|
27
|
+
* Mid-range desktop (2018): ~800–1200ms ✓
|
|
28
|
+
* Low-end / older machine: ~1500–2500ms — acceptable for a one-time unlock
|
|
29
|
+
* RTX 4090 brute-force attack: ~1–3 guesses/sec vs ~400,000/sec for PBKDF2
|
|
30
|
+
* Improvement over current PBKDF2: ~100,000–400,000×
|
|
31
|
+
*
|
|
32
|
+
* MNEMONIC BACKUP (protecting exported backup files):
|
|
33
|
+
* m=65536 (64 MB) — lower because the mnemonic itself is 128-bit entropy;
|
|
34
|
+
* the KDF is a domain separator, not a weak-password defense.
|
|
35
|
+
* t=3
|
|
36
|
+
* p=2
|
|
37
|
+
*
|
|
38
|
+
* If benchmarks on your lowest-spec target device exceed 3s for the passphrase
|
|
39
|
+
* parameters, reduce m to 65536 (64 MB) and t to 3. That is still ~50,000×
|
|
40
|
+
* harder than the current PBKDF2 setup.
|
|
41
|
+
*/
|
|
42
|
+
export declare const ARGON2_PARAMS: {
|
|
43
|
+
readonly PASSPHRASE: {
|
|
44
|
+
readonly m: 131072;
|
|
45
|
+
readonly t: 4;
|
|
46
|
+
readonly p: 4;
|
|
47
|
+
readonly dkLen: 32;
|
|
48
|
+
};
|
|
49
|
+
readonly MNEMONIC: {
|
|
50
|
+
readonly m: 65536;
|
|
51
|
+
readonly t: 3;
|
|
52
|
+
readonly p: 2;
|
|
53
|
+
readonly dkLen: 32;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
export type ARGON2_PARAMS = (typeof ARGON2_PARAMS)[keyof typeof ARGON2_PARAMS];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const KEY_ALGO = { name: "ECDH", namedCurve: "X25519" };
|
|
2
|
+
export const MAJIK_SALT = "MajikMessageSalt";
|
|
3
|
+
export const MAJIK_MNEMONIC_SALT = "MajikMessageMnemonicSalt";
|
|
4
|
+
/**
|
|
5
|
+
* KDF version identifiers.
|
|
6
|
+
* Stored alongside every encrypted private key blob so the correct
|
|
7
|
+
* derivation function is always used on decryption.
|
|
8
|
+
*/
|
|
9
|
+
export const KDF_VERSION = {
|
|
10
|
+
PBKDF2: 1, // legacy — read-only support for existing accounts
|
|
11
|
+
ARGON2ID: 2, // current — all new accounts and re-encryptions
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Argon2id parameters.
|
|
15
|
+
*
|
|
16
|
+
* PASSPHRASE (protecting the private key at rest):
|
|
17
|
+
* m=131072 (128 MB) — double OWASP "high security" tier (64 MB)
|
|
18
|
+
* t=4 — 4 passes
|
|
19
|
+
* p=4 — 4 parallel lanes
|
|
20
|
+
*
|
|
21
|
+
* Benchmark targets (approximate):
|
|
22
|
+
* Modern laptop (2020+): ~600–900ms ✓
|
|
23
|
+
* Mid-range desktop (2018): ~800–1200ms ✓
|
|
24
|
+
* Low-end / older machine: ~1500–2500ms — acceptable for a one-time unlock
|
|
25
|
+
* RTX 4090 brute-force attack: ~1–3 guesses/sec vs ~400,000/sec for PBKDF2
|
|
26
|
+
* Improvement over current PBKDF2: ~100,000–400,000×
|
|
27
|
+
*
|
|
28
|
+
* MNEMONIC BACKUP (protecting exported backup files):
|
|
29
|
+
* m=65536 (64 MB) — lower because the mnemonic itself is 128-bit entropy;
|
|
30
|
+
* the KDF is a domain separator, not a weak-password defense.
|
|
31
|
+
* t=3
|
|
32
|
+
* p=2
|
|
33
|
+
*
|
|
34
|
+
* If benchmarks on your lowest-spec target device exceed 3s for the passphrase
|
|
35
|
+
* parameters, reduce m to 65536 (64 MB) and t to 3. That is still ~50,000×
|
|
36
|
+
* harder than the current PBKDF2 setup.
|
|
37
|
+
*/
|
|
38
|
+
export const ARGON2_PARAMS = {
|
|
39
|
+
PASSPHRASE: {
|
|
40
|
+
m: 131072, // memory in KB (128 MB)
|
|
41
|
+
t: 4, // time cost (passes)
|
|
42
|
+
p: 4, // parallelism (lanes)
|
|
43
|
+
dkLen: 32, // output length in bytes (256-bit AES key)
|
|
44
|
+
},
|
|
45
|
+
MNEMONIC: {
|
|
46
|
+
m: 65536, // 64 MB
|
|
47
|
+
t: 3,
|
|
48
|
+
p: 2,
|
|
49
|
+
dkLen: 32,
|
|
50
|
+
},
|
|
51
|
+
};
|