@majikah/majik-key 0.7.0 → 1.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.
Files changed (55) hide show
  1. package/README.md +566 -175
  2. package/dist/core/backup/index.d.ts +4 -4
  3. package/dist/core/backup/index.js +3 -3
  4. package/dist/core/backup/majik-key-backup.d.ts +3 -3
  5. package/dist/core/backup/majik-key-backup.js +5 -5
  6. package/dist/core/backup/types.d.ts +1 -1
  7. package/dist/core/backup/utils.js +1 -1
  8. package/dist/core/backup/validator.d.ts +1 -1
  9. package/dist/core/backup/validator.js +1 -1
  10. package/dist/core/crypto/constants.d.ts +35 -11
  11. package/dist/core/crypto/constants.js +33 -11
  12. package/dist/core/crypto/crypto-provider.js +2 -2
  13. package/dist/core/crypto/encryption-engine.d.ts +7 -16
  14. package/dist/core/crypto/encryption-engine.js +27 -62
  15. package/dist/core/database/system/identity.d.ts +2 -2
  16. package/dist/core/database/system/identity.js +1 -1
  17. package/dist/core/keys/hkdf-recipe.d.ts +4 -0
  18. package/dist/core/keys/hkdf-recipe.js +26 -0
  19. package/dist/core/keys/key-id.d.ts +50 -0
  20. package/dist/core/keys/key-id.js +64 -0
  21. package/dist/core/keys/key-impls.d.ts +12 -0
  22. package/dist/core/keys/key-impls.js +163 -0
  23. package/dist/core/keys/key-store.d.ts +72 -0
  24. package/dist/core/keys/key-store.js +264 -0
  25. package/dist/core/keys/keypair-handle.d.ts +36 -0
  26. package/dist/core/keys/keypair-handle.js +43 -0
  27. package/dist/core/keys/registry.d.ts +16 -0
  28. package/dist/core/keys/registry.js +144 -0
  29. package/dist/core/keys/types.d.ts +47 -0
  30. package/dist/core/keys/types.js +1 -0
  31. package/dist/core/types.d.ts +12 -1
  32. package/dist/core/utils.d.ts +1 -1
  33. package/dist/core/utils.js +1 -1
  34. package/dist/core/validator.d.ts +1 -1
  35. package/dist/core/validator.js +1 -1
  36. package/dist/core/web3/bitcoin/bitcoin.d.ts +1 -1
  37. package/dist/core/web3/bitcoin/bitcoin.js +3 -3
  38. package/dist/core/web3/bitcoin/types.d.ts +1 -1
  39. package/dist/core/web3/ethereum/constants.d.ts +1 -0
  40. package/dist/core/web3/ethereum/constants.js +4 -0
  41. package/dist/core/web3/ethereum/ethereum.d.ts +21 -0
  42. package/dist/core/web3/ethereum/ethereum.js +91 -0
  43. package/dist/core/web3/ethereum/types.d.ts +32 -0
  44. package/dist/core/web3/ethereum/types.js +1 -0
  45. package/dist/core/web3/index.d.ts +10 -5
  46. package/dist/core/web3/index.js +7 -2
  47. package/dist/core/web3/solana/solana.d.ts +2 -2
  48. package/dist/core/web3/solana/solana.js +4 -4
  49. package/dist/core/web3/solana/types.d.ts +1 -1
  50. package/dist/core/web3/types.d.ts +4 -2
  51. package/dist/index.d.ts +16 -6
  52. package/dist/index.js +14 -5
  53. package/dist/majik-key.d.ts +179 -281
  54. package/dist/majik-key.js +622 -726
  55. package/package.json +20 -5
