Encrypted, ephemeral, private memos on atproto

feat(crypto): verify decrypted result

graham.systems a3f05d30 1cc767e5

verified
Changed files
+50 -8
packages
+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 + 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 + 19 + Deno.test({ 20 + name: "errors when provided an incorrect hash", 21 + fn() { 22 + const keys = generateKeys(); 23 + const text = "Hello, world!"; 24 + const encrypted = encryptText(keys.publicKey, text); 25 + 26 + encrypted.hash = sha3_512(new Uint8Array(24)).toBase64(); 27 + 28 + expect(() => decryptText(keys.secretKey, encrypted)).toThrow(); 29 + }, 30 + }); 31 + 32 + Deno.test({ 33 + name: "errors when provided an incorrect content length", 34 + fn() { 35 + const keys = generateKeys(); 36 + const text = "Hello, world!"; 37 + const encrypted = encryptText(keys.publicKey, text); 38 + 39 + encrypted.length = Math.round(Math.random() * 1000); 40 + 41 + expect(() => decryptText(keys.secretKey, encrypted)).toThrow(); 42 + }, 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 + 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 + const hash = sha3_512(decrypted); 17 + 18 + if (decrypted.byteLength !== payload.length) { 19 + throw new Error( 20 + `content lengths do not match: got ${decrypted.byteLength}, expected ${payload.length}`, 21 + ); 22 + } else if (hash.toBase64() !== payload.hash) { 23 + throw new Error( 24 + `hashes do not match: got ${hash.toBase64()}, expected ${payload.hash}`, 25 + ); 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 - name: "generates non-empty encrypted payload", 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 - const entries = Array.from(Object.values(result)); 12 11 13 - expect(entries).toHaveLength(4); 12 + expect(Object.entries(result)).toHaveLength(5); 14 13 15 - for (const [key, val] of entries) { 16 - expect(val.length, `${key} is empty`).toBeGreaterThan(0); 17 - } 14 + expect(result.cipherText.length).toBeGreaterThan(0); 15 + expect(result.content.length).toBeGreaterThan(0); 16 + expect(result.hash.length).toBeGreaterThan(0); 17 + expect(result.nonce.length).toBeGreaterThan(0); 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 - 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 + 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 - const hash = sha256(content); 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 + 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 + length: number; 6 7 }