@majikah/majik-key 0.2.10 → 0.2.12
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/types.d.ts +6 -0
- package/dist/core/web3/constants.d.ts +1 -0
- package/dist/core/web3/constants.js +1 -0
- package/dist/core/web3/solana.d.ts +74 -0
- package/dist/core/web3/solana.js +146 -0
- package/dist/core/web3/types.d.ts +26 -0
- package/dist/core/web3/types.js +1 -0
- package/dist/majik-key.d.ts +57 -1
- package/dist/majik-key.js +151 -2
- package/package.json +12 -3
package/dist/core/types.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface MajikKeyJSON {
|
|
|
21
21
|
encryptedMlDsaSecretKey?: string;
|
|
22
22
|
mnemonicLanguage?: MnemonicLanguage;
|
|
23
23
|
}
|
|
24
|
+
export interface MajikKeyDangerousJSON extends MajikKeyJSON {
|
|
25
|
+
privateKeyBase64: string;
|
|
26
|
+
mlKemSecretKeyBase64: string;
|
|
27
|
+
edSecretKeyBase64: string;
|
|
28
|
+
mlDsaSecretKeyBase64: string;
|
|
29
|
+
}
|
|
24
30
|
export interface MajikKeyMetadata {
|
|
25
31
|
id: string;
|
|
26
32
|
fingerprint: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const MAJIK_SOLANA_SEED = "MajikKeySolanaSeed";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MAJIK_SOLANA_SEED = "MajikKeySolanaSeed";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* solana.ts
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ EXPERIMENTAL — Solana keypair utilities for MajikKey.
|
|
5
|
+
* This module's API may change or be removed without notice in a minor version.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Solana accounts are plain Ed25519 keypairs — no new key material or
|
|
9
|
+
* wallet standard is needed, just bytes in the right shape.
|
|
10
|
+
* - `secretKey` is always the 64-byte nacl/tweetnacl-compatible format
|
|
11
|
+
* (32-byte seed || 32-byte public key) — exactly what
|
|
12
|
+
* `@solana/web3.js`'s `Keypair.fromSecretKey()` expects, and exactly
|
|
13
|
+
* what `@stablelib/ed25519` already produces.
|
|
14
|
+
* - `@solana/web3.js` is NOT a dependency of this package. It is lazily
|
|
15
|
+
* `import()`-ed only when a caller needs an actual `Keypair`/`PublicKey`
|
|
16
|
+
* instance (e.g. to build a transaction). If it isn't installed, we
|
|
17
|
+
* throw a clear, actionable MajikKeyError instead of failing module
|
|
18
|
+
* load. Base58 address derivation does NOT require the library at all.
|
|
19
|
+
*
|
|
20
|
+
* Two ways to obtain a Solana identity from a MajikKey:
|
|
21
|
+
* 1. deriveSolanaKeypairFromEdSecretKey() — RECOMMENDED. Domain-separates
|
|
22
|
+
* a brand-new Ed25519 keypair from the MajikKey's message-signing
|
|
23
|
+
* Ed25519 secret key, so the Solana key is never reused elsewhere.
|
|
24
|
+
* 2. solanaMaterialFromEd25519SecretKey() — reuses the MajikKey's message
|
|
25
|
+
* signing Ed25519 keypair AS-IS. Simpler, but means the same private
|
|
26
|
+
* key secures two different protocols. Opt-in only.
|
|
27
|
+
*/
|
|
28
|
+
export interface SolanaKeypairMaterial {
|
|
29
|
+
/** 32-byte Ed25519 / Solana public key. */
|
|
30
|
+
publicKey: Uint8Array;
|
|
31
|
+
/** 64-byte nacl-format secret key (32-byte seed || 32-byte public key). */
|
|
32
|
+
secretKey: Uint8Array;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Derive a Solana keypair domain-separated from the MajikKey's
|
|
36
|
+
* message-signing Ed25519 key, but fully deterministic from it (and
|
|
37
|
+
* therefore ultimately from the mnemonic).
|
|
38
|
+
*
|
|
39
|
+
* seed' = SHA256(edSecretKey[0..32] || "MajikMessageSolanaSeed")
|
|
40
|
+
*/
|
|
41
|
+
export declare function deriveSolanaKeypairFromEdSecretKey(edSecretKey: Uint8Array): SolanaKeypairMaterial;
|
|
42
|
+
/**
|
|
43
|
+
* Reuse the MajikKey's existing message-signing Ed25519 keypair directly
|
|
44
|
+
* as a Solana keypair (no re-derivation).
|
|
45
|
+
*
|
|
46
|
+
* ⚠️ Not recommended: the same private key would secure both Majik message
|
|
47
|
+
* signing AND any Solana transactions. Prefer
|
|
48
|
+
* `deriveSolanaKeypairFromEdSecretKey()` unless you specifically want the
|
|
49
|
+
* identical key on both.
|
|
50
|
+
*/
|
|
51
|
+
export declare function solanaMaterialFromEd25519SecretKey(edSecretKey: Uint8Array): SolanaKeypairMaterial;
|
|
52
|
+
export declare function base58Encode(bytes: Uint8Array): string;
|
|
53
|
+
/**
|
|
54
|
+
* Solana address for a given Solana/Ed25519 public key — just its base58
|
|
55
|
+
* encoding. Does NOT require @solana/web3.js.
|
|
56
|
+
*/
|
|
57
|
+
export declare function solanaAddressFromPublicKey(publicKey: Uint8Array): string;
|
|
58
|
+
type SolanaKitModule = typeof import("@solana/kit");
|
|
59
|
+
export declare function loadSolanaKit(): Promise<SolanaKitModule>;
|
|
60
|
+
/**
|
|
61
|
+
* Real @solana/kit KeyPairSigner backed by a genuine CryptoKeyPair.
|
|
62
|
+
* Requires @solana/kit — see loadSolanaKit().
|
|
63
|
+
*/
|
|
64
|
+
export declare function toSolanaKeyPairSigner(material: SolanaKeypairMaterial): Promise<Awaited<ReturnType<SolanaKitModule["createKeyPairSignerFromBytes"]>>>;
|
|
65
|
+
/**
|
|
66
|
+
* Real @solana/kit Address (branded string) for this material's public key.
|
|
67
|
+
*/
|
|
68
|
+
export declare function toSolanaAddress(material: SolanaKeypairMaterial): Promise<Awaited<ReturnType<SolanaKitModule["getAddressFromPublicKey"]>>>;
|
|
69
|
+
/**
|
|
70
|
+
* Sign an arbitrary message with a Solana keypair's Ed25519 secret key.
|
|
71
|
+
* Uses @stablelib/ed25519 directly — does NOT require @solana/web3.js.
|
|
72
|
+
*/
|
|
73
|
+
export declare function signWithSolanaMaterial(material: SolanaKeypairMaterial, message: Uint8Array): Uint8Array;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* solana.ts
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ EXPERIMENTAL — Solana keypair utilities for MajikKey.
|
|
5
|
+
* This module's API may change or be removed without notice in a minor version.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Solana accounts are plain Ed25519 keypairs — no new key material or
|
|
9
|
+
* wallet standard is needed, just bytes in the right shape.
|
|
10
|
+
* - `secretKey` is always the 64-byte nacl/tweetnacl-compatible format
|
|
11
|
+
* (32-byte seed || 32-byte public key) — exactly what
|
|
12
|
+
* `@solana/web3.js`'s `Keypair.fromSecretKey()` expects, and exactly
|
|
13
|
+
* what `@stablelib/ed25519` already produces.
|
|
14
|
+
* - `@solana/web3.js` is NOT a dependency of this package. It is lazily
|
|
15
|
+
* `import()`-ed only when a caller needs an actual `Keypair`/`PublicKey`
|
|
16
|
+
* instance (e.g. to build a transaction). If it isn't installed, we
|
|
17
|
+
* throw a clear, actionable MajikKeyError instead of failing module
|
|
18
|
+
* load. Base58 address derivation does NOT require the library at all.
|
|
19
|
+
*
|
|
20
|
+
* Two ways to obtain a Solana identity from a MajikKey:
|
|
21
|
+
* 1. deriveSolanaKeypairFromEdSecretKey() — RECOMMENDED. Domain-separates
|
|
22
|
+
* a brand-new Ed25519 keypair from the MajikKey's message-signing
|
|
23
|
+
* Ed25519 secret key, so the Solana key is never reused elsewhere.
|
|
24
|
+
* 2. solanaMaterialFromEd25519SecretKey() — reuses the MajikKey's message
|
|
25
|
+
* signing Ed25519 keypair AS-IS. Simpler, but means the same private
|
|
26
|
+
* key secures two different protocols. Opt-in only.
|
|
27
|
+
*/
|
|
28
|
+
import * as ed25519 from "@stablelib/ed25519";
|
|
29
|
+
import { hash } from "@stablelib/sha256";
|
|
30
|
+
import { MAJIK_SOLANA_SEED } from "./constants";
|
|
31
|
+
import { MajikKeyError } from "../error";
|
|
32
|
+
const ED25519_SECRET_KEY_LENGTH = 64;
|
|
33
|
+
const ED25519_SEED_LENGTH = 32;
|
|
34
|
+
// ─── Derivation ─────────────────────────────────────────────────────────────
|
|
35
|
+
/**
|
|
36
|
+
* Derive a Solana keypair domain-separated from the MajikKey's
|
|
37
|
+
* message-signing Ed25519 key, but fully deterministic from it (and
|
|
38
|
+
* therefore ultimately from the mnemonic).
|
|
39
|
+
*
|
|
40
|
+
* seed' = SHA256(edSecretKey[0..32] || "MajikMessageSolanaSeed")
|
|
41
|
+
*/
|
|
42
|
+
export function deriveSolanaKeypairFromEdSecretKey(edSecretKey) {
|
|
43
|
+
if (edSecretKey.length !== ED25519_SECRET_KEY_LENGTH) {
|
|
44
|
+
throw new MajikKeyError(`Expected a 64-byte Ed25519 secret key, got ${edSecretKey.length} bytes`);
|
|
45
|
+
}
|
|
46
|
+
const edSeed = edSecretKey.slice(0, ED25519_SEED_LENGTH);
|
|
47
|
+
const domain = new TextEncoder().encode(MAJIK_SOLANA_SEED);
|
|
48
|
+
const combined = new Uint8Array(edSeed.length + domain.length);
|
|
49
|
+
combined.set(edSeed, 0);
|
|
50
|
+
combined.set(domain, edSeed.length);
|
|
51
|
+
const solanaSeed = hash(combined); // 32 bytes
|
|
52
|
+
const kp = ed25519.generateKeyPairFromSeed(solanaSeed);
|
|
53
|
+
return { publicKey: kp.publicKey, secretKey: kp.secretKey };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reuse the MajikKey's existing message-signing Ed25519 keypair directly
|
|
57
|
+
* as a Solana keypair (no re-derivation).
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ Not recommended: the same private key would secure both Majik message
|
|
60
|
+
* signing AND any Solana transactions. Prefer
|
|
61
|
+
* `deriveSolanaKeypairFromEdSecretKey()` unless you specifically want the
|
|
62
|
+
* identical key on both.
|
|
63
|
+
*/
|
|
64
|
+
export function solanaMaterialFromEd25519SecretKey(edSecretKey) {
|
|
65
|
+
if (edSecretKey.length !== ED25519_SECRET_KEY_LENGTH) {
|
|
66
|
+
throw new MajikKeyError(`Expected a 64-byte Ed25519 secret key, got ${edSecretKey.length} bytes`);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
publicKey: edSecretKey.slice(ED25519_SEED_LENGTH),
|
|
70
|
+
secretKey: edSecretKey.slice(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// ─── Base58 (Solana address encoding) — no external dependency ─────────────
|
|
74
|
+
const BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
75
|
+
export function base58Encode(bytes) {
|
|
76
|
+
if (bytes.length === 0)
|
|
77
|
+
return "";
|
|
78
|
+
const digits = [0];
|
|
79
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
80
|
+
let carry = bytes[i];
|
|
81
|
+
for (let j = 0; j < digits.length; j++) {
|
|
82
|
+
carry += digits[j] << 8;
|
|
83
|
+
digits[j] = carry % 58;
|
|
84
|
+
carry = (carry / 58) | 0;
|
|
85
|
+
}
|
|
86
|
+
while (carry > 0) {
|
|
87
|
+
digits.push(carry % 58);
|
|
88
|
+
carry = (carry / 58) | 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
let leadingZeros = 0;
|
|
92
|
+
for (let i = 0; i < bytes.length && bytes[i] === 0; i++)
|
|
93
|
+
leadingZeros++;
|
|
94
|
+
let result = "1".repeat(leadingZeros);
|
|
95
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
96
|
+
result += BASE58_ALPHABET[digits[i]];
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Solana address for a given Solana/Ed25519 public key — just its base58
|
|
102
|
+
* encoding. Does NOT require @solana/web3.js.
|
|
103
|
+
*/
|
|
104
|
+
export function solanaAddressFromPublicKey(publicKey) {
|
|
105
|
+
return base58Encode(publicKey);
|
|
106
|
+
}
|
|
107
|
+
let _kitModule = null;
|
|
108
|
+
export async function loadSolanaKit() {
|
|
109
|
+
if (_kitModule)
|
|
110
|
+
return _kitModule;
|
|
111
|
+
try {
|
|
112
|
+
_kitModule = (await import(
|
|
113
|
+
/* webpackIgnore: true */
|
|
114
|
+
/* @vite-ignore */
|
|
115
|
+
"@solana/kit"));
|
|
116
|
+
return _kitModule;
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
throw new MajikKeyError("@solana/kit is required for this operation but is not installed. " +
|
|
120
|
+
"Install it in your project with `npm install @solana/kit` " +
|
|
121
|
+
"(or the yarn/pnpm equivalent) and try again.", err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Real @solana/kit KeyPairSigner backed by a genuine CryptoKeyPair.
|
|
126
|
+
* Requires @solana/kit — see loadSolanaKit().
|
|
127
|
+
*/
|
|
128
|
+
export async function toSolanaKeyPairSigner(material) {
|
|
129
|
+
const kit = await loadSolanaKit();
|
|
130
|
+
return kit.createKeyPairSignerFromBytes(material.secretKey);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Real @solana/kit Address (branded string) for this material's public key.
|
|
134
|
+
*/
|
|
135
|
+
export async function toSolanaAddress(material) {
|
|
136
|
+
const kit = await loadSolanaKit();
|
|
137
|
+
const signer = await toSolanaKeyPairSigner(material);
|
|
138
|
+
return signer.address;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Sign an arbitrary message with a Solana keypair's Ed25519 secret key.
|
|
142
|
+
* Uses @stablelib/ed25519 directly — does NOT require @solana/web3.js.
|
|
143
|
+
*/
|
|
144
|
+
export function signWithSolanaMaterial(material, message) {
|
|
145
|
+
return ed25519.sign(material.secretKey, message);
|
|
146
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @experimental Web3 / blockchain integrations are experimental. This
|
|
3
|
+
* namespace's shape may change without a major version bump.
|
|
4
|
+
*/
|
|
5
|
+
export interface MajikKeySolanaNamespace {
|
|
6
|
+
/** 32-byte Solana/Ed25519 public key. */
|
|
7
|
+
readonly publicKey: Uint8Array;
|
|
8
|
+
/** 64-byte nacl-format secret key. Handle with the same care as any private key. */
|
|
9
|
+
readonly secretKey: Uint8Array;
|
|
10
|
+
/** Base58 Solana address — does not require @solana/kit. */
|
|
11
|
+
readonly address: string;
|
|
12
|
+
/** Real @solana/kit Keypair. Lazily loads @solana/kit — throws if not installed. */
|
|
13
|
+
getSolanaKeypair(): Promise<any>;
|
|
14
|
+
/** Real @solana/kit PublicKey. Lazily loads @solana/kit — throws if not installed. */
|
|
15
|
+
getSolanaAddress(): Promise<any>;
|
|
16
|
+
/** Sign a message with this Solana keypair's Ed25519 key. No web3.js needed. */
|
|
17
|
+
sign(message: Uint8Array): Uint8Array;
|
|
18
|
+
}
|
|
19
|
+
/** @experimental */
|
|
20
|
+
export interface MajikKeyWeb3Namespace {
|
|
21
|
+
readonly solana: MajikKeySolanaNamespace;
|
|
22
|
+
}
|
|
23
|
+
/** @experimental */
|
|
24
|
+
export interface MajikKeyWeb3Namespace {
|
|
25
|
+
readonly solana: MajikKeySolanaNamespace;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/majik-key.d.ts
CHANGED
|
@@ -23,10 +23,12 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { MajikContact, MajikContactMeta } from "@majikah/majik-contact";
|
|
25
25
|
import { KDF_VERSION } from "./core/crypto/constants";
|
|
26
|
-
import type { MajikKeyJSON, MajikKeyMetadata, MnemonicJSON } from "./core/types";
|
|
26
|
+
import type { MajikKeyDangerousJSON, MajikKeyJSON, MajikKeyMetadata, MnemonicJSON } from "./core/types";
|
|
27
27
|
import { MajikMessageIdentity } from "./core/database/system/identity";
|
|
28
28
|
import { MajikUser } from "@thezelijah/majik-user";
|
|
29
29
|
import { MnemonicLanguage } from "./core/crypto/wordlist";
|
|
30
|
+
import { SolanaKeypairMaterial } from "./core/web3/solana";
|
|
31
|
+
import { MajikKeyWeb3Namespace } from "./core/web3/types";
|
|
30
32
|
export interface MajikKeyIdentity {
|
|
31
33
|
id: string;
|
|
32
34
|
publicKey: CryptoKey | {
|
|
@@ -117,6 +119,7 @@ export declare class MajikKey {
|
|
|
117
119
|
private _mlDsaSecretKey?;
|
|
118
120
|
private _encryptedMlDsaSecretKey?;
|
|
119
121
|
private _encryptedMlDsaSecretKeyBase64?;
|
|
122
|
+
private _solanaKeypairMaterial?;
|
|
120
123
|
private constructor();
|
|
121
124
|
get id(): string;
|
|
122
125
|
get fingerprint(): string;
|
|
@@ -142,6 +145,20 @@ export declare class MajikKey {
|
|
|
142
145
|
get hasSigningKeys(): boolean;
|
|
143
146
|
static create(mnemonic: string, passphrase: string, label?: string, mnemonicLanguage?: MnemonicLanguage): Promise<MajikKey>;
|
|
144
147
|
static fromJSON(json: MajikKeyJSON | string): MajikKey;
|
|
148
|
+
/**
|
|
149
|
+
* Export a fully unlocked MajikKey with all raw private keys.
|
|
150
|
+
* ⚠️ DANGEROUS — output contains unencrypted private key material.
|
|
151
|
+
* Only use for server-side secrets injection.
|
|
152
|
+
* Never log, store in a database, or transmit over the network.
|
|
153
|
+
*/
|
|
154
|
+
toDangerousJSON(): MajikKeyDangerousJSON;
|
|
155
|
+
/**
|
|
156
|
+
* Reconstruct a fully unlocked MajikKey from a dangerous JSON export.
|
|
157
|
+
* ⚠️ DANGEROUS — input contains unencrypted private key material.
|
|
158
|
+
* Intended for server-side use only (e.g. TSA signing key loaded from Cloudflare Secrets).
|
|
159
|
+
* No KDF is involved — reconstruction is instant.
|
|
160
|
+
*/
|
|
161
|
+
static fromDangerousJSON(json: MajikKeyDangerousJSON | string): MajikKey;
|
|
145
162
|
toMnemonicJSON(mnemonic: string, passphrase?: string): MnemonicJSON;
|
|
146
163
|
static fromMnemonicJSON(mnemonicJson: MnemonicJSON | string, passphrase: string, label?: string): Promise<MajikKey>;
|
|
147
164
|
updateLabel(newLabel: string): this;
|
|
@@ -207,4 +224,43 @@ export declare class MajikKey {
|
|
|
207
224
|
private static _exportMnemonicBackup;
|
|
208
225
|
private static _deriveLegacyMnemonicKey;
|
|
209
226
|
private static _exportKeyToBase64;
|
|
227
|
+
/**
|
|
228
|
+
* @experimental Blockchain/web3 integrations are experimental — this API
|
|
229
|
+
* may change without notice. `undefined` when the MajikKey is locked or
|
|
230
|
+
* has no Ed25519 signing key to derive from.
|
|
231
|
+
*
|
|
232
|
+
* By default `web3.solana` is DOMAIN-SEPARATED from the MajikKey's message
|
|
233
|
+
* signing key (see deriveSolanaKeypairFromEdSecretKey). Use
|
|
234
|
+
* `getSolanaKeypairMaterial({ reuseMessageKey: true })` if you specifically
|
|
235
|
+
* want the identical key reused for Solana instead.
|
|
236
|
+
*/
|
|
237
|
+
get web3(): MajikKeyWeb3Namespace | undefined;
|
|
238
|
+
/**
|
|
239
|
+
* @experimental True if this MajikKey can currently produce a Solana
|
|
240
|
+
* keypair (i.e. it's unlocked and has an Ed25519 signing key).
|
|
241
|
+
*/
|
|
242
|
+
get hasSolanaKeypair(): boolean;
|
|
243
|
+
private _getOrDeriveSolanaMaterial;
|
|
244
|
+
/**
|
|
245
|
+
* @experimental Raw Solana keypair material (public/secret key bytes).
|
|
246
|
+
* Pass `{ reuseMessageKey: true }` to reuse the MajikKey's message signing
|
|
247
|
+
* Ed25519 key directly instead of the domain-separated derivation.
|
|
248
|
+
*/
|
|
249
|
+
getSolanaKeypairMaterial(options?: {
|
|
250
|
+
reuseMessageKey?: boolean;
|
|
251
|
+
}): SolanaKeypairMaterial;
|
|
252
|
+
/**
|
|
253
|
+
* @experimental Real @solana/kit Keypair instance. Lazily loads
|
|
254
|
+
* @solana/kit — throws a MajikKeyError with install instructions if
|
|
255
|
+
* it isn't present in the consuming project.
|
|
256
|
+
*/
|
|
257
|
+
getSolanaKeypair(options?: {
|
|
258
|
+
reuseMessageKey?: boolean;
|
|
259
|
+
}): Promise<any>;
|
|
260
|
+
/**
|
|
261
|
+
* @experimental Base58 Solana address. Does NOT require @solana/kit.
|
|
262
|
+
*/
|
|
263
|
+
getSolanaAddress(options?: {
|
|
264
|
+
reuseMessageKey?: boolean;
|
|
265
|
+
}): string;
|
|
210
266
|
}
|
package/dist/majik-key.js
CHANGED
|
@@ -31,6 +31,7 @@ import { MajikKeyValidator } from "./core/validator";
|
|
|
31
31
|
import { MajikKeyError } from "./core/error";
|
|
32
32
|
import { MajikMessageIdentity } from "./core/database/system/identity";
|
|
33
33
|
import { WORDLISTS } from "./core/crypto/wordlist";
|
|
34
|
+
import { deriveSolanaKeypairFromEdSecretKey, signWithSolanaMaterial, solanaAddressFromPublicKey, solanaMaterialFromEd25519SecretKey, toSolanaAddress, toSolanaKeyPairSigner, } from "./core/web3/solana";
|
|
34
35
|
const SALT_SIZE = 32;
|
|
35
36
|
// ─── MajikKey ─────────────────────────────────────────────────────────────────
|
|
36
37
|
export class MajikKey {
|
|
@@ -60,6 +61,8 @@ export class MajikKey {
|
|
|
60
61
|
_mlDsaSecretKey;
|
|
61
62
|
_encryptedMlDsaSecretKey;
|
|
62
63
|
_encryptedMlDsaSecretKeyBase64;
|
|
64
|
+
//Experimental
|
|
65
|
+
_solanaKeypairMaterial;
|
|
63
66
|
constructor(options) {
|
|
64
67
|
this._id = options.id;
|
|
65
68
|
this._publicKey = options.publicKey;
|
|
@@ -271,6 +274,81 @@ export class MajikKey {
|
|
|
271
274
|
throw new MajikKeyError("Failed to parse MajikKey from JSON", err);
|
|
272
275
|
}
|
|
273
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Export a fully unlocked MajikKey with all raw private keys.
|
|
279
|
+
* ⚠️ DANGEROUS — output contains unencrypted private key material.
|
|
280
|
+
* Only use for server-side secrets injection.
|
|
281
|
+
* Never log, store in a database, or transmit over the network.
|
|
282
|
+
*/
|
|
283
|
+
toDangerousJSON() {
|
|
284
|
+
if (this.isLocked)
|
|
285
|
+
throw new MajikKeyError("MajikKey must be unlocked to export dangerous JSON.");
|
|
286
|
+
if (!this._edSecretKey ||
|
|
287
|
+
!this._mlDsaSecretKey ||
|
|
288
|
+
!this._mlKemSecretKey ||
|
|
289
|
+
!this._privateKeyBase64)
|
|
290
|
+
throw new MajikKeyError("MajikKey is missing secret keys — re-import via importFromMnemonicBackup() first.");
|
|
291
|
+
return {
|
|
292
|
+
...this.toJSON(),
|
|
293
|
+
privateKeyBase64: this._privateKeyBase64,
|
|
294
|
+
mlKemSecretKeyBase64: arrayToBase64(this._mlKemSecretKey),
|
|
295
|
+
edSecretKeyBase64: arrayToBase64(this._edSecretKey),
|
|
296
|
+
mlDsaSecretKeyBase64: arrayToBase64(this._mlDsaSecretKey),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Reconstruct a fully unlocked MajikKey from a dangerous JSON export.
|
|
301
|
+
* ⚠️ DANGEROUS — input contains unencrypted private key material.
|
|
302
|
+
* Intended for server-side use only (e.g. TSA signing key loaded from Cloudflare Secrets).
|
|
303
|
+
* No KDF is involved — reconstruction is instant.
|
|
304
|
+
*/
|
|
305
|
+
static fromDangerousJSON(json) {
|
|
306
|
+
try {
|
|
307
|
+
const parsed = typeof json === "string" ? JSON.parse(json) : json;
|
|
308
|
+
if (!parsed.id ||
|
|
309
|
+
!parsed.fingerprint ||
|
|
310
|
+
!parsed.publicKey ||
|
|
311
|
+
!parsed.privateKeyBase64 ||
|
|
312
|
+
!parsed.edPublicKey ||
|
|
313
|
+
!parsed.edSecretKeyBase64 ||
|
|
314
|
+
!parsed.mlDsaPublicKey ||
|
|
315
|
+
!parsed.mlDsaSecretKeyBase64 ||
|
|
316
|
+
!parsed.mlKemPublicKey ||
|
|
317
|
+
!parsed.mlKemSecretKeyBase64)
|
|
318
|
+
throw new MajikKeyError("Invalid MajikKeyDangerousJSON — missing required fields");
|
|
319
|
+
const privateKeyBytes = base64ToUint8Array(parsed.privateKeyBase64);
|
|
320
|
+
const edPublicKey = base64ToUint8Array(parsed.edPublicKey);
|
|
321
|
+
const edSecretKey = base64ToUint8Array(parsed.edSecretKeyBase64);
|
|
322
|
+
const mlDsaPublicKey = base64ToUint8Array(parsed.mlDsaPublicKey);
|
|
323
|
+
const mlDsaSecretKey = base64ToUint8Array(parsed.mlDsaSecretKeyBase64);
|
|
324
|
+
const mlKemPublicKey = base64ToUint8Array(parsed.mlKemPublicKey);
|
|
325
|
+
const mlKemSecretKey = base64ToUint8Array(parsed.mlKemSecretKeyBase64);
|
|
326
|
+
return new MajikKey({
|
|
327
|
+
id: parsed.id,
|
|
328
|
+
fingerprint: parsed.fingerprint,
|
|
329
|
+
publicKey: { raw: base64ToUint8Array(parsed.publicKey) },
|
|
330
|
+
publicKeyBase64: parsed.publicKey,
|
|
331
|
+
privateKey: { raw: privateKeyBytes },
|
|
332
|
+
privateKeyBase64: parsed.privateKeyBase64,
|
|
333
|
+
encryptedPrivateKey: new ArrayBuffer(0),
|
|
334
|
+
encryptedPrivateKeyBase64: parsed.encryptedPrivateKey,
|
|
335
|
+
salt: parsed.salt,
|
|
336
|
+
backup: parsed.backup,
|
|
337
|
+
kdfVersion: parsed?.kdfVersion || KDF_VERSION.ARGON2ID,
|
|
338
|
+
mlKemPublicKey,
|
|
339
|
+
mlKemSecretKey,
|
|
340
|
+
edPublicKey,
|
|
341
|
+
edSecretKey,
|
|
342
|
+
mlDsaPublicKey,
|
|
343
|
+
mlDsaSecretKey,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
if (err instanceof MajikKeyError)
|
|
348
|
+
throw err;
|
|
349
|
+
throw new MajikKeyError("Failed to reconstruct MajikKey from dangerous JSON", err);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
274
352
|
// ── MnemonicJSON ─────────────────────────────────────────────────────────────
|
|
275
353
|
toMnemonicJSON(mnemonic, passphrase) {
|
|
276
354
|
if (this.isLocked)
|
|
@@ -382,8 +460,9 @@ export class MajikKey {
|
|
|
382
460
|
this._privateKey = undefined;
|
|
383
461
|
this._privateKeyBase64 = undefined;
|
|
384
462
|
this._mlKemSecretKey = undefined;
|
|
385
|
-
this._edSecretKey = undefined;
|
|
386
|
-
this._mlDsaSecretKey = undefined;
|
|
463
|
+
this._edSecretKey = undefined;
|
|
464
|
+
this._mlDsaSecretKey = undefined;
|
|
465
|
+
this._solanaKeypairMaterial = undefined;
|
|
387
466
|
return this;
|
|
388
467
|
}
|
|
389
468
|
async unlock(passphrase) {
|
|
@@ -831,4 +910,74 @@ export class MajikKey {
|
|
|
831
910
|
const raw = await crypto.subtle.exportKey("raw", key);
|
|
832
911
|
return arrayBufferToBase64(raw);
|
|
833
912
|
}
|
|
913
|
+
// ── WEB3 (EXPERIMENTAL) ─────────────────────────────────────────────────────
|
|
914
|
+
/**
|
|
915
|
+
* @experimental Blockchain/web3 integrations are experimental — this API
|
|
916
|
+
* may change without notice. `undefined` when the MajikKey is locked or
|
|
917
|
+
* has no Ed25519 signing key to derive from.
|
|
918
|
+
*
|
|
919
|
+
* By default `web3.solana` is DOMAIN-SEPARATED from the MajikKey's message
|
|
920
|
+
* signing key (see deriveSolanaKeypairFromEdSecretKey). Use
|
|
921
|
+
* `getSolanaKeypairMaterial({ reuseMessageKey: true })` if you specifically
|
|
922
|
+
* want the identical key reused for Solana instead.
|
|
923
|
+
*/
|
|
924
|
+
get web3() {
|
|
925
|
+
if (!this.hasSolanaKeypair)
|
|
926
|
+
return undefined;
|
|
927
|
+
const material = this._getOrDeriveSolanaMaterial();
|
|
928
|
+
return {
|
|
929
|
+
solana: {
|
|
930
|
+
publicKey: material.publicKey,
|
|
931
|
+
secretKey: material.secretKey,
|
|
932
|
+
address: solanaAddressFromPublicKey(material.publicKey),
|
|
933
|
+
getSolanaKeypair: () => toSolanaKeyPairSigner(material),
|
|
934
|
+
getSolanaAddress: () => toSolanaAddress(material),
|
|
935
|
+
sign: (message) => signWithSolanaMaterial(material, message),
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* @experimental True if this MajikKey can currently produce a Solana
|
|
941
|
+
* keypair (i.e. it's unlocked and has an Ed25519 signing key).
|
|
942
|
+
*/
|
|
943
|
+
get hasSolanaKeypair() {
|
|
944
|
+
return this.isUnlocked && this._edSecretKey !== undefined;
|
|
945
|
+
}
|
|
946
|
+
_getOrDeriveSolanaMaterial() {
|
|
947
|
+
if (!this._edSecretKey)
|
|
948
|
+
throw new MajikKeyError("No Ed25519 secret key — MajikKey must be unlocked and have signing keys.");
|
|
949
|
+
if (!this._solanaKeypairMaterial) {
|
|
950
|
+
this._solanaKeypairMaterial = deriveSolanaKeypairFromEdSecretKey(this._edSecretKey);
|
|
951
|
+
}
|
|
952
|
+
return this._solanaKeypairMaterial;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* @experimental Raw Solana keypair material (public/secret key bytes).
|
|
956
|
+
* Pass `{ reuseMessageKey: true }` to reuse the MajikKey's message signing
|
|
957
|
+
* Ed25519 key directly instead of the domain-separated derivation.
|
|
958
|
+
*/
|
|
959
|
+
getSolanaKeypairMaterial(options) {
|
|
960
|
+
if (this.isLocked)
|
|
961
|
+
throw new MajikKeyError("MajikKey is locked. Call unlock() first.");
|
|
962
|
+
if (!this._edSecretKey)
|
|
963
|
+
throw new MajikKeyError("No Ed25519 secret key — re-import via importFromMnemonicBackup() first.");
|
|
964
|
+
if (options?.reuseMessageKey) {
|
|
965
|
+
return solanaMaterialFromEd25519SecretKey(this._edSecretKey);
|
|
966
|
+
}
|
|
967
|
+
return this._getOrDeriveSolanaMaterial();
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* @experimental Real @solana/kit Keypair instance. Lazily loads
|
|
971
|
+
* @solana/kit — throws a MajikKeyError with install instructions if
|
|
972
|
+
* it isn't present in the consuming project.
|
|
973
|
+
*/
|
|
974
|
+
async getSolanaKeypair(options) {
|
|
975
|
+
return toSolanaKeyPairSigner(this.getSolanaKeypairMaterial(options));
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* @experimental Base58 Solana address. Does NOT require @solana/kit.
|
|
979
|
+
*/
|
|
980
|
+
getSolanaAddress(options) {
|
|
981
|
+
return solanaAddressFromPublicKey(this.getSolanaKeypairMaterial(options).publicKey);
|
|
982
|
+
}
|
|
834
983
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@majikah/majik-key",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "A post-quantum ready seed phrase account library for the Majikah ecosystem. Manages deterministic X25519 and ML-KEM-768 identities with Argon2id protection and seamless legacy account migration.",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.12",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|
|
@@ -60,14 +60,23 @@
|
|
|
60
60
|
"@stablelib/pbkdf2": "^2.0.1",
|
|
61
61
|
"@stablelib/sha256": "^2.0.1",
|
|
62
62
|
"@stablelib/x25519": "^2.0.1",
|
|
63
|
-
"@thezelijah/majik-user": "^1.0.
|
|
63
|
+
"@thezelijah/majik-user": "^1.0.9",
|
|
64
64
|
"ed2curve": "^0.3.0",
|
|
65
65
|
"hash-wasm": "^4.12.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
+
"@solana/kit": "^7.0.0",
|
|
68
69
|
"@types/ed2curve": "^0.2.4",
|
|
69
|
-
"@types/node": "^26.
|
|
70
|
+
"@types/node": "^26.1.0",
|
|
70
71
|
"typescript": "^6.0.3",
|
|
71
72
|
"vitest": "^4.1.9"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"@solana/kit": "^7.0.0"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"@solana/kit": {
|
|
79
|
+
"optional": true
|
|
80
|
+
}
|
|
72
81
|
}
|
|
73
82
|
}
|