mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Add a crypto module
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import nacl from "tweetnacl";
|
||||
import { hkdfExtractAndExpand } from "../crypto/kdf";
|
||||
|
||||
export interface X25519KeyPair {
|
||||
publicKey: Uint8Array;
|
||||
privateKey: Uint8Array;
|
||||
}
|
||||
|
||||
export function generateX25519KeyPair(): X25519KeyPair {
|
||||
const kp = nacl.box.keyPair();
|
||||
return { publicKey: kp.publicKey, privateKey: kp.secretKey };
|
||||
}
|
||||
|
||||
export function ecdhSharedSecret(myPrivateKey: Uint8Array, theirPublicKey: Uint8Array): Uint8Array {
|
||||
// nacl.box.before returns shared key (Curve25519, XSalsa20-Poly1305 context). We use it as IKM into HKDF.
|
||||
return nacl.box.before(theirPublicKey, myPrivateKey);
|
||||
}
|
||||
|
||||
export async function deriveWrappingKey(sharedSecret: Uint8Array, salt: Uint8Array, info: Uint8Array): Promise<Uint8Array> {
|
||||
return hkdfExtractAndExpand(sharedSecret.buffer, salt, info, 32);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { aesGcmDecrypt, aesGcmEncrypt } from "./symmetric";
|
||||
import { importPassword, deriveKEK, randomBytes } from "./kdf";
|
||||
|
||||
export interface PrivateKeyBundle {
|
||||
version: 1;
|
||||
privateKey: Uint8Array; // X25519 private key
|
||||
}
|
||||
|
||||
export interface EncryptedBackupBlob {
|
||||
salt: Uint8Array; // for PBKDF2 derivation of KEK
|
||||
iv: Uint8Array; // AES-GCM IV
|
||||
ciphertext: Uint8Array; // encrypted serialized PrivateKeyBundle
|
||||
}
|
||||
|
||||
export function serializeBundle(bundle: PrivateKeyBundle): Uint8Array {
|
||||
const header = new Uint8Array([bundle.version]);
|
||||
const len = new Uint8Array(new Uint32Array([bundle.privateKey.length]).buffer);
|
||||
const out = new Uint8Array(1 + 4 + bundle.privateKey.length);
|
||||
out.set(header, 0);
|
||||
out.set(len, 1);
|
||||
out.set(bundle.privateKey, 5);
|
||||
return out;
|
||||
}
|
||||
|
||||
export function deserializeBundle(data: Uint8Array): PrivateKeyBundle {
|
||||
const version = data[0] as 1;
|
||||
const len = new Uint32Array(data.slice(1, 5).buffer)[0];
|
||||
const pk = data.slice(5, 5 + len);
|
||||
return { version, privateKey: pk };
|
||||
}
|
||||
|
||||
export async function encryptBackupWithPassword(password: string, bundle: PrivateKeyBundle): Promise<EncryptedBackupBlob> {
|
||||
const salt = randomBytes(16);
|
||||
const pw = await importPassword(password);
|
||||
const kek = await deriveKEK(pw, salt);
|
||||
const serialized = serializeBundle(bundle);
|
||||
const { iv, ciphertext } = await aesGcmEncrypt(kek, serialized);
|
||||
return { salt, iv, ciphertext };
|
||||
}
|
||||
|
||||
export async function decryptBackupWithPassword(password: string, blob: EncryptedBackupBlob): Promise<PrivateKeyBundle> {
|
||||
const pw = await importPassword(password);
|
||||
const kek = await deriveKEK(pw, blob.salt);
|
||||
const plaintext = await aesGcmDecrypt(kek, blob.iv, blob.ciphertext);
|
||||
return deserializeBundle(plaintext);
|
||||
}
|
||||
|
||||
export function encodeBlob(blob: EncryptedBackupBlob): string {
|
||||
function b64(a: Uint8Array) { return btoa(String.fromCharCode(...a)); }
|
||||
return JSON.stringify({
|
||||
salt: b64(blob.salt),
|
||||
iv: b64(blob.iv),
|
||||
ciphertext: b64(blob.ciphertext)
|
||||
});
|
||||
}
|
||||
|
||||
export function decodeBlob(json: string): EncryptedBackupBlob {
|
||||
function ub64(s: string) {
|
||||
const bin = atob(s);
|
||||
const arr = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
|
||||
return arr;
|
||||
}
|
||||
const obj = JSON.parse(json);
|
||||
return { salt: ub64(obj.salt), iv: ub64(obj.iv), ciphertext: ub64(obj.ciphertext) };
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./kdf";
|
||||
export * from "./symmetric";
|
||||
export * from "./asymmetric";
|
||||
export * from "./backup";
|
||||
@@ -0,0 +1,28 @@
|
||||
export async function importPassword(password: string): Promise<CryptoKey> {
|
||||
const enc = new TextEncoder();
|
||||
return crypto.subtle.importKey("raw", enc.encode(password), "PBKDF2", false, ["deriveKey", "deriveBits"]);
|
||||
}
|
||||
|
||||
export async function deriveKEK(passwordKey: CryptoKey, salt: Uint8Array, iterations = 210_000): Promise<CryptoKey> {
|
||||
return crypto.subtle.deriveKey(
|
||||
{ name: "PBKDF2", salt, iterations, hash: "SHA-256" },
|
||||
passwordKey,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
}
|
||||
|
||||
export async function hkdfExtractAndExpand(inputKeyMaterial: ArrayBuffer, salt: Uint8Array, info: Uint8Array, length = 32): Promise<Uint8Array> {
|
||||
const ikmKey = await crypto.subtle.importKey("raw", inputKeyMaterial, { name: "HKDF" }, false, ["deriveBits"]);
|
||||
const bits = await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, ikmKey, length * 8);
|
||||
return new Uint8Array(bits);
|
||||
}
|
||||
|
||||
export function randomBytes(length: number): Uint8Array {
|
||||
const out = new Uint8Array(length);
|
||||
crypto.getRandomValues(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface AesGcmCiphertext {
|
||||
iv: Uint8Array;
|
||||
ciphertext: Uint8Array;
|
||||
}
|
||||
|
||||
export async function aesGcmEncrypt(key: CryptoKey, plaintext: Uint8Array): Promise<AesGcmCiphertext> {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
|
||||
return { iv, ciphertext: new Uint8Array(ct) };
|
||||
}
|
||||
|
||||
export async function aesGcmDecrypt(key: CryptoKey, iv: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
|
||||
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
||||
return new Uint8Array(pt);
|
||||
}
|
||||
|
||||
export async function importAesGcmKey(rawKey: Uint8Array): Promise<CryptoKey> {
|
||||
return crypto.subtle.importKey("raw", rawKey, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
declare module "tweetnacl" {
|
||||
const nacl: any;
|
||||
export default nacl;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -44,6 +44,7 @@
|
||||
"vite-plugin-html": "^3.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"mdui": "^2.1.4"
|
||||
"mdui": "^2.1.4",
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user