@@ -0,0 +1,144 @@
1
+ /**
2
+ * registry.ts — Phase 1 skeleton: DEFINITIONS ONLY (no derive/encrypt yet).
3
+ * Phase 2 attaches derive()/publicFromSecret() implementations to each entry.
4
+ */
5
+ import { MajikKeyError } from "../error.js";
6
+ import { CORE_KEYS, KeyId, keyFamilyOf } from "./key-id.js";
7
+ const hkdf = (id) => ({
8
+ scheme: "hkdf-sha512-v1",
9
+ version: 1,
10
+ info: `majik/v1/${id}`,
11
+ });
12
+ const legacy = (note) => ({
13
+ scheme: "legacy-v1",
14
+ version: 1,
15
+ note,
16
+ });
17
+ function def(id, purpose, standard, derivation, opts = {}) {
18
+ return {
19
+ id,
20
+ family: keyFamilyOf(id),
21
+ purpose,
22
+ kind: opts.kind ?? "stored",
23
+ status: opts.status ?? "stable",
24
+ implemented: opts.implemented ?? false,
25
+ standard,
26
+ derivation,
27
+ derivedFrom: opts.derivedFrom,
28
+ note: opts.note,
29
+ };
30
+ }
31
+ const SLH = (id, variant) => def(id, "signature", `FIPS 205 (${variant})`, hkdf(id), {
32
+ implemented: true,
33
+ });
34
+ export const KEY_ALGORITHMS = Object.freeze({
35
+ // ── classic ── (legacy recipes are frozen; see vectors/legacy-v1.vectors.json)
36
+ [KeyId.X25519]: def(KeyId.X25519, "key-agreement", "RFC 7748", {
37
+ scheme: "ed2curve",
38
+ version: 1,
39
+ note: "ed2curve(classic:ed25519); account id/fingerprint anchor",
40
+ }, { implemented: true }),
41
+ [KeyId.ED25519]: def(KeyId.ED25519, "signature", "RFC 8032", legacy("BIP-39 seed[0..32]"), { implemented: true }),
42
+ // ── pq KEM ──
43
+ [KeyId.ML_KEM_512]: def(KeyId.ML_KEM_512, "kem", "FIPS 203", hkdf(KeyId.ML_KEM_512), { implemented: true }),
44
+ [KeyId.ML_KEM_768]: def(KeyId.ML_KEM_768, "kem", "FIPS 203", legacy("full 64-byte BIP-39 seed"), { implemented: true }),
45
+ [KeyId.ML_KEM_1024]: def(KeyId.ML_KEM_1024, "kem", "FIPS 203", hkdf(KeyId.ML_KEM_1024), { implemented: true }),
46
+ [KeyId.HQC_128]: def(KeyId.HQC_128, "kem", "NIST HQC (draft)", hkdf(KeyId.HQC_128), {
47
+ status: "reserved",
48
+ note: "NIST backup KEM; standard not final and no vetted JS implementation in the current dependency set.",
49
+ }),
50
+ [KeyId.HQC_192]: def(KeyId.HQC_192, "kem", "NIST HQC (draft)", hkdf(KeyId.HQC_192), { status: "reserved", note: "See pq:hqc-128." }),
51
+ [KeyId.HQC_256]: def(KeyId.HQC_256, "kem", "NIST HQC (draft)", hkdf(KeyId.HQC_256), { status: "reserved", note: "See pq:hqc-128." }),
52
+ // ── pq signatures ──
53
+ [KeyId.ML_DSA_44]: def(KeyId.ML_DSA_44, "signature", "FIPS 204", hkdf(KeyId.ML_DSA_44), { implemented: true }),
54
+ [KeyId.ML_DSA_65]: def(KeyId.ML_DSA_65, "signature", "FIPS 204", hkdf(KeyId.ML_DSA_65), { implemented: true }),
55
+ [KeyId.ML_DSA_87]: def(KeyId.ML_DSA_87, "signature", "FIPS 204", legacy('sha256(seed64 || "MajikSignatureSeedDSA")'), { implemented: true }),
56
+ [KeyId.SLH_DSA_SHA2_128S]: SLH(KeyId.SLH_DSA_SHA2_128S, "SHA2-128s"),
57
+ [KeyId.SLH_DSA_SHA2_128F]: SLH(KeyId.SLH_DSA_SHA2_128F, "SHA2-128f"),
58
+ [KeyId.SLH_DSA_SHA2_192S]: SLH(KeyId.SLH_DSA_SHA2_192S, "SHA2-192s"),
59
+ [KeyId.SLH_DSA_SHA2_192F]: SLH(KeyId.SLH_DSA_SHA2_192F, "SHA2-192f"),
60
+ [KeyId.SLH_DSA_SHA2_256S]: SLH(KeyId.SLH_DSA_SHA2_256S, "SHA2-256s"),
61
+ [KeyId.SLH_DSA_SHA2_256F]: SLH(KeyId.SLH_DSA_SHA2_256F, "SHA2-256f"),
62
+ [KeyId.SLH_DSA_SHAKE_128S]: SLH(KeyId.SLH_DSA_SHAKE_128S, "SHAKE-128s"),
63
+ [KeyId.SLH_DSA_SHAKE_128F]: SLH(KeyId.SLH_DSA_SHAKE_128F, "SHAKE-128f"),
64
+ [KeyId.SLH_DSA_SHAKE_192S]: SLH(KeyId.SLH_DSA_SHAKE_192S, "SHAKE-192s"),
65
+ [KeyId.SLH_DSA_SHAKE_192F]: SLH(KeyId.SLH_DSA_SHAKE_192F, "SHAKE-192f"),
66
+ [KeyId.SLH_DSA_SHAKE_256S]: SLH(KeyId.SLH_DSA_SHAKE_256S, "SHAKE-256s"),
67
+ [KeyId.SLH_DSA_SHAKE_256F]: SLH(KeyId.SLH_DSA_SHAKE_256F, "SHAKE-256f"),
68
+ [KeyId.FALCON_512]: def(KeyId.FALCON_512, "signature", "Falcon (NIST PQC Round 3)", hkdf(KeyId.FALCON_512), {
69
+ status: "experimental",
70
+ implemented: true,
71
+ note: "Round 3 Falcon, NOT FIPS 206. FN-DSA is expected to be incompatible; it will get its own ids.",
72
+ }),
73
+ [KeyId.FALCON_1024]: def(KeyId.FALCON_1024, "signature", "Falcon (NIST PQC Round 3)", hkdf(KeyId.FALCON_1024), { status: "experimental", implemented: true, note: "See pq:falcon-512." }),
74
+ [KeyId.FN_DSA_512]: def(KeyId.FN_DSA_512, "signature", "FIPS 206 (draft)", hkdf(KeyId.FN_DSA_512), {
75
+ status: "reserved",
76
+ note: "Reserved until FIPS 206 is final and an implementation tracks it.",
77
+ }),
78
+ [KeyId.FN_DSA_1024]: def(KeyId.FN_DSA_1024, "signature", "FIPS 206 (draft)", hkdf(KeyId.FN_DSA_1024), { status: "reserved", note: "See pq:fn-dsa-512." }),
79
+ [KeyId.LMS]: def(KeyId.LMS, "signature", "NIST SP 800-208 / RFC 8554", hkdf(KeyId.LMS), {
80
+ status: "unsupported",
81
+ note: "Stateful: every signature consumes a one-time key index. Mnemonic recovery, backups and multi-device use " +
82
+ "all reset that state and make one-time-key reuse (total forgery) likely. Not offered as a stored signing key.",
83
+ }),
84
+ // ── web3 ──
85
+ [KeyId.BTC]: def(KeyId.BTC, "wallet", "BIP-32 / BIP-84", {
86
+ scheme: "bip32",
87
+ version: 1,
88
+ path: "m/84'/1971'/0'/0/0",
89
+ note: "Majik domain-separated path (legacy default)",
90
+ }, { implemented: true }),
91
+ [KeyId.ETH]: def(KeyId.ETH, "wallet", "BIP-32 / BIP-44 (SLIP-44 coin 60)", {
92
+ scheme: "bip32",
93
+ version: 1,
94
+ path: "m/44'/60'/0'/0/0",
95
+ note: "Standard path: MetaMask-compatible",
96
+ }, { implemented: true }),
97
+ [KeyId.SOL]: def(KeyId.SOL, "wallet", "Ed25519 (Solana)", {
98
+ scheme: "derived-view",
99
+ version: 1,
100
+ note: 'sha256(edSeed || "MajikKeySolanaSeed")',
101
+ }, { kind: "derived", derivedFrom: KeyId.ED25519, implemented: true }),
102
+ });
103
+ const ORDER = Object.keys(KEY_ALGORITHMS);
104
+ export function getAlgorithm(id) {
105
+ return KEY_ALGORITHMS[id];
106
+ }
107
+ /** Everything the registry knows, in canonical order (includes reserved/unsupported). */
108
+ export function knownKeyIds(family) {
109
+ return family ? ORDER.filter((id) => keyFamilyOf(id) === family) : [...ORDER];
110
+ }
111
+ /** Ids that can actually be enabled today. */
112
+ export function enableableKeyIds() {
113
+ return ORDER.filter((id) => {
114
+ const d = KEY_ALGORITHMS[id];
115
+ return (d.implemented && (d.status === "stable" || d.status === "experimental"));
116
+ });
117
+ }
118
+ /**
119
+ * Validate a caller's `keys` option and return the STORED key ids to create:
120
+ * CORE_KEYS ∪ requested, de-duplicated, canonical order. Derived views
121
+ * (e.g. web3:sol) are accepted as no-ops because they require a stored key
122
+ * that is already in the core set.
123
+ */
124
+ export function resolveRequestedKeys(requested = []) {
125
+ const stored = new Set(CORE_KEYS);
126
+ for (const raw of requested) {
127
+ const d = getAlgorithm(raw);
128
+ if (!d)
129
+ throw new MajikKeyError(`Unknown key algorithm "${raw}"`);
130
+ if (d.status === "unsupported")
131
+ throw new MajikKeyError(`"${raw}" is not supported: ${d.note}`);
132
+ if (d.status === "reserved")
133
+ throw new MajikKeyError(`"${raw}" is reserved and cannot be enabled yet: ${d.note}`);
134
+ if (!d.implemented)
135
+ throw new MajikKeyError(`"${raw}" is defined but not implemented in this version`);
136
+ if (d.kind === "derived") {
137
+ if (d.derivedFrom)
138
+ stored.add(d.derivedFrom);
139
+ continue;
140
+ }
141
+ stored.add(d.id);
142
+ }
143
+ return ORDER.filter((id) => stored.has(id));
144
+ }
@@ -0,0 +1,47 @@
1
+ import type { KeyFamily, KeyId } from "./key-id.js";
2
+ export type KeyPurpose = "kem" | "key-agreement" | "signature" | "wallet";
3
+ /**
4
+ * stable — standardized or production-grade, safe to enable
5
+ * experimental — works, but the spec/impl may change (ids stay, derivation may be versioned)
6
+ * reserved — id is claimed but cannot be enabled yet (no vetted impl / standard not final)
7
+ * unsupported — deliberately not offered (see `note`)
8
+ */
9
+ export type KeyStatus = "stable" | "experimental" | "reserved" | "unsupported";
10
+ export interface KeyDerivation {
11
+ /** "legacy-v1" (frozen pre-registry recipes) | "hkdf-sha512-v1" | "bip32" | "ed2curve" | "derived-view" */
12
+ scheme: string;
13
+ version: number;
14
+ /** HKDF info string, e.g. "majik/v1/pq:ml-kem-1024". */
15
+ info?: string;
16
+ /** BIP-32 path for web3 keys. */
17
+ path?: string;
18
+ /** Free-form human note (e.g. "ed2curve(classic:ed25519)"). */
19
+ note?: string;
20
+ }
21
+ /** One stored entry inside `MajikKeyJSON.keys`. No raw secret ever lives here. */
22
+ export interface KeyEntryJSON {
23
+ id: KeyId;
24
+ /** Base64 public key. */
25
+ publicKey: string;
26
+ /** Base64 AES-256-GCM (IV || ciphertext) under the account's passphrase-derived key. */
27
+ encryptedSecretKey?: string;
28
+ derivation: KeyDerivation;
29
+ createdAt?: string;
30
+ }
31
+ export interface KeyAlgorithmDefinition {
32
+ id: KeyId;
33
+ family: KeyFamily;
34
+ purpose: KeyPurpose;
35
+ /** "stored" = encrypted at rest in `keys`; "derived" = computed on demand from another key. */
36
+ kind: "stored" | "derived";
37
+ status: KeyStatus;
38
+ /** True once derive/encrypt/decrypt are wired up in the registry (phase 2+). */
39
+ implemented: boolean;
40
+ /** Standard / spec this follows. */
41
+ standard: string;
42
+ /** Recipe used when this key is derived for NEW accounts. Legacy accounts keep "legacy-v1". */
43
+ derivation: KeyDerivation;
44
+ /** For derived views: the stored key it is computed from. */
45
+ derivedFrom?: KeyId;
46
+ note?: string;
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,5 @@
1
- import { MnemonicLanguage } from "./crypto/wordlist";
1
+ import { MnemonicLanguage } from "./crypto/wordlist.js";
2
+ import { KeyEntryJSON } from "./keys/types.js";
2
3
  /** ISO 8601 timestamp string, e.g. `"2026-07-11T00:00:00.000Z"`. */
3
4
  export type ISODateString = string;
4
5
  /** Base64-encoded public key material. Safe to store, log, or transmit. */
@@ -69,6 +70,10 @@ export interface MajikKeyJSON {
69
70
  encryptedBtcSecretKey?: string;
70
71
  /** BIP-39 wordlist language the original mnemonic was generated/validated against. Defaults to `"en"`. */
71
72
  mnemonicLanguage?: MnemonicLanguage;
73
+ /** Registry schema version. Absent on pre-registry JSON. */
74
+ keysVersion?: number;
75
+ /** Every keypair on the account (public key + passphrase-encrypted secret + derivation). */
76
+ keys?: KeyEntryJSON[];
72
77
  }
73
78
  /**
74
79
  * ⚠️ DANGEROUS. Every field below is a *raw, unencrypted* private key,
@@ -93,6 +98,8 @@ export interface MajikKeyDangerousJSON extends MajikKeyJSON {
93
98
  mlDsaSecretKeyBase64: string;
94
99
  /** @experimental ⚠️ Raw Bitcoin private key, base64. Unencrypted. */
95
100
  btcSecretKeyBase64?: string;
101
+ /** ⚠️ Raw secret of every stored key, base64, keyed by namespaced id. Unencrypted. */
102
+ secretKeys?: Record<string, string>;
96
103
  }
