tangled
alpha
login
or
join now
graham.systems
/
cistern
3
fork
atom
Encrypted, ephemeral, private memos on atproto
3
fork
atom
overview
issues
pulls
pipelines
feat(crypto): verify decrypted result
graham.systems
4 months ago
a3f05d30
1cc767e5
verified
This commit was signed with the committer's
known signature
.
graham.systems
SSH Key Fingerprint:
SHA256:Fvaam8TgCBeBlr/Fo7eA6VGAIAWmzjwUqUTw5o6anWA=
+50
-8
5 changed files
expand all
collapse all
unified
split
packages
crypto
src
decrypt.test.ts
decrypt.ts
encrypt.test.ts
encrypt.ts
types.ts
+27
packages/crypto/src/decrypt.test.ts
···
2
2
import { generateKeys } from "./keys.ts";
3
3
import { encryptText } from "./encrypt.ts";
4
4
import { decryptText } from "./decrypt.ts";
5
5
+
import { sha3_512 } from "@noble/hashes/sha3.js";
5
6
6
7
Deno.test({
7
8
name: "decrypts an encrypted value",
···
14
15
expect(decrypted).toEqual(text);
15
16
},
16
17
});
18
18
+
19
19
+
Deno.test({
20
20
+
name: "errors when provided an incorrect hash",
21
21
+
fn() {
22
22
+
const keys = generateKeys();
23
23
+
const text = "Hello, world!";
24
24
+
const encrypted = encryptText(keys.publicKey, text);
25
25
+
26
26
+
encrypted.hash = sha3_512(new Uint8Array(24)).toBase64();
27
27
+
28
28
+
expect(() => decryptText(keys.secretKey, encrypted)).toThrow();
29
29
+
},
30
30
+
});
31
31
+
32
32
+
Deno.test({
33
33
+
name: "errors when provided an incorrect content length",
34
34
+
fn() {
35
35
+
const keys = generateKeys();
36
36
+
const text = "Hello, world!";
37
37
+
const encrypted = encryptText(keys.publicKey, text);
38
38
+
39
39
+
encrypted.length = Math.round(Math.random() * 1000);
40
40
+
41
41
+
expect(() => decryptText(keys.secretKey, encrypted)).toThrow();
42
42
+
},
43
43
+
});
+12
packages/crypto/src/decrypt.ts
···
1
1
import { XWing } from "@noble/post-quantum/hybrid.js";
2
2
import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
3
3
+
import { sha3_512 } from "@noble/hashes/sha3.js";
3
4
import type { EncryptedPayload } from "./types.ts";
4
5
5
6
export function decryptText(
···
12
13
const sharedSecret = XWing.decapsulate(cipherText, secretKey);
13
14
const cipher = xchacha20poly1305(sharedSecret, nonce);
14
15
const decrypted = cipher.decrypt(content);
16
16
+
const hash = sha3_512(decrypted);
17
17
+
18
18
+
if (decrypted.byteLength !== payload.length) {
19
19
+
throw new Error(
20
20
+
`content lengths do not match: got ${decrypted.byteLength}, expected ${payload.length}`,
21
21
+
);
22
22
+
} else if (hash.toBase64() !== payload.hash) {
23
23
+
throw new Error(
24
24
+
`hashes do not match: got ${hash.toBase64()}, expected ${payload.hash}`,
25
25
+
);
26
26
+
}
15
27
16
28
return new TextDecoder().decode(decrypted);
17
29
}
+7
-6
packages/crypto/src/encrypt.test.ts
···
3
3
import { encryptText } from "./encrypt.ts";
4
4
5
5
Deno.test({
6
6
-
name: "generates non-empty encrypted payload",
6
6
+
name: "generates an encrypted payload",
7
7
fn() {
8
8
const keys = generateKeys();
9
9
const text = "Hello, world!";
10
10
const result = encryptText(keys.publicKey, text);
11
11
-
const entries = Array.from(Object.values(result));
12
11
13
13
-
expect(entries).toHaveLength(4);
12
12
+
expect(Object.entries(result)).toHaveLength(5);
14
13
15
15
-
for (const [key, val] of entries) {
16
16
-
expect(val.length, `${key} is empty`).toBeGreaterThan(0);
17
17
-
}
14
14
+
expect(result.cipherText.length).toBeGreaterThan(0);
15
15
+
expect(result.content.length).toBeGreaterThan(0);
16
16
+
expect(result.hash.length).toBeGreaterThan(0);
17
17
+
expect(result.nonce.length).toBeGreaterThan(0);
18
18
+
expect(result.length).toBeGreaterThan(0);
18
19
},
19
20
});
+3
-2
packages/crypto/src/encrypt.ts
···
1
1
import { XWing } from "@noble/post-quantum/hybrid.js";
2
2
-
import { sha256 } from "@noble/hashes/sha2.js";
3
2
import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
4
3
import { randomBytes } from "@noble/hashes/utils.js";
4
4
+
import { sha3_512 } from "@noble/hashes/sha3.js";
5
5
import type { EncryptedPayload } from "./types.ts";
6
6
7
7
export function encryptText(
···
13
13
const contentBytes = new TextEncoder().encode(text);
14
14
const cipher = xchacha20poly1305(sharedSecret, nonce);
15
15
const content = cipher.encrypt(contentBytes);
16
16
-
const hash = sha256(content);
16
16
+
const hash = sha3_512(contentBytes);
17
17
18
18
return {
19
19
cipherText: cipherText.toBase64(),
20
20
content: content.toBase64(),
21
21
nonce: nonce.toBase64(),
22
22
hash: hash.toBase64(),
23
23
+
length: contentBytes.byteLength,
23
24
};
24
25
}
+1
packages/crypto/src/types.ts
···
3
3
content: string;
4
4
nonce: string;
5
5
hash: string;
6
6
+
length: number;
6
7
}