mirror of
https://github.com/zadam/trilium.git
synced 2026-05-07 02:07:03 +02:00
fix(standalone): encrypt subtree not working
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
"globals": "17.4.0",
|
||||
"i18next": "26.0.3",
|
||||
"i18next-http-backend": "3.0.4",
|
||||
"aes-js": "3.1.2",
|
||||
"jquery": "4.0.0",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
"js-md5": "0.8.3",
|
||||
@@ -72,6 +73,7 @@
|
||||
"vanilla-js-wheel-zoom": "9.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/aes-js": "3.1.4",
|
||||
"@ckeditor/ckeditor5-inspector": "5.0.0",
|
||||
"@preact/preset-vite": "2.10.2",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
|
||||
@@ -5,11 +5,13 @@ import { sha256 } from "js-sha256";
|
||||
import { sha512 } from "js-sha512";
|
||||
import { md5 } from "js-md5";
|
||||
import { scrypt } from "scrypt-js";
|
||||
import aesjs from "aes-js";
|
||||
|
||||
const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
/**
|
||||
* Crypto provider for browser environments using the Web Crypto API.
|
||||
* Crypto provider for browser environments using pure JavaScript crypto libraries.
|
||||
* Uses aes-js for synchronous AES encryption (matching Node.js behavior).
|
||||
*/
|
||||
export default class BrowserCryptoProvider implements CryptoProvider {
|
||||
|
||||
@@ -34,13 +36,11 @@ export default class BrowserCryptoProvider implements CryptoProvider {
|
||||
}
|
||||
|
||||
createCipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): Cipher {
|
||||
// Web Crypto API doesn't support streaming cipher like Node.js
|
||||
// We need to implement a wrapper that collects data and encrypts on final()
|
||||
return new WebCryptoCipher(algorithm, key, iv, "encrypt");
|
||||
return new AesJsCipher(algorithm, key, iv, "encrypt");
|
||||
}
|
||||
|
||||
createDecipheriv(algorithm: "aes-128-cbc", key: Uint8Array, iv: Uint8Array): Cipher {
|
||||
return new WebCryptoCipher(algorithm, key, iv, "decrypt");
|
||||
return new AesJsCipher(algorithm, key, iv, "decrypt");
|
||||
}
|
||||
|
||||
randomBytes(size: number): Uint8Array {
|
||||
@@ -102,25 +102,22 @@ export default class BrowserCryptoProvider implements CryptoProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* A cipher implementation that wraps Web Crypto API.
|
||||
* Note: This buffers all data until final() is called, which differs from
|
||||
* Node.js's streaming cipher behavior.
|
||||
* A synchronous cipher implementation using aes-js.
|
||||
* Matches Node.js crypto behavior with update() and final() methods.
|
||||
*/
|
||||
class WebCryptoCipher implements Cipher {
|
||||
class AesJsCipher implements Cipher {
|
||||
private chunks: Uint8Array[] = [];
|
||||
private algorithm: string;
|
||||
private key: Uint8Array;
|
||||
private iv: Uint8Array;
|
||||
private mode: "encrypt" | "decrypt";
|
||||
private finalized = false;
|
||||
|
||||
constructor(
|
||||
algorithm: "aes-128-cbc",
|
||||
_algorithm: "aes-128-cbc",
|
||||
key: Uint8Array,
|
||||
iv: Uint8Array,
|
||||
mode: "encrypt" | "decrypt"
|
||||
) {
|
||||
this.algorithm = algorithm;
|
||||
this.key = key;
|
||||
this.iv = iv;
|
||||
this.mode = mode;
|
||||
@@ -130,9 +127,9 @@ class WebCryptoCipher implements Cipher {
|
||||
if (this.finalized) {
|
||||
throw new Error("Cipher has already been finalized");
|
||||
}
|
||||
// Buffer the data - Web Crypto doesn't support streaming
|
||||
// Buffer the data - we process everything in final() to match streaming behavior
|
||||
this.chunks.push(data);
|
||||
// Return empty array since we process everything in final()
|
||||
// Return empty array since aes-js CBC doesn't support true streaming
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
@@ -142,24 +139,6 @@ class WebCryptoCipher implements Cipher {
|
||||
}
|
||||
this.finalized = true;
|
||||
|
||||
// Web Crypto API is async, but we need sync behavior
|
||||
// This is a fundamental limitation that requires architectural changes
|
||||
// For now, throw an error directing users to use async methods
|
||||
throw new Error(
|
||||
"Synchronous cipher finalization not available in browser. " +
|
||||
"The Web Crypto API is async-only. Use finalizeAsync() instead."
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Async version that actually performs the encryption/decryption.
|
||||
*/
|
||||
async finalizeAsync(): Promise<Uint8Array> {
|
||||
if (this.finalized) {
|
||||
throw new Error("Cipher has already been finalized");
|
||||
}
|
||||
this.finalized = true;
|
||||
|
||||
// Concatenate all chunks
|
||||
const totalLength = this.chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
const data = new Uint8Array(totalLength);
|
||||
@@ -169,24 +148,33 @@ class WebCryptoCipher implements Cipher {
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
// Copy key and iv to ensure they're plain ArrayBuffer-backed
|
||||
const keyBuffer = new Uint8Array(this.key);
|
||||
const ivBuffer = new Uint8Array(this.iv);
|
||||
if (this.mode === "encrypt") {
|
||||
// PKCS7 padding for encryption
|
||||
const blockSize = 16;
|
||||
const paddingLength = blockSize - (data.length % blockSize);
|
||||
const paddedData = new Uint8Array(data.length + paddingLength);
|
||||
paddedData.set(data);
|
||||
paddedData.fill(paddingLength, data.length);
|
||||
|
||||
// Import the key
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
keyBuffer,
|
||||
{ name: "AES-CBC" },
|
||||
false,
|
||||
[this.mode]
|
||||
);
|
||||
const aesCbc = new aesjs.ModeOfOperation.cbc(
|
||||
Array.from(this.key),
|
||||
Array.from(this.iv)
|
||||
);
|
||||
return new Uint8Array(aesCbc.encrypt(paddedData));
|
||||
} else {
|
||||
// Decryption
|
||||
const aesCbc = new aesjs.ModeOfOperation.cbc(
|
||||
Array.from(this.key),
|
||||
Array.from(this.iv)
|
||||
);
|
||||
const decrypted = new Uint8Array(aesCbc.decrypt(data));
|
||||
|
||||
// Perform encryption/decryption
|
||||
const result = this.mode === "encrypt"
|
||||
? await crypto.subtle.encrypt({ name: "AES-CBC", iv: ivBuffer }, cryptoKey, data)
|
||||
: await crypto.subtle.decrypt({ name: "AES-CBC", iv: ivBuffer }, cryptoKey, data);
|
||||
|
||||
return new Uint8Array(result);
|
||||
// Remove PKCS7 padding
|
||||
const paddingLength = decrypted[decrypted.length - 1];
|
||||
if (paddingLength > 0 && paddingLength <= 16) {
|
||||
return decrypted.slice(0, decrypted.length - paddingLength);
|
||||
}
|
||||
return decrypted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,12 +125,16 @@ async function encryptAsync(key: Uint8Array, plainText: Uint8Array | string): Pr
|
||||
const digest = shaArray(plainTextUint8Array).slice(0, 4);
|
||||
const digestWithPayload = concat2(digest, plainTextUint8Array);
|
||||
|
||||
cipher.update(digestWithPayload);
|
||||
|
||||
// Use async finalization if available (browser), otherwise sync (Node.js)
|
||||
const encryptedData = cipher.finalizeAsync
|
||||
? await cipher.finalizeAsync()
|
||||
: concat2(cipher.update(digestWithPayload), cipher.final());
|
||||
let encryptedData: Uint8Array;
|
||||
if (cipher.finalizeAsync) {
|
||||
// Browser: update() buffers data, finalizeAsync() encrypts and returns all
|
||||
cipher.update(digestWithPayload);
|
||||
encryptedData = await cipher.finalizeAsync();
|
||||
} else {
|
||||
// Node.js: update() and final() both return encrypted chunks
|
||||
encryptedData = concat2(cipher.update(digestWithPayload), cipher.final());
|
||||
}
|
||||
|
||||
const encryptedDataWithIv = concat2(iv, encryptedData);
|
||||
|
||||
@@ -162,12 +166,16 @@ async function decryptAsync(key: Uint8Array, cipherText: string | Uint8Array): P
|
||||
|
||||
const decipher = getCrypto().createDecipheriv("aes-128-cbc", pad(key), pad(iv));
|
||||
|
||||
decipher.update(cipherTextUint8Array);
|
||||
|
||||
// Use async finalization if available (browser), otherwise sync (Node.js)
|
||||
const decryptedBytes = decipher.finalizeAsync
|
||||
? await decipher.finalizeAsync()
|
||||
: concat2(decipher.update(cipherTextUint8Array), decipher.final());
|
||||
let decryptedBytes: Uint8Array;
|
||||
if (decipher.finalizeAsync) {
|
||||
// Browser: update() buffers data, finalizeAsync() decrypts and returns all
|
||||
decipher.update(cipherTextUint8Array);
|
||||
decryptedBytes = await decipher.finalizeAsync();
|
||||
} else {
|
||||
// Node.js: update() and final() both return decrypted chunks
|
||||
decryptedBytes = concat2(decipher.update(cipherTextUint8Array), decipher.final());
|
||||
}
|
||||
|
||||
const digest = decryptedBytes.slice(0, 4);
|
||||
const payload = decryptedBytes.slice(4);
|
||||
|
||||
@@ -29,6 +29,15 @@ function encrypt(plainText: string | Uint8Array) {
|
||||
return dataEncryptionService.encrypt(dataKey, plainText);
|
||||
}
|
||||
|
||||
async function encryptAsync(plainText: string | Uint8Array): Promise<string | null> {
|
||||
const dataKey = getDataKey();
|
||||
if (plainText === null || dataKey === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dataEncryptionService.encryptAsync(dataKey, plainText);
|
||||
}
|
||||
|
||||
function decrypt(cipherText: string | Uint8Array): Uint8Array | null {
|
||||
const dataKey = getDataKey();
|
||||
if (cipherText === null || dataKey === null) {
|
||||
@@ -38,6 +47,15 @@ function decrypt(cipherText: string | Uint8Array): Uint8Array | null {
|
||||
return dataEncryptionService.decrypt(dataKey, cipherText) || null;
|
||||
}
|
||||
|
||||
async function decryptAsync(cipherText: string | Uint8Array): Promise<Uint8Array | null> {
|
||||
const dataKey = getDataKey();
|
||||
if (cipherText === null || dataKey === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (await dataEncryptionService.decryptAsync(dataKey, cipherText)) || null;
|
||||
}
|
||||
|
||||
function decryptString(cipherText: string): string | null {
|
||||
const dataKey = getDataKey();
|
||||
if (dataKey === null) {
|
||||
@@ -46,6 +64,14 @@ function decryptString(cipherText: string): string | null {
|
||||
return dataEncryptionService.decryptString(dataKey, cipherText);
|
||||
}
|
||||
|
||||
async function decryptStringAsync(cipherText: string): Promise<string | null> {
|
||||
const dataKey = getDataKey();
|
||||
if (dataKey === null) {
|
||||
return null;
|
||||
}
|
||||
return dataEncryptionService.decryptStringAsync(dataKey, cipherText);
|
||||
}
|
||||
|
||||
let lastProtectedSessionOperationDate: number | null = null;
|
||||
|
||||
function touchProtectedSession() {
|
||||
@@ -63,8 +89,11 @@ export default {
|
||||
resetDataKey,
|
||||
isProtectedSessionAvailable,
|
||||
encrypt,
|
||||
encryptAsync,
|
||||
decrypt,
|
||||
decryptAsync,
|
||||
decryptString,
|
||||
decryptStringAsync,
|
||||
touchProtectedSession,
|
||||
getLastProtectedSessionOperationDate
|
||||
};
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -491,6 +491,9 @@ importers:
|
||||
'@zumer/snapdom':
|
||||
specifier: 2.7.0
|
||||
version: 2.7.0
|
||||
aes-js:
|
||||
specifier: 3.1.2
|
||||
version: 3.1.2
|
||||
autocomplete.js:
|
||||
specifier: 0.38.1
|
||||
version: 0.38.1
|
||||
@@ -609,6 +612,9 @@ importers:
|
||||
'@preact/preset-vite':
|
||||
specifier: 2.10.2
|
||||
version: 2.10.2(@babel/core@7.29.0)(preact@10.29.1)(vite@8.0.7(@types/node@24.12.2)(esbuild@0.28.0)(jiti@2.6.1)(less@4.1.3)(sass-embedded@1.91.0)(sass@1.91.0)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@types/aes-js':
|
||||
specifier: 3.1.4
|
||||
version: 3.1.4
|
||||
'@types/bootstrap':
|
||||
specifier: 5.2.10
|
||||
version: 5.2.10
|
||||
@@ -5469,6 +5475,9 @@ packages:
|
||||
'@tybys/wasm-util@0.10.1':
|
||||
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
|
||||
|
||||
'@types/aes-js@3.1.4':
|
||||
resolution: {integrity: sha512-v3D66IptpUqh+pHKVNRxY8yvp2ESSZXe0rTzsGdzUhEwag7ljVfgCllkWv2YgiYXDhWFBrEywll4A5JToyTNFA==}
|
||||
|
||||
'@types/appdmg@0.5.5':
|
||||
resolution: {integrity: sha512-G+n6DgZTZFOteITE30LnWj+HRVIGr7wMlAiLWOO02uJFWVEitaPU9JVXm9wJokkgshBawb2O1OykdcsmkkZfgg==}
|
||||
|
||||
@@ -6866,6 +6875,9 @@ packages:
|
||||
resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==}
|
||||
engines: {node: '>=12.0'}
|
||||
|
||||
aes-js@3.1.2:
|
||||
resolution: {integrity: sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==}
|
||||
|
||||
agent-base@6.0.2:
|
||||
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
|
||||
engines: {node: '>= 6.0.0'}
|
||||
@@ -19078,6 +19090,8 @@ snapshots:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@types/aes-js@3.1.4': {}
|
||||
|
||||
'@types/appdmg@0.5.5':
|
||||
dependencies:
|
||||
'@types/node': 24.12.2
|
||||
@@ -21588,6 +21602,8 @@ snapshots:
|
||||
|
||||
adm-zip@0.5.16: {}
|
||||
|
||||
aes-js@3.1.2: {}
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
|
||||
Reference in New Issue
Block a user