97
104
  /**
98
105
  * Lightweight, non-secret summary of a MajikKey — useful for account
@@ -119,8 +126,12 @@ export interface MajikKeyMetadata {
119
126
  hasBitcoin?: boolean;
120
127
  /** @experimental `true` if this account can derive a Solana keypair (i.e. has an Ed25519 signing key and is unlocked). */
121
128
  hasSolana?: boolean;
129
+ /** @experimental `true` if this account has a stored Ethereum keypair. */
130
+ hasEthereum?: boolean;
122
131
  };
123
132
  mnemonicLanguage?: MnemonicLanguage;
133
+ /** Namespaced ids of every key available on the account. */
134
+ keys?: string[];
124
135
  }
125
136
  /**
126
137
  * Portable seed export — the format behind `toMnemonicJSON()` /
@@ -1,4 +1,4 @@
1
- import { MnemonicJSON } from "./types";
1
+ import { MnemonicJSON } from "./types.js";
2
2
  export declare function keyToBase64(key: CryptoKey | {
3
3
  raw: Uint8Array;
4
4
  }): Promise<string>;
@@ -1,7 +1,7 @@
1
1
  /* ================================
2
2
  * Utilities
3
3
  * ================================ */
4
- import { KEY_ALGO } from "./crypto/constants";
4
+ import { KEY_ALGO } from "./crypto/constants.js";
5
5
  export async function keyToBase64(key) {
6
6
  const anyKey = key;
7
7
  if (anyKey && anyKey.raw instanceof Uint8Array) {
@@ -1,4 +1,4 @@
1
- import { MajikKeyJSON } from "./types";
1
+ import { MajikKeyJSON } from "./types.js";
2
2
  export declare class MajikKeyValidator {
3
3
  static validateMnemonic(mnemonic: string): void;
4
4
  static validatePassphrase(passphrase: string, fieldName?: string): void;
@@ -1,7 +1,7 @@
1
1
  /* -------------------------------
2
2
  * Validators
3
3
  * ------------------------------- */
4
- import { MajikKeyError } from "./error";
4
+ import { MajikKeyError } from "./error.js";
5
5
  export class MajikKeyValidator {
6
6
  static validateMnemonic(mnemonic) {
7
7
  if (!mnemonic || typeof mnemonic !== "string") {
@@ -23,7 +23,7 @@
23
23
  * installed, we throw a clear, actionable MajikKeyError instead of
24
24
  * failing module load.
25
25
  */
26
- import { BitcoinRawPublicKey } from "../../types";
26
+ import { BitcoinRawPublicKey } from "../../types.js";
27
27
  export interface BitcoinKeypairMaterial {
28
28
  /** 32-byte secp256k1 private key. */
29
29
  privateKey: Uint8Array;
@@ -25,10 +25,10 @@
25
25
  */
26
26
  import { HDKey } from "@scure/bip32";
27
27
  import { schnorr, secp256k1 } from "@noble/curves/secp256k1.js";
28
- import { MAJIK_BITCOIN_STANDARD_PATH, MAJIK_BITCOIN_DOMAIN_PATH, } from "./constants";
28
+ import { MAJIK_BITCOIN_STANDARD_PATH, MAJIK_BITCOIN_DOMAIN_PATH, } from "./constants.js";
29
29
  import { hash } from "@stablelib/sha256";
30
- import { base58Encode } from "../utils";
31
- import { MajikKeyError } from "../../error";
30
+ import { base58Encode } from "../utils.js";
31
+ import { MajikKeyError } from "../../error.js";
32
32
  import { randomBytes } from "@noble/hashes/utils.js";
33
33
  // ─── Derivation ─────────────────────────────────────────────────────────────
34
34
  /**
@@ -1,4 +1,4 @@
1
- import { BitcoinRawPublicKey } from "../../types";
1
+ import { BitcoinRawPublicKey } from "../../types.js";
2
2
  /**
3
3
  * @experimental By default this is Majik's DOMAIN-SEPARATED Bitcoin key
4
4
  * (derived via `MAJIK_BITCOIN_DOMAIN_PATH`) — deterministic and fully
@@ -0,0 +1 @@
1
+ export declare const MAJIK_ETHEREUM_STANDARD_PATH = "m/44'/60'/0'/0/0";
@@ -0,0 +1,4 @@
1
+ // SLIP-44 coin type 60 = Ethereum. The path MetaMask, Ledger, Trezor, Rabby, etc.
2
+ // derive by default (account 0, address index 0) — so the address from a
3
+ // MajikKey is the SAME address those wallets show for the same mnemonic.
4
+ export const MAJIK_ETHEREUM_STANDARD_PATH = "m/44'/60'/0'/0/0";
@@ -0,0 +1,21 @@
1
+ import type { EthereumSignature } from "./types.js";
2
+ export interface EthereumKeypairMaterial {
3
+ /** 32-byte secp256k1 private key. */
4
+ privateKey: Uint8Array;
5
+ /** 33-byte compressed secp256k1 public key. */
6
+ publicKey: Uint8Array;
7
+ }
8
+ /** Uncompressed (65-byte, 0x04-prefixed) form of a compressed or uncompressed public key. */
9
+ export declare function ethereumUncompressedPublicKey(publicKey: Uint8Array): Uint8Array;
10
+ /** EIP-55 mixed-case checksum of a lowercase 40-char hex address (no 0x). */
11
+ export declare function toChecksumAddress(lowerHex: string): string;
12
+ /** EIP-55 address: last 20 bytes of keccak256(uncompressed pubkey without the 0x04 prefix). */
13
+ export declare function ethereumAddressFromPublicKey(publicKey: Uint8Array): string;
14
+ export declare function toEthereumPrivateKeyHex(material: EthereumKeypairMaterial): string;
15
+ /** keccak256("\x19Ethereum Signed Message:\n" + byteLength + message) — EIP-191 version 0x45. */
16
+ export declare function hashEthereumMessage(message: string | Uint8Array): Uint8Array;
17
+ /** Sign a 32-byte hash. Deterministic (RFC 6979), low-s, with recovery id. */
18
+ export declare function signEthereumHash(material: EthereumKeypairMaterial, hash32: Uint8Array): EthereumSignature;
19
+ export declare function signEthereumMessage(material: EthereumKeypairMaterial, message: string | Uint8Array): EthereumSignature;
20
+ /** Recover the signer's EIP-55 address from a hash and signature (ecrecover). */
21
+ export declare function recoverEthereumAddress(hash32: Uint8Array, sig: Pick<EthereumSignature, "r" | "s" | "recovery">): string;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * ethereum.ts
3
+ *
4
+ * ⚠️ EXPERIMENTAL — Ethereum keypair utilities for MajikKey.
5
+ *
6
+ * Design (mirrors bitcoin.ts):
7
+ * - Real BIP-32/BIP-44 derivation off the raw 64-byte BIP-39 seed at the
8
+ * STANDARD path m/44'/60'/0'/0/0 (see ./constants), so the account is
9
+ * recoverable in any Ethereum wallet from the mnemonic alone.
10
+ * - Everything here is pure @noble — address derivation (keccak-256 +
11
+ * EIP-55), EIP-191 message hashing, recoverable signing — so NO optional
12
+ * peer dependency is needed for the common operations. Transaction
13
+ * building / EIP-712 typed data should lazy-load viem or ethers
14
+ * (not included here).
15
+ */
16
+ import { secp256k1 } from "@noble/curves/secp256k1.js";
17
+ import { keccak_256 } from "@noble/hashes/sha3.js";
18
+ import { bytesToHex } from "@noble/hashes/utils.js";
19
+ import { MajikKeyError } from "../../error.js";
20
+ const enc = (s) => new TextEncoder().encode(s);
21
+ const hex0x = (u) => "0x" + bytesToHex(u);
22
+ /** Uncompressed (65-byte, 0x04-prefixed) form of a compressed or uncompressed public key. */
23
+ export function ethereumUncompressedPublicKey(publicKey) {
24
+ if (publicKey.length === 65 && publicKey[0] === 0x04)
25
+ return publicKey;
26
+ if (publicKey.length !== 33)
27
+ throw new MajikKeyError(`Expected a 33-byte compressed secp256k1 public key, got ${publicKey.length} bytes`);
28
+ return secp256k1.Point.fromBytes(publicKey).toBytes(false);
29
+ }
30
+ /** EIP-55 mixed-case checksum of a lowercase 40-char hex address (no 0x). */
31
+ export function toChecksumAddress(lowerHex) {
32
+ const h = bytesToHex(keccak_256(enc(lowerHex)));
33
+ let out = "0x";
34
+ for (let i = 0; i < lowerHex.length; i++)
35
+ out += parseInt(h[i], 16) >= 8 ? lowerHex[i].toUpperCase() : lowerHex[i];
36
+ return out;
37
+ }
38
+ /** EIP-55 address: last 20 bytes of keccak256(uncompressed pubkey without the 0x04 prefix). */
39
+ export function ethereumAddressFromPublicKey(publicKey) {
40
+ const uncompressed = ethereumUncompressedPublicKey(publicKey);
41
+ return toChecksumAddress(bytesToHex(keccak_256(uncompressed.slice(1)).slice(-20)));
42
+ }
43
+ export function toEthereumPrivateKeyHex(material) {
44
+ return hex0x(material.privateKey);
45
+ }
46
+ /** keccak256("\x19Ethereum Signed Message:\n" + byteLength + message) — EIP-191 version 0x45. */
47
+ export function hashEthereumMessage(message) {
48
+ const body = typeof message === "string" ? enc(message) : message;
49
+ const prefix = enc(`\x19Ethereum Signed Message:\n${body.length}`);
50
+ const buf = new Uint8Array(prefix.length + body.length);
51
+ buf.set(prefix, 0);
52
+ buf.set(body, prefix.length);
53
+ return keccak_256(buf);
54
+ }
55
+ /** Sign a 32-byte hash. Deterministic (RFC 6979), low-s, with recovery id. */
56
+ export function signEthereumHash(material, hash32) {
57
+ if (hash32.length !== 32)
58
+ throw new MajikKeyError(`Expected a 32-byte hash, got ${hash32.length} bytes`);
59
+ // noble v2 "recovered" format = [recovery(1) | r(32) | s(32)]
60
+ const sig = secp256k1.sign(hash32, material.privateKey, {
61
+ prehash: false,
62
+ lowS: true,
63
+ format: "recovered",
64
+ });
65
+ const recovery = sig[0];
66
+ const r = sig.slice(1, 33);
67
+ const s = sig.slice(33, 65);
68
+ const v = (27 + recovery);
69
+ return {
70
+ r: hex0x(r),
71
+ s: hex0x(s),
72
+ v,
73
+ recovery,
74
+ serialized: hex0x(r) + bytesToHex(s) + v.toString(16),
75
+ };
76
+ }
77
+ export function signEthereumMessage(material, message) {
78
+ return signEthereumHash(material, hashEthereumMessage(message));
79
+ }
80
+ /** Recover the signer's EIP-55 address from a hash and signature (ecrecover). */
81
+ export function recoverEthereumAddress(hash32, sig) {
82
+ const fromHex = (h) => Uint8Array.from(Buffer.from(h.slice(2), "hex"));
83
+ const raw = new Uint8Array(65);
84
+ raw[0] = sig.recovery;
85
+ raw.set(fromHex(sig.r), 1);
86
+ raw.set(fromHex(sig.s), 33);
87
+ const pub = secp256k1.recoverPublicKey(raw, hash32, {
88
+ prehash: false,
89
+ });
90
+ return ethereumAddressFromPublicKey(pub);
91
+ }
@@ -0,0 +1,32 @@
1
+ /** @experimental */
2
+ export interface EthereumSignature {
3
+ /** 0x-prefixed 32-byte hex. */
4
+ r: string;
5
+ /** 0x-prefixed 32-byte hex (low-s normalized, EIP-2). */
6
+ s: string;
7
+ /** 27 | 28 (Ethereum "v"). */
8
+ v: 27 | 28;
9
+ /** Raw recovery id, 0 | 1. */
10
+ recovery: 0 | 1;
11
+ /** 0x + r || s || v (65 bytes) — what personal_sign / ecrecover consumers expect. */
12
+ serialized: string;
13
+ }
14
+ /**
15
+ * @experimental Ethereum account derived at the STANDARD path
16
+ * (`m/44'/60'/0'/0/0`): the same address MetaMask and hardware wallets show
17
+ * for this mnemonic. Anyone holding the mnemonic controls the funds.
18
+ */
19
+ export interface MajikKeyEthereumNamespace {
20
+ /** 33-byte compressed secp256k1 public key. */
21
+ readonly publicKey: Uint8Array;
22
+ /** 32-byte secp256k1 private key. Handle with the same care as any private key. */
23
+ readonly privateKey: Uint8Array;
24
+ /** EIP-55 checksummed address. Pure computation — needs no extra dependency. */
25
+ readonly address: string;
26
+ /** 0x-prefixed private key hex — pastes directly into any wallet's "import private key". */
27
+ getPrivateKeyHex(): string;
28
+ /** Sign a 32-byte hash (e.g. a tx hash or EIP-712 digest). Returns r/s/v. */
29
+ signHash(hash32: Uint8Array): EthereumSignature;
30
+ /** EIP-191 `personal_sign`: signs keccak256("\x19Ethereum Signed Message:\n" + len + message). */
31
+ signMessage(message: string | Uint8Array): EthereumSignature;
32
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,10 @@
1
- export * from "./bitcoin/bitcoin";
2
- export * from "./solana/solana";
3
- export type * from "./types";
4
- export type * from "./bitcoin/types";
5
- export type * from "./solana/types";
1
+ export * from "./bitcoin/bitcoin.js";
2
+ export * from "./solana/solana.js";
3
+ export * from "./ethereum/ethereum.js";
4
+ export * from "./bitcoin/constants.js";
5
+ export * from "./solana/constants.js";
6
+ export * from "./ethereum/constants.js";
7
+ export type * from "./types.js";
8
+ export type * from "./bitcoin/types.js";
9
+ export type * from "./solana/types.js";
10
+ export type * from "./ethereum/types.js";
@@ -1,2 +1,7 @@
1
- export * from "./bitcoin/bitcoin";
2
- export * from "./solana/solana";
1
+ // src/core/web3/index.ts
2
+ export * from "./bitcoin/bitcoin.js";
3
+ export * from "./solana/solana.js";
4
+ export * from "./ethereum/ethereum.js";
5
+ export * from "./bitcoin/constants.js";
6
+ export * from "./solana/constants.js";
7
+ export * from "./ethereum/constants.js";
@@ -25,7 +25,7 @@
25
25
  * signing Ed25519 keypair AS-IS. Simpler, but means the same private
26
26
  * key secures two different protocols. Opt-in only.
27
27
  */
28
- import { ED25519RawPublicKey } from "../../types";
28
+ import { ED25519RawPublicKey } from "../../types.js";
29
29
  export interface SolanaKeypairMaterial {
30
30
  /** 32-byte Ed25519 / Solana public key. */
31
31
  publicKey: ED25519RawPublicKey;
@@ -37,7 +37,7 @@ export interface SolanaKeypairMaterial {
37
37
  * message-signing Ed25519 key, but fully deterministic from it (and
38
38
  * therefore ultimately from the mnemonic).
39
39
  *
40
- * seed' = SHA256(edSecretKey[0..32] || "MajikMessageSolanaSeed")
40
+ * seed' = SHA256(edSecretKey[0..32] || "MajikKeySolanaSeed")
41
41
  */
42
42
  export declare function deriveSolanaKeypairFromEdSecretKey(edSecretKey: Uint8Array): SolanaKeypairMaterial;
43
43
  /**
@@ -27,9 +27,9 @@
27
27
  */
28
28
  import * as ed25519 from "@stablelib/ed25519";
29
29
  import { hash } from "@stablelib/sha256";
30
- import { MAJIK_SOLANA_SEED } from "./constants";
31
- import { MajikKeyError } from "../../error";
32
- import { base58Encode } from "../utils";
30
+ import { MAJIK_SOLANA_SEED } from "./constants.js";
31
+ import { MajikKeyError } from "../../error.js";
32
+ import { base58Encode } from "../utils.js";
33
33
  const ED25519_SECRET_KEY_LENGTH = 64;
34
34
  const ED25519_SEED_LENGTH = 32;
35
35
  // ─── Derivation ─────────────────────────────────────────────────────────────
@@ -38,7 +38,7 @@ const ED25519_SEED_LENGTH = 32;
38
38
  * message-signing Ed25519 key, but fully deterministic from it (and
39
39
  * therefore ultimately from the mnemonic).
40
40
  *
41
- * seed' = SHA256(edSecretKey[0..32] || "MajikMessageSolanaSeed")
41
+ * seed' = SHA256(edSecretKey[0..32] || "MajikKeySolanaSeed")
42
42
  */
43
43
  export function deriveSolanaKeypairFromEdSecretKey(edSecretKey) {
44
44
  if (edSecretKey.length !== ED25519_SECRET_KEY_LENGTH) {
@@ -1,4 +1,4 @@
1
- import { ED25519RawPublicKey } from "../../types";
1
+ import { ED25519RawPublicKey } from "../../types.js";
2
2
  /**
3
3
  * @experimental Web3 / blockchain integrations are experimental. This
4
4
  * namespace's shape may change without a major version bump.
@@ -1,7 +1,9 @@
1
- import { MajikKeyBitcoinNamespace } from "./bitcoin/types";
2
- import { MajikKeySolanaNamespace } from "./solana/types";
1
+ import type { MajikKeyBitcoinNamespace } from "./bitcoin/types.js";
2
+ import type { MajikKeyEthereumNamespace } from "./ethereum/types.js";
3
+ import type { MajikKeySolanaNamespace } from "./solana/types.js";
3
4
  /** @experimental */
4
5
  export interface MajikKeyWeb3Namespace {
5
6
  readonly solana: MajikKeySolanaNamespace;
6
7
  readonly bitcoin?: MajikKeyBitcoinNamespace;
8
+ readonly ethereum?: MajikKeyEthereumNamespace;
7
9
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,16 @@
1
- export * from "./majik-key";
2
- export type * from "./core/types";
3
- export * from "./core/error";
4
- export * from "./core/validator";
5
- export * from "./core/web3";
6
- export * from "./core/backup";
1
+ export * from "./majik-key.js";
2
+ export type * from "./core/types.js";
3
+ export * from "./core/error.js";
4
+ export * from "./core/validator.js";
5
+ export * from "./core/web3/index.js";
6
+ export * from "./core/backup/index.js";
7
+ export type * from "./core/keys/types.js";
8
+ export * from "./core/keys/hkdf-recipe.js";
9
+ export * from "./core/keys/key-id.js";
10
+ export * from "./core/keys/key-store.js";
11
+ export * from "./core/keys/key-impls.js";
12
+ export * from "./core/keys/keypair-handle.js";
13
+ export * from "./core/keys/registry.js";
14
+ export * from "./core/crypto/wordlist.js";
15
+ export * from "./core/crypto/constants.js";
16
+ export * from "./core/crypto/crypto-provider.js";
package/dist/index.js CHANGED
@@ -1,5 +1,14 @@
1
- export * from "./majik-key";
2
- export * from "./core/error";
3
- export * from "./core/validator";
4
- export * from "./core/web3";
5
- export * from "./core/backup";
1
+ export * from "./majik-key.js";
2
+ export * from "./core/error.js";
3
+ export * from "./core/validator.js";
4
+ export * from "./core/web3/index.js";
5
+ export * from "./core/backup/index.js";
6
+ export * from "./core/keys/hkdf-recipe.js";
7
+ export * from "./core/keys/key-id.js";
8
+ export * from "./core/keys/key-store.js";
9
+ export * from "./core/keys/key-impls.js";
10
+ export * from "./core/keys/keypair-handle.js";
11
+ export * from "./core/keys/registry.js";
12
+ export * from "./core/crypto/wordlist.js";
13
+ export * from "./core/crypto/constants.js";
14
+ export * from "./core/crypto/crypto-provider.js";