diff --git a/common/crypto/aes.test.ts b/common/crypto/aes.test.ts new file mode 100644 index 0000000..eb66e9b --- /dev/null +++ b/common/crypto/aes.test.ts @@ -0,0 +1,33 @@ +import { assertEquals } from "../../test_deps.ts"; +import { + decryptAES, + decryptPath, + deriveKeyFromPassword, + encryptAES, + encryptPath, +} from "./aes.ts"; + +Deno.test("AES encryption and decryption", async () => { + const password = "YourPassword"; + const salt = "UniquePerUserSalt"; + const message = "Hello, World!"; + + const key = await deriveKeyFromPassword(password, salt); + const encrypted = await encryptAES(key, message); + + const decrypted = await decryptAES(key, encrypted); + assertEquals(decrypted, message); + + // Test that checks if a path is encrypted the same way every time and can be unencrypted + const path = + "this/is/a/long/path/that/needs/to/be/encrypted because that's what we do.md"; + const encryptedPath = await encryptPath(key, path); + const encryptedPath2 = await encryptPath(key, path); + // Assure two runs give the same result + assertEquals(encryptedPath, encryptedPath2); + + // Ensure decryption works + const decryptedPath = await decryptPath(key, encryptedPath); + console.log(encryptedPath); + assertEquals(decryptedPath, path); +}); diff --git a/common/crypto/aes.ts b/common/crypto/aes.ts new file mode 100644 index 0000000..041439e --- /dev/null +++ b/common/crypto/aes.ts @@ -0,0 +1,111 @@ +import { + base64Decode, + base64Encode, +} from "../../plugos/asset_bundle/base64.ts"; + +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +export async function deriveKeyFromPassword( + password: string, + salt: string, +): Promise { + const baseKey = encoder.encode(password); + const importedKey = await window.crypto.subtle.importKey( + "raw", + baseKey, + { name: "PBKDF2" }, + false, + ["deriveKey"], + ); + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: encoder.encode(salt), + iterations: 10000, + hash: "SHA-256", + }, + importedKey, + { + name: "AES-GCM", + length: 256, + }, + true, + ["encrypt", "decrypt"], + ); +} + +export async function encryptAES( + key: CryptoKey, + message: string, +): Promise { + const iv = crypto.getRandomValues(new Uint8Array(12)); + const encodedMessage = encoder.encode(message); + const ciphertext = await window.crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + encodedMessage, + ); + return appendBuffer(iv, ciphertext); +} + +export async function decryptAES( + key: CryptoKey, + data: ArrayBuffer, +): Promise { + const iv = data.slice(0, 12); + const ciphertext = data.slice(12); + const decrypted = await window.crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: iv, + }, + key, + ciphertext, + ); + return decoder.decode(decrypted); +} + +function appendBuffer(buffer1: ArrayBuffer, buffer2: ArrayBuffer): ArrayBuffer { + const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); + tmp.set(new Uint8Array(buffer1), 0); + tmp.set(new Uint8Array(buffer2), buffer1.byteLength); + return tmp.buffer; +} + +// This is against security recommendations, but we need a way to always generate the same encrypted path for the same path and password +const pathIv = new Uint8Array(12); // 12 bytes of 0 + +export async function encryptPath( + key: CryptoKey, + path: string, +): Promise { + const encodedMessage = encoder.encode(path); + const ciphertext = await crypto.subtle.encrypt( + { + name: "AES-GCM", + iv: pathIv, + }, + key, + encodedMessage, + ); + return base64Encode(new Uint8Array(ciphertext)); +} + +export async function decryptPath( + key: CryptoKey, + data: string, +): Promise { + const decrypted = await crypto.subtle.decrypt( + { + name: "AES-GCM", + iv: pathIv, + }, + key, + base64Decode(data), + ); + return decoder.decode(decrypted); +}