@gryt/crypto 0.2.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/README.md CHANGED
@@ -1,7 +1,14 @@
1
- # @gryt/crypto
1
+ <div align="center">
2
+ <img src="https://raw.githubusercontent.com/Gryt-chat/client/main/public/logo.svg" width="80" alt="Gryt logo" />
3
+ <h1>@gryt/crypto</h1>
4
+ <p>Message encryption for <a href="https://gryt.chat">Gryt</a>.<br />Key derivation, key bindings, sealed envelopes, pinning, and the code two people compare out of band.</p>
5
+ </div>
2
6
 
3
- Message encryption for [Gryt](https://gryt.chat): key derivation, key bindings,
4
- sealed envelopes, pinning, and the code two people compare out of band.
7
+ <br />
8
+
9
+ ```sh
10
+ npm install @gryt/crypto
11
+ ```
5
12
 
6
13
  Used by the desktop client and the mobile app. They run this, not two ports of
7
14
  it — two implementations of one envelope is a pair of clients that send each
@@ -72,6 +79,20 @@ what a client installs. `check-crypto-vectors.mjs` holds bytes produced before
72
79
  the WebCrypto-to-noble conversion and nothing regenerates them — a change that
73
80
  quietly altered the envelope would leave every message already sent unreadable.
74
81
 
75
- ## Licence
82
+ ## Issues
83
+
84
+ Please report bugs and request features in the
85
+ [main Gryt repository](https://github.com/Gryt-chat/gryt/issues).
86
+
87
+ ## Sponsors
88
+
89
+ What sponsoring pays for, the tiers, and everyone who has sponsored:
90
+ [gryt.chat/sponsors](https://gryt.chat/sponsors). To sponsor:
91
+ [GitHub Sponsors](https://github.com/sponsors/Gryt-chat).
92
+
93
+ The list itself lives in the [Gryt README](https://github.com/Gryt-chat/gryt#sponsors),
94
+ in one place rather than ten, so it cannot fall out of step across repositories.
95
+
96
+ ## License
76
97
 
77
- AGPL-3.0-only.
98
+ [AGPL-3.0](https://github.com/Gryt-chat/gryt/blob/main/LICENSE) — Part of [Gryt](https://github.com/Gryt-chat/gryt)
package/dist/index.d.ts CHANGED
@@ -33,6 +33,7 @@
33
33
  * setting.
34
34
  */
35
35
  export * from "./attachments.js";
36
+ export * from "./base64.js";
36
37
  export * from "./comparison-code.js";
37
38
  export * from "./conversation-encryption.js";
38
39
  export * from "./dm-key-binding.js";
@@ -41,4 +42,5 @@ export * from "./member-keys.js";
41
42
  export * from "./message-keys.js";
42
43
  export * from "./peer-keys.js";
43
44
  export * from "./scope.js";
45
+ export * from "./seed-words.js";
44
46
  export * from "./thumbprint.js";
package/dist/index.js CHANGED
@@ -33,6 +33,10 @@
33
33
  * setting.
34
34
  */
35
35
  export * from "./attachments.js";
36
+ // Exported because both apps carry their own copy of exactly this, and one of
37
+ // the two is the `btoa` version this file was written to replace. They cannot
38
+ // drop theirs while it is package-internal.
39
+ export * from "./base64.js";
36
40
  export * from "./comparison-code.js";
37
41
  export * from "./conversation-encryption.js";
38
42
  export * from "./dm-key-binding.js";
@@ -41,4 +45,5 @@ export * from "./member-keys.js";
41
45
  export * from "./message-keys.js";
42
46
  export * from "./peer-keys.js";
43
47
  export * from "./scope.js";
48
+ export * from "./seed-words.js";
44
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.2.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",