@gryt/crypto 0.1.0 → 0.3.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 +28 -7
- package/dist/attachments.d.ts +23 -9
- package/dist/attachments.js +25 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
|
@@ -20,8 +27,8 @@ either way.
|
|
|
20
27
|
- **`conversation-encryption`** — every member has a usable key, or nobody gets
|
|
21
28
|
it sealed.
|
|
22
29
|
- **`comparison-code`** — sixty digits two people read to each other.
|
|
23
|
-
- **`attachments`** — a key per file, bound
|
|
24
|
-
sealed message.
|
|
30
|
+
- **`attachments`** — a key per file, bound so one file’s bytes cannot be served
|
|
31
|
+
as another’s, with the key inside the sealed message.
|
|
25
32
|
|
|
26
33
|
## Importing it
|
|
27
34
|
|
|
@@ -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
|
-
##
|
|
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-
|
|
98
|
+
[AGPL-3.0](https://github.com/Gryt-chat/gryt/blob/main/LICENSE) — Part of [Gryt](https://github.com/Gryt-chat/gryt)
|
package/dist/attachments.d.ts
CHANGED
|
@@ -8,6 +8,23 @@ export declare const SEALED_ATTACHMENT_TYPE = "gryt-sealed-attachment";
|
|
|
8
8
|
* needed.
|
|
9
9
|
*/
|
|
10
10
|
export interface SealedAttachmentKey {
|
|
11
|
+
/**
|
|
12
|
+
* What the ciphertext is bound to. Generated here, not supplied.
|
|
13
|
+
*
|
|
14
|
+
* It used to be the file id, and that could not work: the server assigns the
|
|
15
|
+
* id, and it assigns it in the response to the upload — by which point the
|
|
16
|
+
* bytes have already been encrypted and sent. A caller would have had to
|
|
17
|
+
* choose the id and talk the server into using it, which means a
|
|
18
|
+
* client-chosen primary key and a uniqueness problem that is the server's to
|
|
19
|
+
* lose.
|
|
20
|
+
*
|
|
21
|
+
* A random value chosen here does the same job. The point of binding was
|
|
22
|
+
* never the id itself: it is that a server serving one file's bytes under
|
|
23
|
+
* another's name produces something that does not open. Bound to a value the
|
|
24
|
+
* sender picked and wrote into the encrypted metadata, a swap still fails,
|
|
25
|
+
* and nobody has to agree on an id first.
|
|
26
|
+
*/
|
|
27
|
+
id: string;
|
|
11
28
|
/** The file's own key, base64url. */
|
|
12
29
|
key: string;
|
|
13
30
|
/** Its nonce, base64url. */
|
|
@@ -24,16 +41,14 @@ export interface SealedAttachmentKey {
|
|
|
24
41
|
/**
|
|
25
42
|
* Encrypt a file, ready to upload.
|
|
26
43
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* twice.
|
|
44
|
+
* Nothing has to be agreed with the server first. The value the ciphertext is
|
|
45
|
+
* bound to is generated here and returned in `meta`, so a caller can encrypt,
|
|
46
|
+
* upload, take whatever id the server hands back, and file the metadata under
|
|
47
|
+
* it — see {@link SealedAttachmentKey.id}.
|
|
32
48
|
*/
|
|
33
|
-
export declare function sealAttachment({ bytes, conversationId,
|
|
49
|
+
export declare function sealAttachment({ bytes, conversationId, name, mime, width, height, }: {
|
|
34
50
|
bytes: Uint8Array;
|
|
35
51
|
conversationId: string;
|
|
36
|
-
fileId: string;
|
|
37
52
|
name?: string;
|
|
38
53
|
mime?: string;
|
|
39
54
|
width?: number;
|
|
@@ -49,9 +64,8 @@ export declare function sealAttachment({ bytes, conversationId, fileId, name, mi
|
|
|
49
64
|
* message, where a member who joined later legitimately has no key — so a
|
|
50
65
|
* caller should say the file is broken rather than draw an empty one.
|
|
51
66
|
*/
|
|
52
|
-
export declare function openAttachment({ ciphertext, conversationId,
|
|
67
|
+
export declare function openAttachment({ ciphertext, conversationId, meta, }: {
|
|
53
68
|
ciphertext: Uint8Array;
|
|
54
69
|
conversationId: string;
|
|
55
|
-
fileId: string;
|
|
56
70
|
meta: SealedAttachmentKey;
|
|
57
71
|
}): Uint8Array<ArrayBuffer>;
|
package/dist/attachments.js
CHANGED
|
@@ -40,6 +40,14 @@ import { base64Url, base64UrlDecode } from "./base64.js";
|
|
|
40
40
|
*/
|
|
41
41
|
const IV_BYTES = 12;
|
|
42
42
|
const FILE_KEY_BYTES = 32;
|
|
43
|
+
/**
|
|
44
|
+
* Enough that two attachments never collide, and no more.
|
|
45
|
+
*
|
|
46
|
+
* This is not a secret and not a key. It exists so that one file's ciphertext
|
|
47
|
+
* does not open under another's metadata, so all it has to be is unique among
|
|
48
|
+
* the attachments a person sends.
|
|
49
|
+
*/
|
|
50
|
+
const BINDING_ID_BYTES = 16;
|
|
43
51
|
export const SEALED_ATTACHMENT_TYPE = "gryt-sealed-attachment";
|
|
44
52
|
function randomBytes(length) {
|
|
45
53
|
const bytes = new Uint8Array(length);
|
|
@@ -49,34 +57,36 @@ function randomBytes(length) {
|
|
|
49
57
|
/**
|
|
50
58
|
* What the ciphertext is bound to.
|
|
51
59
|
*
|
|
52
|
-
* The
|
|
53
|
-
* under another and still open. A server that swapped two
|
|
54
|
-
* would otherwise produce files that decrypt perfectly and are
|
|
55
|
-
* and since the reader never saw the original, nothing would
|
|
60
|
+
* The sender's own id for this attachment goes in, so bytes stored under one
|
|
61
|
+
* cannot be served back under another and still open. A server that swapped two
|
|
62
|
+
* members' uploads would otherwise produce files that decrypt perfectly and are
|
|
63
|
+
* the wrong ones — and since the reader never saw the original, nothing would
|
|
64
|
+
* look wrong.
|
|
56
65
|
*
|
|
57
66
|
* The conversation goes in for the reason `message-keys.ts` gives: the same
|
|
58
67
|
* pair talking in two places must not be able to have a file replayed between
|
|
59
68
|
* them.
|
|
60
69
|
*/
|
|
61
|
-
function fileContext(conversationId,
|
|
62
|
-
return new TextEncoder().encode(`${SEALED_ATTACHMENT_TYPE}/v1/${conversationId}/${
|
|
70
|
+
function fileContext(conversationId, id) {
|
|
71
|
+
return new TextEncoder().encode(`${SEALED_ATTACHMENT_TYPE}/v1/${conversationId}/${id}`);
|
|
63
72
|
}
|
|
64
73
|
/**
|
|
65
74
|
* Encrypt a file, ready to upload.
|
|
66
75
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* twice.
|
|
76
|
+
* Nothing has to be agreed with the server first. The value the ciphertext is
|
|
77
|
+
* bound to is generated here and returned in `meta`, so a caller can encrypt,
|
|
78
|
+
* upload, take whatever id the server hands back, and file the metadata under
|
|
79
|
+
* it — see {@link SealedAttachmentKey.id}.
|
|
72
80
|
*/
|
|
73
|
-
export function sealAttachment({ bytes, conversationId,
|
|
81
|
+
export function sealAttachment({ bytes, conversationId, name, mime, width, height, }) {
|
|
82
|
+
const id = base64Url(randomBytes(BINDING_ID_BYTES));
|
|
74
83
|
const key = randomBytes(FILE_KEY_BYTES);
|
|
75
84
|
const iv = randomBytes(IV_BYTES);
|
|
76
|
-
const ciphertext = gcm(key, iv, fileContext(conversationId,
|
|
85
|
+
const ciphertext = gcm(key, iv, fileContext(conversationId, id)).encrypt(bytes);
|
|
77
86
|
return {
|
|
78
87
|
ciphertext: ciphertext,
|
|
79
88
|
meta: {
|
|
89
|
+
id,
|
|
80
90
|
key: base64Url(key),
|
|
81
91
|
iv: base64Url(iv),
|
|
82
92
|
size: bytes.length,
|
|
@@ -94,6 +104,6 @@ export function sealAttachment({ bytes, conversationId, fileId, name, mime, widt
|
|
|
94
104
|
* message, where a member who joined later legitimately has no key — so a
|
|
95
105
|
* caller should say the file is broken rather than draw an empty one.
|
|
96
106
|
*/
|
|
97
|
-
export function openAttachment({ ciphertext, conversationId,
|
|
98
|
-
return gcm(base64UrlDecode(meta.key), base64UrlDecode(meta.iv), fileContext(conversationId,
|
|
107
|
+
export function openAttachment({ ciphertext, conversationId, meta, }) {
|
|
108
|
+
return gcm(base64UrlDecode(meta.key), base64UrlDecode(meta.iv), fileContext(conversationId, meta.id)).decrypt(ciphertext);
|
|
99
109
|
}
|
package/dist/index.d.ts
CHANGED
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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gryt/crypto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": {
|