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
implement basic crypto package
graham.systems
4 months ago
1df8c7ef
f3ec2608
verified
This commit was signed with the committer's
known signature
.
graham.systems
SSH Key Fingerprint:
SHA256:Fvaam8TgCBeBlr/Fo7eA6VGAIAWmzjwUqUTw5o6anWA=
+185
10 changed files
expand all
collapse all
unified
split
deno.lock
packages
crypto
deno.jsonc
mod.ts
src
decrypt.test.ts
decrypt.ts
encrypt.test.ts
encrypt.ts
keys.test.ts
keys.ts
types.ts
+58
deno.lock
···
1
1
{
2
2
"version": "5",
3
3
"specifiers": {
4
4
+
"jsr:@noble/ciphers@*": "2.0.1",
5
5
+
"jsr:@noble/ciphers@^2.0.1": "2.0.1",
6
6
+
"jsr:@noble/curves@2.0": "2.0.1",
7
7
+
"jsr:@noble/hashes@*": "2.0.1",
8
8
+
"jsr:@noble/hashes@2": "2.0.1",
9
9
+
"jsr:@noble/hashes@2.0": "2.0.1",
10
10
+
"jsr:@noble/hashes@^2.0.1": "2.0.1",
11
11
+
"jsr:@noble/post-quantum@*": "0.5.2",
12
12
+
"jsr:@noble/post-quantum@~0.5.2": "0.5.2",
13
13
+
"jsr:@std/assert@^1.0.14": "1.0.15",
14
14
+
"jsr:@std/expect@^1.0.17": "1.0.17",
15
15
+
"jsr:@std/internal@^1.0.10": "1.0.12",
16
16
+
"jsr:@std/internal@^1.0.12": "1.0.12",
4
17
"npm:@atproto/lexicon@~0.5.1": "0.5.1"
5
18
},
19
19
+
"jsr": {
20
20
+
"@noble/ciphers@2.0.1": {
21
21
+
"integrity": "1d28df773a29684c85844d27eefbb7cad3e4ce62849b63dae3024baf66cf769f"
22
22
+
},
23
23
+
"@noble/curves@2.0.1": {
24
24
+
"integrity": "21ef41d207a203f60ba37a4fdcbc4f4a545b10c5dab7f293889f18292f81ab23",
25
25
+
"dependencies": [
26
26
+
"jsr:@noble/hashes@2"
27
27
+
]
28
28
+
},
29
29
+
"@noble/hashes@2.0.1": {
30
30
+
"integrity": "e0e908292a0bf91099cf8ba0720a1647cef82ab38b588815b5e9535b4ff4d7bb"
31
31
+
},
32
32
+
"@noble/post-quantum@0.5.2": {
33
33
+
"integrity": "e15f0a636f8d3b39ad584fe9b8a7dd203a6330e9ec48728be7edd6af8e2c7136",
34
34
+
"dependencies": [
35
35
+
"jsr:@noble/curves",
36
36
+
"jsr:@noble/hashes@2.0"
37
37
+
]
38
38
+
},
39
39
+
"@std/assert@1.0.15": {
40
40
+
"integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b",
41
41
+
"dependencies": [
42
42
+
"jsr:@std/internal@^1.0.12"
43
43
+
]
44
44
+
},
45
45
+
"@std/expect@1.0.17": {
46
46
+
"integrity": "316b47dd65c33e3151344eb3267bf42efba17d1415425f07ed96185d67fc04d9",
47
47
+
"dependencies": [
48
48
+
"jsr:@std/assert",
49
49
+
"jsr:@std/internal@^1.0.10"
50
50
+
]
51
51
+
},
52
52
+
"@std/internal@1.0.12": {
53
53
+
"integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027"
54
54
+
}
55
55
+
},
6
56
"npm": {
7
57
"@atproto/common-web@0.4.3": {
8
58
"integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==",
···
47
97
},
48
98
"workspace": {
49
99
"members": {
100
100
+
"packages/crypto": {
101
101
+
"dependencies": [
102
102
+
"jsr:@noble/ciphers@^2.0.1",
103
103
+
"jsr:@noble/hashes@^2.0.1",
104
104
+
"jsr:@noble/post-quantum@~0.5.2",
105
105
+
"jsr:@std/expect@^1.0.17"
106
106
+
]
107
107
+
},
50
108
"packages/lexicon": {
51
109
"dependencies": [
52
110
"npm:@atproto/lexicon@~0.5.1"
+12
packages/crypto/deno.jsonc
···
1
1
+
{
2
2
+
"name": "@cistern/crypto",
3
3
+
"exports": {
4
4
+
".": "mod.ts"
5
5
+
},
6
6
+
"imports": {
7
7
+
"@noble/ciphers": "jsr:@noble/ciphers@^2.0.1",
8
8
+
"@noble/hashes": "jsr:@noble/hashes@^2.0.1",
9
9
+
"@noble/post-quantum": "jsr:@noble/post-quantum@^0.5.2",
10
10
+
"@std/expect": "jsr:@std/expect@^1.0.17"
11
11
+
}
12
12
+
}
+4
packages/crypto/mod.ts
···
1
1
+
export * from "./src/keys.ts";
2
2
+
export * from "./src/encrypt.ts";
3
3
+
export * from "./src/decrypt.ts";
4
4
+
export * from "./src/types.ts";
+16
packages/crypto/src/decrypt.test.ts
···
1
1
+
import { expect } from "@std/expect";
2
2
+
import { generateKeys } from "./keys.ts";
3
3
+
import { encryptText } from "./encrypt.ts";
4
4
+
import { decryptText } from "./decrypt.ts";
5
5
+
6
6
+
Deno.test({
7
7
+
name: "decrypts an encrypted value",
8
8
+
fn() {
9
9
+
const keys = generateKeys();
10
10
+
const text = "Hello, world!";
11
11
+
const encrypted = encryptText(keys.publicKey, text);
12
12
+
const decrypted = decryptText(keys.secretKey, encrypted);
13
13
+
14
14
+
expect(decrypted).toEqual(text);
15
15
+
},
16
16
+
});
+17
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 type { EncryptedPayload } from "./types.ts";
4
4
+
5
5
+
export function decryptText(
6
6
+
secretKey: Uint8Array,
7
7
+
payload: EncryptedPayload,
8
8
+
): string {
9
9
+
const cipherText = Uint8Array.fromBase64(payload.cipherText);
10
10
+
const nonce = Uint8Array.fromBase64(payload.nonce);
11
11
+
const content = Uint8Array.fromBase64(payload.content);
12
12
+
const sharedSecret = XWing.decapsulate(cipherText, secretKey);
13
13
+
const cipher = xchacha20poly1305(sharedSecret, nonce);
14
14
+
const decrypted = cipher.decrypt(content);
15
15
+
16
16
+
return new TextDecoder().decode(decrypted);
17
17
+
}
+19
packages/crypto/src/encrypt.test.ts
···
1
1
+
import { expect } from "@std/expect";
2
2
+
import { generateKeys } from "./keys.ts";
3
3
+
import { encryptText } from "./encrypt.ts";
4
4
+
5
5
+
Deno.test({
6
6
+
name: "generates non-empty 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
12
+
13
13
+
expect(entries).toHaveLength(4);
14
14
+
15
15
+
for (const [key, val] of entries) {
16
16
+
expect(val.length, `${key} is empty`).toBeGreaterThan(0);
17
17
+
}
18
18
+
},
19
19
+
});
+24
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
3
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
4
4
+
import { randomBytes } from "@noble/hashes/utils.js";
5
5
+
import type { EncryptedPayload } from "./types.ts";
6
6
+
7
7
+
export function encryptText(
8
8
+
publicKey: Uint8Array,
9
9
+
text: string,
10
10
+
): EncryptedPayload {
11
11
+
const { cipherText, sharedSecret } = XWing.encapsulate(publicKey);
12
12
+
const nonce = randomBytes(24);
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);
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
+
};
24
24
+
}
+16
packages/crypto/src/keys.test.ts
···
1
1
+
import { expect } from "@std/expect";
2
2
+
import { generateKeys } from "./keys.ts";
3
3
+
4
4
+
Deno.test({
5
5
+
name: "generates non-empty keys",
6
6
+
fn() {
7
7
+
const keys = generateKeys();
8
8
+
const entries = Array.from(Object.entries(keys));
9
9
+
10
10
+
expect(entries).toHaveLength(2);
11
11
+
12
12
+
for (const [key, val] of entries) {
13
13
+
expect(val.length, `${key} is empty`).toBeGreaterThan(0);
14
14
+
}
15
15
+
},
16
16
+
});
+13
packages/crypto/src/keys.ts
···
1
1
+
import { XWing } from "@noble/post-quantum/hybrid.js";
2
2
+
3
3
+
export function generateKeys() {
4
4
+
return XWing.keygen();
5
5
+
}
6
6
+
7
7
+
export function serializeKey(key: Uint8Array) {
8
8
+
return key.toBase64();
9
9
+
}
10
10
+
11
11
+
export function deserializeKey(key: string) {
12
12
+
return Uint8Array.fromBase64(key);
13
13
+
}
+6
packages/crypto/src/types.ts
···
1
1
+
export interface EncryptedPayload {
2
2
+
cipherText: string;
3
3
+
content: string;
4
4
+
nonce: string;
5
5
+
hash: string;
6
6
+
}