@majikah/majik-key 0.1.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.
@@ -0,0 +1,6 @@
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";
@@ -0,0 +1,3 @@
1
+ export const KEY_ALGO = { name: "ECDH", namedCurve: "X25519" };
2
+ export const MAJIK_SALT = "MajikMessageSalt";
3
+ export const MAJIK_MNEMONIC_SALT = "MajikMessageMnemonicSalt";
@@ -0,0 +1,21 @@
1
+ export declare const IV_LENGTH = 12;
2
+ export declare function generateRandomBytes(len: number): Uint8Array;
3
+ export declare function generateEd25519Keypair(): {
4
+ edPublic: Uint8Array;
5
+ edSecret: Uint8Array;
6
+ xPublic: Uint8Array | null;
7
+ xSecret: Uint8Array | null;
8
+ };
9
+ export declare function deriveEd25519FromSeed(seed32: Uint8Array): {
10
+ edPublic: Uint8Array;
11
+ edSecret: Uint8Array;
12
+ xPublic: Uint8Array | null;
13
+ xSecret: Uint8Array | null;
14
+ };
15
+ export declare function fingerprintFromPublicRaw(rawPublic: Uint8Array): string;
16
+ export declare function aesGcmEncrypt(keyBytes: Uint8Array, iv: Uint8Array, plaintext: Uint8Array): Uint8Array;
17
+ export declare function aesGcmDecrypt(keyBytes: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array | null;
18
+ export declare function deriveKeyFromPassphrase(passphrase: string, salt: Uint8Array, iterations?: number, keyLen?: number): Uint8Array;
19
+ export declare function deriveKeyFromMnemonic(mnemonic: string, salt: Uint8Array, iterations?: number, keyLen?: number): Uint8Array;
20
+ export declare function x25519SharedSecret(privRaw: Uint8Array, pubRaw: Uint8Array): Uint8Array;
21
+ export declare function sha256(input: string): string;
@@ -0,0 +1,74 @@
1
+ import * as ed25519 from "@stablelib/ed25519";
2
+ import ed2curve from "ed2curve";
3
+ import { AES } from "@stablelib/aes";
4
+ import { GCM } from "@stablelib/gcm";
5
+ import { deriveKey } from "@stablelib/pbkdf2";
6
+ import { hash, SHA256 } from "@stablelib/sha256";
7
+ import * as x25519 from "@stablelib/x25519";
8
+ import { arrayToBase64 } from "../utils";
9
+ export const IV_LENGTH = 12;
10
+ export function generateRandomBytes(len) {
11
+ const b = new Uint8Array(len);
12
+ crypto.getRandomValues(b);
13
+ return b;
14
+ }
15
+ export function generateEd25519Keypair() {
16
+ const ed = ed25519.generateKeyPair();
17
+ const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
18
+ const skCurve = ed2curve.convertSecretKey(ed.secretKey);
19
+ return {
20
+ edPublic: ed.publicKey,
21
+ edSecret: ed.secretKey,
22
+ xPublic: pkCurve ? new Uint8Array(pkCurve) : null,
23
+ xSecret: skCurve ? new Uint8Array(skCurve) : null,
24
+ };
25
+ }
26
+ export function deriveEd25519FromSeed(seed32) {
27
+ const ed = ed25519.generateKeyPairFromSeed(seed32);
28
+ const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
29
+ const skCurve = ed2curve.convertSecretKey(ed.secretKey);
30
+ return {
31
+ edPublic: ed.publicKey,
32
+ edSecret: ed.secretKey,
33
+ xPublic: pkCurve ? new Uint8Array(pkCurve) : null,
34
+ xSecret: skCurve ? new Uint8Array(skCurve) : null,
35
+ };
36
+ }
37
+ export function fingerprintFromPublicRaw(rawPublic) {
38
+ const digest = hash(rawPublic);
39
+ return arrayToBase64(digest);
40
+ }
41
+ export function aesGcmEncrypt(keyBytes, iv, plaintext) {
42
+ const aes = new AES(keyBytes);
43
+ const gcm = new GCM(aes);
44
+ return gcm.seal(iv, plaintext);
45
+ }
46
+ export function aesGcmDecrypt(keyBytes, iv, ciphertext) {
47
+ const aes = new AES(keyBytes);
48
+ const gcm = new GCM(aes);
49
+ return gcm.open(iv, ciphertext);
50
+ }
51
+ export function deriveKeyFromPassphrase(passphrase, salt, iterations = 250000, keyLen = 32) {
52
+ const pw = new TextEncoder().encode(passphrase);
53
+ return deriveKey(SHA256, pw, salt, iterations, keyLen);
54
+ }
55
+ export function deriveKeyFromMnemonic(mnemonic, salt, iterations = 200000, keyLen = 32) {
56
+ const m = new TextEncoder().encode(mnemonic);
57
+ return deriveKey(SHA256, m, salt, iterations, keyLen);
58
+ }
59
+ export function x25519SharedSecret(privRaw, pubRaw) {
60
+ // Use @stablelib/x25519 for scalar multiplication / shared secret
61
+ const priv = new Uint8Array(privRaw);
62
+ const pub = new Uint8Array(pubRaw);
63
+ if (x25519.scalarMult) {
64
+ return x25519.scalarMult(priv, pub);
65
+ }
66
+ if (x25519.sharedKey) {
67
+ return x25519.sharedKey(priv, pub);
68
+ }
69
+ throw new Error("@stablelib/x25519: compatible API not found");
70
+ }
71
+ export function sha256(input) {
72
+ const hashed = hash(new TextEncoder().encode(input));
73
+ return arrayToBase64(hashed);
74
+ }
@@ -0,0 +1,36 @@
1
+ export interface EncryptionIdentity {
2
+ publicKey: CryptoKey | {
3
+ raw: Uint8Array;
4
+ };
5
+ privateKey: CryptoKey | {
6
+ raw: Uint8Array;
7
+ };
8
+ fingerprint: string;
9
+ }
10
+ /**
11
+ * EncryptionEngine
12
+ * ----------------
13
+ * Core cryptographic engine.
14
+ */
15
+ export declare class EncryptionEngine {
16
+ /**
17
+ * Generates a long-term X25519 identity keypair.
18
+ */
19
+ static generateIdentity(): Promise<EncryptionIdentity>;
20
+ /**
21
+ * Derive an identity deterministically from a BIP39 mnemonic.
22
+ * Uses Stablelib Ed25519 to derive a keypair from seed and converts to X25519.
23
+ */
24
+ static deriveIdentityFromMnemonic(mnemonic: string): Promise<EncryptionIdentity>;
25
+ /**
26
+ * Generates a SHA-256 fingerprint from a public key.
27
+ */
28
+ static fingerprintFromPublicKey(publicKey: CryptoKey | {
29
+ raw: Uint8Array;
30
+ }): Promise<string>;
31
+ private static assertPublicKey;
32
+ }
33
+ export declare class CryptoError extends Error {
34
+ cause?: unknown;
35
+ constructor(message: string, cause?: unknown);
36
+ }
@@ -0,0 +1,114 @@
1
+ import { mnemonicToSeedSync } from "@scure/bip39";
2
+ import * as ed25519 from "@stablelib/ed25519";
3
+ import ed2curve from "ed2curve";
4
+ import { fingerprintFromPublicRaw, } from "./crypto-provider";
5
+ /**
6
+ * EncryptionEngine
7
+ * ----------------
8
+ * Core cryptographic engine.
9
+ */
10
+ export class EncryptionEngine {
11
+ /* ================================
12
+ * Identity
13
+ * ================================ */
14
+ /**
15
+ * Generates a long-term X25519 identity keypair.
16
+ */
17
+ static async generateIdentity() {
18
+ try {
19
+ // Generate an Ed25519 keypair (stablelib) and convert to Curve25519
20
+ const ed = ed25519.generateKeyPair();
21
+ const skCurve = ed2curve.convertSecretKey(ed.secretKey);
22
+ const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
23
+ if (!skCurve || !pkCurve) {
24
+ throw new CryptoError("Failed to convert Ed25519 keys to Curve25519");
25
+ }
26
+ const pkBytes = new Uint8Array(pkCurve);
27
+ const skBytes = new Uint8Array(skCurve);
28
+ // Use raw key wrappers (Stablelib-backed) to avoid WebCrypto import variability
29
+ const publicKey = { type: "public", raw: pkBytes };
30
+ const privateKey = { type: "private", raw: skBytes };
31
+ const fingerprint = fingerprintFromPublicRaw(pkBytes);
32
+ return { publicKey, privateKey, fingerprint };
33
+ }
34
+ catch (err) {
35
+ throw new CryptoError("Failed to generate identity", err);
36
+ }
37
+ }
38
+ /**
39
+ * Derive an identity deterministically from a BIP39 mnemonic.
40
+ * Uses Stablelib Ed25519 to derive a keypair from seed and converts to X25519.
41
+ */
42
+ static async deriveIdentityFromMnemonic(mnemonic) {
43
+ try {
44
+ if (typeof mnemonic !== "string" || mnemonic.trim().length === 0) {
45
+ throw new CryptoError("Mnemonic must be a non-empty string");
46
+ }
47
+ // Convert mnemonic to seed (64 bytes) then reduce to 32 bytes
48
+ const seed = mnemonicToSeedSync(mnemonic); // Buffer
49
+ const seed32 = new Uint8Array(seed.slice(0, 32));
50
+ // Derive Ed25519 keypair from seed (stablelib)
51
+ const ed = ed25519.generateKeyPairFromSeed(seed32);
52
+ // Convert Ed25519 keys to X25519 (curve25519)
53
+ const skCurve = ed2curve.convertSecretKey(ed.secretKey);
54
+ const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
55
+ if (!skCurve || !pkCurve) {
56
+ throw new CryptoError("Failed to convert derived Ed25519 keys to Curve25519");
57
+ }
58
+ // Ensure plain Uint8Array
59
+ const pkCurveBytes = new Uint8Array(pkCurve);
60
+ const skCurveBytes = new Uint8Array(skCurve);
61
+ const publicKey = { type: "public", raw: pkCurveBytes };
62
+ const privateKey = { type: "private", raw: skCurveBytes };
63
+ const fingerprint = fingerprintFromPublicRaw(pkCurveBytes);
64
+ return { publicKey, privateKey, fingerprint };
65
+ }
66
+ catch (err) {
67
+ throw new CryptoError("Failed to derive identity from mnemonic", err);
68
+ }
69
+ }
70
+ /* ================================
71
+ * Fingerprinting
72
+ * ================================ */
73
+ /**
74
+ * Generates a SHA-256 fingerprint from a public key.
75
+ */
76
+ static async fingerprintFromPublicKey(publicKey) {
77
+ // Accept both CryptoKey and raw wrappers; use stablelib sha256 via provider
78
+ const anyKey = publicKey;
79
+ let rawBytes;
80
+ if (anyKey && anyKey.raw instanceof Uint8Array) {
81
+ rawBytes = anyKey.raw;
82
+ }
83
+ else {
84
+ this.assertPublicKey(publicKey);
85
+ const exported = await crypto.subtle.exportKey("raw", publicKey);
86
+ rawBytes = new Uint8Array(exported);
87
+ }
88
+ return fingerprintFromPublicRaw(rawBytes);
89
+ }
90
+ /* ================================
91
+ * Validation Helpers
92
+ * ================================ */
93
+ static assertPublicKey(key) {
94
+ const anyKey = key;
95
+ if (!key)
96
+ throw new CryptoError("Invalid public key");
97
+ if (anyKey.raw instanceof Uint8Array)
98
+ return; // raw wrapper
99
+ if (key.type !== "public") {
100
+ throw new CryptoError("Invalid public key");
101
+ }
102
+ }
103
+ }
104
+ /* ================================
105
+ * Errors
106
+ * ================================ */
107
+ export class CryptoError extends Error {
108
+ cause;
109
+ constructor(message, cause) {
110
+ super(message);
111
+ this.name = "CryptoError";
112
+ this.cause = cause;
113
+ }
114
+ }
@@ -0,0 +1,61 @@
1
+ import { MajikUser } from "@thezelijah/majik-user";
2
+ import { SerializedMajikContact } from "../../majik-contact";
3
+ export interface MajikMessageIdentityJSON {
4
+ id: string;
5
+ user_id: string;
6
+ public_key: string;
7
+ phash: string;
8
+ label: string;
9
+ timestamp: string;
10
+ restricted: boolean;
11
+ }
12
+ /**
13
+ * MajikMessageIdentity
14
+ * Immutable identity container with integrity verification
15
+ */
16
+ export declare class MajikMessageIdentity {
17
+ private readonly _id;
18
+ private readonly _userId;
19
+ private readonly _publicKey;
20
+ private readonly _phash;
21
+ private _label;
22
+ private readonly _timestamp;
23
+ private readonly _restricted;
24
+ /**
25
+ * Constructor is private to enforce controlled creation
26
+ */
27
+ private constructor();
28
+ /**
29
+ * Create a new immutable identity from MajikUser
30
+ */
31
+ static create(user: MajikUser, account: SerializedMajikContact, options?: {
32
+ label?: string;
33
+ restricted?: boolean;
34
+ }): MajikMessageIdentity;
35
+ get id(): string;
36
+ get userID(): string;
37
+ get publicKey(): string;
38
+ get phash(): string;
39
+ get label(): string;
40
+ get timestamp(): string;
41
+ get restricted(): boolean;
42
+ /**
43
+ * Only mutable field
44
+ */
45
+ set label(label: string);
46
+ /**
47
+ * Returns true if identity is restricted
48
+ */
49
+ isRestricted(): boolean;
50
+ /**
51
+ * Verify identity integrity
52
+ * Detects tampering of id/public_key
53
+ */
54
+ validateIntegrity(): boolean;
55
+ /**
56
+ * Explicit verification helper
57
+ */
58
+ matches(userId: string, publicKey: string): boolean;
59
+ toJSON(): MajikMessageIdentityJSON;
60
+ static fromJSON(json: string | MajikMessageIdentityJSON): MajikMessageIdentity;
61
+ }
@@ -0,0 +1,171 @@
1
+ import { hash } from "@stablelib/sha256";
2
+ import { arrayToBase64 } from "../../utils";
3
+ /**
4
+ * Utility assertions
5
+ */
6
+ function assert(condition, message) {
7
+ if (!condition)
8
+ throw new Error(message);
9
+ }
10
+ function assertString(value, field) {
11
+ assert(typeof value === "string" && value.trim().length > 0, `${field} must be a non-empty string`);
12
+ }
13
+ function assertISODate(value, field) {
14
+ const date = new Date(value);
15
+ assert(!isNaN(date.getTime()), `${field} must be a valid ISO timestamp`);
16
+ }
17
+ function sha256(input) {
18
+ const hashed = hash(new TextEncoder().encode(input));
19
+ return arrayToBase64(hashed);
20
+ }
21
+ /**
22
+ * MajikMessageIdentity
23
+ * Immutable identity container with integrity verification
24
+ */
25
+ export class MajikMessageIdentity {
26
+ // 🔒 Private backing fields
27
+ _id;
28
+ _userId;
29
+ _publicKey;
30
+ _phash;
31
+ _label;
32
+ _timestamp;
33
+ _restricted;
34
+ /**
35
+ * Constructor is private to enforce controlled creation
36
+ */
37
+ constructor(params) {
38
+ assertString(params.id, "id");
39
+ assertString(params.userId, "user_id");
40
+ assertString(params.publicKey, "public_key");
41
+ assertString(params.phash, "phash");
42
+ assertString(params.label, "label");
43
+ assertISODate(params.timestamp, "timestamp");
44
+ assert(typeof params.restricted === "boolean", "restricted must be boolean");
45
+ this._id = params.id;
46
+ this._userId = params.userId;
47
+ this._publicKey = params.publicKey;
48
+ this._phash = params.phash;
49
+ this._label = params.label;
50
+ this._timestamp = params.timestamp;
51
+ this._restricted = params.restricted;
52
+ // Final integrity check at construction
53
+ assert(this.validateIntegrity(), "Identity integrity validation failed");
54
+ }
55
+ // ─────────────────────────────
56
+ // Static factory
57
+ // ─────────────────────────────
58
+ /**
59
+ * Create a new immutable identity from MajikUser
60
+ */
61
+ static create(user, account, options) {
62
+ assert(user, "MajikUser is required");
63
+ const userValidResult = user.validate();
64
+ if (!userValidResult.isValid) {
65
+ throw new Error(`Invalid MajikUser: ${userValidResult.errors.join(", ")}`);
66
+ }
67
+ const label = options?.label || account?.meta?.label || user.displayName;
68
+ assertString(label, "label");
69
+ const timestamp = new Date().toISOString();
70
+ const publicKey = account.publicKeyBase64;
71
+ const phash = sha256(`${user.id}:${publicKey}:${account.id}`);
72
+ return new MajikMessageIdentity({
73
+ id: account.id,
74
+ userId: user.id,
75
+ publicKey: publicKey,
76
+ phash,
77
+ label,
78
+ timestamp,
79
+ restricted: options?.restricted ?? false,
80
+ });
81
+ }
82
+ // ─────────────────────────────
83
+ // Getters (safe, read-only)
84
+ // ─────────────────────────────
85
+ get id() {
86
+ return this._id;
87
+ }
88
+ get userID() {
89
+ return this._userId;
90
+ }
91
+ get publicKey() {
92
+ return this._publicKey;
93
+ }
94
+ get phash() {
95
+ return this._phash;
96
+ }
97
+ get label() {
98
+ return this._label;
99
+ }
100
+ get timestamp() {
101
+ return this._timestamp;
102
+ }
103
+ get restricted() {
104
+ return this._restricted;
105
+ }
106
+ // ─────────────────────────────
107
+ // Mutators (restricted)
108
+ // ─────────────────────────────
109
+ /**
110
+ * Only mutable field
111
+ */
112
+ set label(label) {
113
+ assertString(label, "label");
114
+ this._label = label;
115
+ }
116
+ // ─────────────────────────────
117
+ // Identity checks
118
+ // ─────────────────────────────
119
+ /**
120
+ * Returns true if identity is restricted
121
+ */
122
+ isRestricted() {
123
+ return this._restricted === true;
124
+ }
125
+ /**
126
+ * Verify identity integrity
127
+ * Detects tampering of id/public_key
128
+ */
129
+ validateIntegrity() {
130
+ const expected = sha256(`${this._userId}:${this._publicKey}:${this._id}`);
131
+ return expected === this._phash;
132
+ }
133
+ /**
134
+ * Explicit verification helper
135
+ */
136
+ matches(userId, publicKey) {
137
+ assertString(userId, "userId");
138
+ assertString(publicKey, "publicKey");
139
+ const hash = sha256(`${userId}:${publicKey}:${this._id}`);
140
+ return hash === this._phash;
141
+ }
142
+ // ─────────────────────────────
143
+ // Serialization
144
+ // ─────────────────────────────
145
+ toJSON() {
146
+ return {
147
+ id: this._id,
148
+ user_id: this._userId,
149
+ public_key: this._publicKey,
150
+ phash: this._phash,
151
+ label: this._label,
152
+ timestamp: this._timestamp,
153
+ restricted: this._restricted,
154
+ };
155
+ }
156
+ static fromJSON(json) {
157
+ const obj = typeof json === "string" ? JSON.parse(json) : json;
158
+ assert(typeof obj === "object" && obj !== null, "Invalid JSON object");
159
+ const identity = new MajikMessageIdentity({
160
+ id: obj.id,
161
+ userId: obj.user_id,
162
+ publicKey: obj.public_key,
163
+ phash: obj.phash,
164
+ label: obj.label,
165
+ timestamp: obj.timestamp,
166
+ restricted: obj.restricted,
167
+ });
168
+ assert(identity.validateIntegrity(), "Invalid phash in JSON");
169
+ return identity;
170
+ }
171
+ }
@@ -0,0 +1,4 @@
1
+ export declare class MajikKeyError extends Error {
2
+ cause?: unknown;
3
+ constructor(message: string, cause?: unknown);
4
+ }
@@ -0,0 +1,11 @@
1
+ /* -------------------------------
2
+ * Errors
3
+ * ------------------------------- */
4
+ export class MajikKeyError extends Error {
5
+ cause;
6
+ constructor(message, cause) {
7
+ super(message);
8
+ this.name = "MajikKeyError";
9
+ this.cause = cause;
10
+ }
11
+ }
@@ -0,0 +1,72 @@
1
+ import { MajikMessageIdentityJSON } from "./database/system/identity";
2
+ import { ISODateString } from "./types";
3
+ export type SerializedMajikContact = Omit<MajikContactData, "publicKey"> & {
4
+ publicKeyBase64: string;
5
+ };
6
+ export interface MajikContactMeta {
7
+ label?: string;
8
+ notes?: string;
9
+ blocked?: boolean;
10
+ createdAt?: ISODateString;
11
+ updatedAt?: ISODateString;
12
+ }
13
+ export interface MajikContactData {
14
+ id: string;
15
+ publicKey: CryptoKey | {
16
+ raw: Uint8Array;
17
+ };
18
+ fingerprint: string;
19
+ meta?: MajikContactMeta;
20
+ majikah_registered?: boolean;
21
+ }
22
+ export interface MajikContactCard {
23
+ id: string;
24
+ publicKey: string;
25
+ fingerprint: string;
26
+ label: string;
27
+ }
28
+ export declare class MajikContactError extends Error {
29
+ cause?: unknown;
30
+ constructor(message: string, cause?: unknown);
31
+ }
32
+ export declare class MajikContact {
33
+ readonly id: string;
34
+ readonly publicKey: CryptoKey | {
35
+ raw: Uint8Array;
36
+ };
37
+ readonly fingerprint: string;
38
+ meta: MajikContactMeta;
39
+ private majikah_registered?;
40
+ constructor(data: MajikContactData);
41
+ static create(id: string, publicKey: CryptoKey | {
42
+ raw: Uint8Array;
43
+ }, fingerprint: string, meta?: Partial<MajikContactMeta>): MajikContact;
44
+ private assertId;
45
+ private assertPublicKey;
46
+ private assertFingerprint;
47
+ private updateTimestamp;
48
+ updateLabel(label: string): this;
49
+ updateNotes(notes: string): this;
50
+ isBlocked(): boolean;
51
+ setBlocked(blocked: boolean): this;
52
+ block(): this;
53
+ unblock(): this;
54
+ isMajikahIdentityChecked(): boolean;
55
+ isMajikahRegistered(): boolean;
56
+ setMajikahStatus(status: boolean): this;
57
+ getDisplayName(): Promise<string>;
58
+ /**
59
+ * Support both CryptoKey and raw-key wrappers (fallbacks when WebCrypto X25519 unsupported)
60
+ */
61
+ getPublicKeyBase64(): Promise<string>;
62
+ toJSON(): Promise<SerializedMajikContact>;
63
+ /**
64
+ * Reconstruct a MajikContact from its serialized form
65
+ */
66
+ static fromJSON(serialized: SerializedMajikContact): MajikContact;
67
+ /**
68
+ * Create a new MajikContact from a MajikMessageIdentityJSON
69
+ */
70
+ static fromIdentityJSON(identityJSON: MajikMessageIdentityJSON): Promise<MajikContact>;
71
+ static isBlocked(contact: MajikContact): boolean;
72
+ }