@gryt/crypto 0.3.0 → 0.4.0

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/index.d.ts CHANGED
@@ -42,4 +42,5 @@ export * from "./member-keys.js";
42
42
  export * from "./message-keys.js";
43
43
  export * from "./peer-keys.js";
44
44
  export * from "./scope.js";
45
+ export * from "./seed-words.js";
45
46
  export * from "./thumbprint.js";
package/dist/index.js CHANGED
@@ -45,4 +45,5 @@ export * from "./member-keys.js";
45
45
  export * from "./message-keys.js";
46
46
  export * from "./peer-keys.js";
47
47
  export * from "./scope.js";
48
+ export * from "./seed-words.js";
48
49
  export * from "./thumbprint.js";
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The identity seed as 24 words, and back.
3
+ *
4
+ * Both apps had this, and a phrase written down from one has to restore on the
5
+ * other or it is worthless. They agreed — same wordlist, same BIP-39 calls, the
6
+ * same 24 — but they agreed by two people keeping two files in step, which is
7
+ * the arrangement this package exists to end.
8
+ *
9
+ * The standard 2048-word list is 11 bits a word: 256 bits of seed plus 8 bits of
10
+ * checksum makes 264, which is 24 words exactly. The checksum lives inside the
11
+ * words rather than beside them, so a mistyped or reordered phrase is rejected
12
+ * instead of quietly producing a different identity — and since no two words in
13
+ * the list share their first four letters, a misread word usually is not a word
14
+ * at all and fails before the checksum is reached.
15
+ *
16
+ * Only the encoding is borrowed from wallets. The key-stretching step they do on
17
+ * top is not wanted here: these 256 bits are the seed already, not a passphrase
18
+ * to grind into one.
19
+ */
20
+ /** Length of the seed every local identity is calculated from. */
21
+ export declare const SEED_BYTES = 32;
22
+ /** How many words a backup is. Stated once so the message and the check agree. */
23
+ export declare const BACKUP_WORDS = 24;
24
+ /**
25
+ * Refuse a seed that cannot be one.
26
+ *
27
+ * The length check is the real one. The repeated-byte check is for a generator
28
+ * that has failed open and is handing back zeros, which is worth catching loudly
29
+ * rather than deriving a whole identity from.
30
+ */
31
+ export declare function assertUsableSeed(seed: Uint8Array): void;
32
+ export declare function seedToWords(seed: Uint8Array): string;
33
+ /**
34
+ * The messages here are read by somebody typing 24 words back in, so they say
35
+ * what is wrong rather than that something is.
36
+ */
37
+ export declare function wordsToSeed(phrase: string): Uint8Array;
@@ -0,0 +1,64 @@
1
+ import { entropyToMnemonic, mnemonicToEntropy, validateMnemonic, } from "@scure/bip39";
2
+ import { wordlist } from "@scure/bip39/wordlists/english.js";
3
+ /**
4
+ * The identity seed as 24 words, and back.
5
+ *
6
+ * Both apps had this, and a phrase written down from one has to restore on the
7
+ * other or it is worthless. They agreed — same wordlist, same BIP-39 calls, the
8
+ * same 24 — but they agreed by two people keeping two files in step, which is
9
+ * the arrangement this package exists to end.
10
+ *
11
+ * The standard 2048-word list is 11 bits a word: 256 bits of seed plus 8 bits of
12
+ * checksum makes 264, which is 24 words exactly. The checksum lives inside the
13
+ * words rather than beside them, so a mistyped or reordered phrase is rejected
14
+ * instead of quietly producing a different identity — and since no two words in
15
+ * the list share their first four letters, a misread word usually is not a word
16
+ * at all and fails before the checksum is reached.
17
+ *
18
+ * Only the encoding is borrowed from wallets. The key-stretching step they do on
19
+ * top is not wanted here: these 256 bits are the seed already, not a passphrase
20
+ * to grind into one.
21
+ */
22
+ /** Length of the seed every local identity is calculated from. */
23
+ export const SEED_BYTES = 32;
24
+ /** How many words a backup is. Stated once so the message and the check agree. */
25
+ export const BACKUP_WORDS = 24;
26
+ /**
27
+ * Refuse a seed that cannot be one.
28
+ *
29
+ * The length check is the real one. The repeated-byte check is for a generator
30
+ * that has failed open and is handing back zeros, which is worth catching loudly
31
+ * rather than deriving a whole identity from.
32
+ */
33
+ export function assertUsableSeed(seed) {
34
+ if (seed.length !== SEED_BYTES) {
35
+ throw new Error(`An identity seed is ${SEED_BYTES} bytes, not ${seed.length}.`);
36
+ }
37
+ const first = seed[0];
38
+ if (seed.every((b) => b === first)) {
39
+ throw new Error("Identity seed is a single repeated byte — the generator is broken.");
40
+ }
41
+ }
42
+ export function seedToWords(seed) {
43
+ assertUsableSeed(seed);
44
+ return entropyToMnemonic(seed, wordlist);
45
+ }
46
+ /**
47
+ * The messages here are read by somebody typing 24 words back in, so they say
48
+ * what is wrong rather than that something is.
49
+ */
50
+ export function wordsToSeed(phrase) {
51
+ const normalised = phrase.trim().toLowerCase().split(/\s+/).join(" ");
52
+ if (!normalised)
53
+ throw new Error("Enter your identity words.");
54
+ const count = normalised.split(" ").length;
55
+ if (count !== BACKUP_WORDS) {
56
+ throw new Error(`That is ${count} words — an identity backup is ${BACKUP_WORDS}.`);
57
+ }
58
+ if (!validateMnemonic(normalised, wordlist)) {
59
+ throw new Error("Those words aren't a valid identity backup. Check for a mistyped or swapped word.");
60
+ }
61
+ const seed = mnemonicToEntropy(normalised, wordlist);
62
+ assertUsableSeed(seed);
63
+ return seed;
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gryt/crypto",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Message encryption for Gryt: key derivation, key bindings, sealed envelopes, pinning and comparison codes. Shared by the desktop client and the mobile app.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -33,7 +33,8 @@
33
33
  "dependencies": {
34
34
  "@noble/ciphers": "^2",
35
35
  "@noble/curves": "^2.3.0",
36
- "@noble/hashes": "^2.3.0"
36
+ "@noble/hashes": "^2.3.0",
37
+ "@scure/bip39": "^2.3.0"
37
38
  },
38
39
  "devDependencies": {
39
40
  "@types/node": "^20.19.33",