mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement standalone FromChat Protocol
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import { API_BASE_URL } from "@/core/config";
|
||||
import type { LoginRequest, RegisterRequest, LoginResponse } from "@/core/types";
|
||||
import { generateX25519KeyPair } from "@/utils/crypto/asymmetric";
|
||||
import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "@/utils/crypto/backup";
|
||||
import { generateX25519KeyPair, hkdfExtractAndExpand, encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "@fromchat/protocol";
|
||||
import { b64, ub64 } from "@/utils/utils";
|
||||
import { hkdfExtractAndExpand } from "@/utils/crypto/kdf";
|
||||
import { fetchPublicKey, uploadPublicKey, fetchBackupBlob, uploadBackupBlob } from "../crypto";
|
||||
import type { Headers } from "@/core/types";
|
||||
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
import { API_BASE_URL } from "@/core/config";
|
||||
import { getAuthHeaders } from "../user/auth";
|
||||
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
||||
import { randomBytes } from "@/utils/crypto/kdf";
|
||||
import { getCurrentKeys } from "../user/auth";
|
||||
import { request } from "@/core/websocket";
|
||||
import type { SendDMRequest, DmEnvelope, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope, User } from "@/core/types";
|
||||
import type { SendDMRequest, DmEnvelope, DMEditRequest, BaseDmEnvelope, User } from "@/core/types";
|
||||
import { b64, ub64 } from "@/utils/utils";
|
||||
import { fetchUserPublicKey } from "../crypto/identity";
|
||||
import { fetchUsers, searchUsers } from "../user/search";
|
||||
import { getOrInitProtocol } from "@/utils/crypto/fromchatInit";
|
||||
import { ecdhSharedSecret, deriveWrappingKey, importAesGcmKey, aesGcmEncrypt, randomBytes } from "@fromchat/protocol";
|
||||
|
||||
export async function decrypt(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> {
|
||||
const keys = getCurrentKeys();
|
||||
if (!keys) throw new Error("Keys not initialized");
|
||||
|
||||
// Obtain the key
|
||||
const shared = ecdhSharedSecret(keys.privateKey, ub64(senderPublicKeyB64));
|
||||
const wkRaw = await deriveWrappingKey(shared, ub64(envelope.salt), new Uint8Array([1]));
|
||||
const wk = await importAesGcmKey(wkRaw);
|
||||
const mk = await aesGcmDecrypt(wk, ub64(envelope.iv2), ub64(envelope.wrappedMk));
|
||||
|
||||
// Decrypt
|
||||
const msg = await aesGcmDecrypt(await importAesGcmKey(mk), ub64(envelope.iv), ub64(envelope.ciphertext));
|
||||
return new TextDecoder().decode(msg);
|
||||
const protocol = getOrInitProtocol();
|
||||
const senderPublicKey = ub64(senderPublicKeyB64);
|
||||
|
||||
return await protocol.decryptMessage(senderPublicKey, envelope);
|
||||
}
|
||||
|
||||
export async function fetchMessages(userId: number, token: string, limit: number = 50, beforeId?: number): Promise<{ messages: DmEnvelope[]; has_more: boolean }> {
|
||||
@@ -39,27 +30,14 @@ export async function fetchMessages(userId: number, token: string, limit: number
|
||||
}
|
||||
|
||||
export async function send(recipientId: number, recipientPublicKeyB64: string, plaintext: string, authToken: string, replyToId?: number): Promise<void> {
|
||||
const keys = getCurrentKeys();
|
||||
if (!keys) throw new Error("Keys not initialized");
|
||||
|
||||
// Encryption key
|
||||
const mk = randomBytes(32);
|
||||
const wkSalt = randomBytes(16);
|
||||
const shared = ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
|
||||
const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1]));
|
||||
const wk = await importAesGcmKey(wkRaw);
|
||||
|
||||
// Encrypt the message
|
||||
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(plaintext));
|
||||
const wrap = await aesGcmEncrypt(wk, mk);
|
||||
|
||||
const protocol = getOrInitProtocol();
|
||||
const recipientPublicKey = ub64(recipientPublicKeyB64);
|
||||
|
||||
const encrypted = await protocol.encryptMessage(recipientPublicKey, plaintext);
|
||||
|
||||
const payload: SendDMRequest = {
|
||||
recipientId: recipientId,
|
||||
iv: b64(encMsg.iv),
|
||||
ciphertext: b64(encMsg.ciphertext),
|
||||
salt: b64(wkSalt),
|
||||
iv2: b64(wrap.iv),
|
||||
wrappedMk: b64(wrap.ciphertext)
|
||||
...encrypted
|
||||
};
|
||||
if (replyToId) payload.replyToId = replyToId;
|
||||
|
||||
@@ -74,15 +52,16 @@ export async function send(recipientId: number, recipientPublicKeyB64: string, p
|
||||
}
|
||||
|
||||
export async function sendWithFiles(recipientId: number, recipientPublicKeyB64: string, plaintextJson: string, files: File[], token: string): Promise<void> {
|
||||
// For files, we need to use the same message key for both the message and files
|
||||
// So we'll do the encryption manually here to reuse the mk
|
||||
const keys = getCurrentKeys();
|
||||
if (!keys) throw new Error("Keys not initialized");
|
||||
|
||||
const mk = randomBytes(32);
|
||||
const wkSalt = randomBytes(16);
|
||||
const shared = await ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
|
||||
const shared = ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
|
||||
const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1]));
|
||||
const wk = await importAesGcmKey(wkRaw);
|
||||
|
||||
const wrap = await aesGcmEncrypt(wk, mk);
|
||||
|
||||
const form = new FormData();
|
||||
@@ -96,21 +75,14 @@ export async function sendWithFiles(recipientId: number, recipientPublicKeyB64:
|
||||
const data = new Uint8Array(await f.arrayBuffer());
|
||||
const enc = await aesGcmEncrypt(await importAesGcmKey(mk), data);
|
||||
const blob = new Blob([sliceBuffer(enc.iv), sliceBuffer(enc.ciphertext)], { type: "application/octet-stream" });
|
||||
const serverName = f.name; // server uses provided name
|
||||
const serverName = f.name;
|
||||
names.push(serverName);
|
||||
form.append("files", new File([blob], serverName));
|
||||
}
|
||||
form.append("fileNames", JSON.stringify(names));
|
||||
|
||||
// Merge files metadata into plaintext JSON and encrypt
|
||||
let obj: DmEncryptedJSON;
|
||||
try {
|
||||
obj = JSON.parse(plaintextJson);
|
||||
} catch {
|
||||
obj = { type: "text", data: { content: String(plaintextJson) } };
|
||||
}
|
||||
|
||||
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(JSON.stringify(obj)));
|
||||
// Encrypt the plaintext JSON with the same mk
|
||||
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(plaintextJson));
|
||||
form.append("dm_payload", JSON.stringify({
|
||||
recipientId: recipientId,
|
||||
iv: b64(encMsg.iv),
|
||||
@@ -128,28 +100,17 @@ export async function sendWithFiles(recipientId: number, recipientPublicKeyB64:
|
||||
}
|
||||
|
||||
export async function edit(id: number, recipientPublicKeyB64: string, newPlaintextJson: string, authToken: string): Promise<void> {
|
||||
const keys = getCurrentKeys();
|
||||
if (!keys) throw new Error("Keys not initialized");
|
||||
|
||||
// We cannot reuse the old mk safely without knowing it; generate a fresh mk and wrap
|
||||
const mk = randomBytes(32);
|
||||
const wkSalt = randomBytes(16);
|
||||
const shared = await ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64));
|
||||
const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1]));
|
||||
const wk = await importAesGcmKey(wkRaw);
|
||||
const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(newPlaintextJson));
|
||||
const wrap = await aesGcmEncrypt(wk, mk);
|
||||
const protocol = getOrInitProtocol();
|
||||
const recipientPublicKey = ub64(recipientPublicKeyB64);
|
||||
|
||||
const encrypted = await protocol.encryptMessage(recipientPublicKey, newPlaintextJson);
|
||||
|
||||
await request({
|
||||
type: "dmEdit",
|
||||
credentials: { scheme: "Bearer", credentials: authToken },
|
||||
data: {
|
||||
id,
|
||||
iv: b64(encMsg.iv),
|
||||
ciphertext: b64(encMsg.ciphertext),
|
||||
iv2: b64(wrap.iv),
|
||||
wrappedMk: b64(wrap.ciphertext),
|
||||
salt: b64(wkSalt)
|
||||
...encrypted
|
||||
}
|
||||
} as DMEditRequest);
|
||||
}
|
||||
@@ -189,6 +150,4 @@ export async function markRead(id: number, authToken: string): Promise<void> {
|
||||
}
|
||||
|
||||
// Re-export user functions for convenience
|
||||
export { fetchUsers, searchUsers, fetchUserPublicKey };
|
||||
|
||||
|
||||
export { fetchUsers, searchUsers, fetchUserPublicKey };
|
||||
@@ -1,12 +1,12 @@
|
||||
import { API_BASE_URL } from "@/core/config";
|
||||
import { getAuthHeaders } from "../user/auth";
|
||||
import type { BackupBlob } from "@/core/types";
|
||||
import api from "@/core/api";
|
||||
|
||||
/**
|
||||
* Fetches the current user's backup blob
|
||||
*/
|
||||
export async function fetchBackupBlob(token: string): Promise<string | null> {
|
||||
const headers = getAuthHeaders(token, true);
|
||||
const headers = api.user.auth.getAuthHeaders(token, true);
|
||||
const res = await fetch(`${API_BASE_URL}/crypto/backup`, {
|
||||
method: "GET",
|
||||
headers
|
||||
@@ -25,7 +25,7 @@ export async function fetchBackupBlob(token: string): Promise<string | null> {
|
||||
export async function uploadBackupBlob(blobJson: string, token: string): Promise<void> {
|
||||
const payload: BackupBlob = { blob: blobJson }
|
||||
|
||||
const headers = getAuthHeaders(token, true);
|
||||
const headers = api.user.auth.getAuthHeaders(token, true);
|
||||
const res = await fetch(`${API_BASE_URL}/crypto/backup`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { API_BASE_URL } from "@/core/config";
|
||||
import { getAuthHeaders } from "./account";
|
||||
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
||||
import { randomBytes } from "@/utils/crypto/kdf";
|
||||
import { ecdhSharedSecret, deriveWrappingKey, importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt, randomBytes } from "@fromchat/protocol";
|
||||
import { getCurrentKeys } from "./account";
|
||||
import { request } from "@/core/websocket";
|
||||
import type { SendDMRequest, DmEnvelope, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope, User } from "@/core/types";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { API_BASE_URL } from "@/core/config";
|
||||
import { getAuthHeaders } from "./account";
|
||||
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
||||
import { randomBytes } from "@/utils/crypto/kdf";
|
||||
import { ecdhSharedSecret, deriveWrappingKey, importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt, randomBytes } from "@fromchat/protocol";
|
||||
import { getCurrentKeys } from "./account";
|
||||
import { request } from "@/core/websocket";
|
||||
import type { SendDMRequest, DmEnvelope, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope, User } from "@/core/types";
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { API_BASE_URL } from "@/core/config";
|
||||
import type { LoginRequest, RegisterRequest, LoginResponse, Headers } from "@/core/types";
|
||||
import { generateX25519KeyPair } from "@/utils/crypto/asymmetric";
|
||||
import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "@/utils/crypto/backup";
|
||||
import { generateX25519KeyPair, hkdfExtractAndExpand, encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "@fromchat/protocol";
|
||||
import { b64, ub64 } from "@/utils/utils";
|
||||
import { hkdfExtractAndExpand } from "@/utils/crypto/kdf";
|
||||
import { fetchPublicKey, uploadPublicKey } from "../crypto/identity";
|
||||
import { fetchBackupBlob, uploadBackupBlob } from "../crypto/backup";
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
||||
import { randomBytes } from "@/utils/crypto/kdf";
|
||||
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt, randomBytes, ecdhSharedSecret, deriveWrappingKey } from "@fromchat/protocol";
|
||||
import { b64, ub64 } from "@/utils/utils";
|
||||
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||
import api from "@/core/api";
|
||||
import type { WrappedSessionKeyPayload } from "@/core/types";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import api from "@/core/api";
|
||||
import type { CallSignalingMessage, WrappedSessionKeyPayload } from "@/core/types";
|
||||
import { request } from "@/core/websocket";
|
||||
import { wrapCallSessionKeyForRecipient, unwrapCallSessionKeyFromSender, rotateCallSessionKey } from "./encryption";
|
||||
import { importAesGcmKey } from "@/utils/crypto/symmetric";
|
||||
import { importAesGcmKey } from "@fromchat/protocol";
|
||||
import E2EEWorker from "./e2eeWorker?worker";
|
||||
import { delay } from "@/utils/utils";
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@ import { parse } from "marked";
|
||||
import { escape as escapeHtml } from "he";
|
||||
import { useEffect, useState, useRef, useMemo } from "react";
|
||||
import api from "@/core/api";
|
||||
import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric";
|
||||
import { importAesGcmKey, aesGcmDecrypt } from "@/utils/crypto/symmetric";
|
||||
import { ecdhSharedSecret, deriveWrappingKey, importAesGcmKey, aesGcmDecrypt } from "@fromchat/protocol";
|
||||
import { useUserStore } from "@/state/user";
|
||||
import { useProfileStore } from "@/state/profile";
|
||||
import { StatusBadge } from "@/core/components/StatusBadge";
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import nacl from "tweetnacl";
|
||||
import { hkdfExtractAndExpand } from "./kdf";
|
||||
|
||||
export interface X25519KeyPair {
|
||||
publicKey: Uint8Array;
|
||||
privateKey: Uint8Array;
|
||||
}
|
||||
|
||||
export type KeyPair = X25519KeyPair;
|
||||
|
||||
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 as ArrayBuffer, salt, info, 32);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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,26 @@
|
||||
import { FromChatProtocol } from "@fromchat/protocol";
|
||||
import { getCurrentKeys } from "@/core/api/user/auth";
|
||||
|
||||
let protocolInstance: FromChatProtocol | null = null;
|
||||
|
||||
export function getFromChatProtocol(): FromChatProtocol | null {
|
||||
return protocolInstance;
|
||||
}
|
||||
|
||||
export function initializeFromChatProtocol(privateKey: Uint8Array): FromChatProtocol {
|
||||
protocolInstance = new FromChatProtocol(privateKey);
|
||||
return protocolInstance;
|
||||
}
|
||||
|
||||
export function getOrInitProtocol(): FromChatProtocol {
|
||||
if (protocolInstance) {
|
||||
return protocolInstance;
|
||||
}
|
||||
|
||||
const keys = getCurrentKeys();
|
||||
if (!keys) {
|
||||
throw new Error("Keys not initialized");
|
||||
}
|
||||
|
||||
return initializeFromChatProtocol(keys.privateKey);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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 | ArrayBuffer, iterations = 210_000): Promise<CryptoKey> {
|
||||
const saltBuffer = salt instanceof Uint8Array ? salt.buffer as ArrayBuffer : salt;
|
||||
return crypto.subtle.deriveKey(
|
||||
{ name: "PBKDF2", salt: saltBuffer, iterations, hash: "SHA-256" },
|
||||
passwordKey,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
}
|
||||
|
||||
export async function hkdfExtractAndExpand(inputKeyMaterial: Uint8Array | ArrayBuffer, salt: Uint8Array | ArrayBuffer, info: Uint8Array | ArrayBuffer, length = 32): Promise<Uint8Array> {
|
||||
const inputBuffer = inputKeyMaterial instanceof Uint8Array ? inputKeyMaterial.buffer as ArrayBuffer : inputKeyMaterial;
|
||||
const saltBuffer = salt instanceof Uint8Array ? salt.buffer as ArrayBuffer : salt;
|
||||
const infoBuffer = info instanceof Uint8Array ? info.buffer as ArrayBuffer : info;
|
||||
|
||||
const ikmKey = await crypto.subtle.importKey("raw", inputBuffer, { name: "HKDF" }, false, ["deriveBits"]);
|
||||
const bits = await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: saltBuffer, info: infoBuffer }, ikmKey, length * 8);
|
||||
return new Uint8Array(bits);
|
||||
}
|
||||
|
||||
export function randomBytes(length: number): Uint8Array {
|
||||
const out = new Uint8Array(length);
|
||||
crypto.getRandomValues(out);
|
||||
return out;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
export interface AesGcmCiphertext {
|
||||
iv: Uint8Array;
|
||||
ciphertext: Uint8Array;
|
||||
}
|
||||
|
||||
export async function aesGcmEncrypt(key: CryptoKey, plaintext: Uint8Array | ArrayBuffer): Promise<AesGcmCiphertext> {
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const plaintextBuffer = plaintext instanceof Uint8Array ? plaintext.buffer as ArrayBuffer : plaintext;
|
||||
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintextBuffer);
|
||||
return { iv, ciphertext: new Uint8Array(ct) };
|
||||
}
|
||||
|
||||
export async function aesGcmDecrypt(key: CryptoKey, iv: Uint8Array | ArrayBuffer, ciphertext: Uint8Array | ArrayBuffer): Promise<Uint8Array> {
|
||||
// Normalize IV to ArrayBuffer (12 bytes for AES-GCM)
|
||||
const ivBuf: ArrayBuffer = iv instanceof Uint8Array
|
||||
? (iv.buffer as ArrayBuffer).slice(iv.byteOffset, iv.byteOffset + iv.byteLength)
|
||||
: (iv as ArrayBuffer);
|
||||
|
||||
// Normalize ciphertext to a contiguous ArrayBuffer slice
|
||||
const ctBuf: ArrayBuffer = ciphertext instanceof Uint8Array
|
||||
? (ciphertext.buffer as ArrayBuffer).slice(ciphertext.byteOffset, ciphertext.byteOffset + ciphertext.byteLength)
|
||||
: (ciphertext as ArrayBuffer);
|
||||
|
||||
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: ivBuf }, key, ctBuf);
|
||||
return new Uint8Array(pt as ArrayBuffer);
|
||||
}
|
||||
|
||||
export async function importAesGcmKey(rawKey: Uint8Array | ArrayBuffer): Promise<CryptoKey> {
|
||||
// Normalize to a contiguous ArrayBuffer slice to avoid offset/length issues
|
||||
const keyBuffer = rawKey instanceof Uint8Array
|
||||
? (rawKey.buffer as ArrayBuffer).slice(rawKey.byteOffset, rawKey.byteOffset + rawKey.byteLength)
|
||||
: (rawKey as ArrayBuffer);
|
||||
return crypto.subtle.importKey("raw", keyBuffer, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
|
||||
}
|
||||
Reference in New Issue
Block a user