feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
118
packages/lib/services/e2ee/crypto.ts
普通文件
118
packages/lib/services/e2ee/crypto.ts
普通文件
@@ -0,0 +1,118 @@
|
||||
import { Crypto, CryptoBuffer, CryptoBufferEncoding, Digest, EncryptionResult, EncryptionParameters } from './types';
|
||||
import { webcrypto } from 'crypto';
|
||||
import { Buffer } from 'buffer';
|
||||
import {
|
||||
generateNonce as generateNonceShared,
|
||||
increaseNonce as increaseNonceShared,
|
||||
setRandomBytesImplementation,
|
||||
} from './cryptoShared';
|
||||
|
||||
const pbkdf2Raw = async (password: string, salt: Uint8Array, iterations: number, keylenBytes: number, digest: Digest) => {
|
||||
const encoder = new TextEncoder();
|
||||
const key = await webcrypto.subtle.importKey(
|
||||
'raw', encoder.encode(password), { name: 'PBKDF2' }, false, ['deriveKey'],
|
||||
);
|
||||
return webcrypto.subtle.deriveKey(
|
||||
{ name: 'PBKDF2', salt, iterations, hash: digest }, key, { name: 'AES-GCM', length: keylenBytes * 8 }, false, ['encrypt', 'decrypt'],
|
||||
);
|
||||
};
|
||||
|
||||
const encryptRaw = async (data: Uint8Array, key: webcrypto.CryptoKey, iv: Uint8Array, authTagLengthBytes: number, additionalData: Uint8Array) => {
|
||||
return Buffer.from(await webcrypto.subtle.encrypt({
|
||||
name: 'AES-GCM',
|
||||
iv,
|
||||
additionalData,
|
||||
tagLength: authTagLengthBytes * 8,
|
||||
}, key, data));
|
||||
};
|
||||
|
||||
const decryptRaw = async (data: Uint8Array, key: webcrypto.CryptoKey, iv: Uint8Array, authTagLengthBytes: number, associatedData: Uint8Array) => {
|
||||
return Buffer.from(await webcrypto.subtle.decrypt({
|
||||
name: 'AES-GCM',
|
||||
iv,
|
||||
additionalData: associatedData,
|
||||
tagLength: authTagLengthBytes * 8,
|
||||
}, key, data));
|
||||
};
|
||||
|
||||
const crypto: Crypto = {
|
||||
|
||||
randomBytes: async (size: number) => {
|
||||
// .getRandomValues has a maximum output size
|
||||
const maxChunkSize = 65536;
|
||||
const result = new Uint8Array(size);
|
||||
|
||||
if (size <= maxChunkSize) {
|
||||
webcrypto.getRandomValues(result);
|
||||
} else {
|
||||
const fullSizeChunk = new Uint8Array(maxChunkSize);
|
||||
const lastChunkSize = size % maxChunkSize;
|
||||
const maxOffset = size - lastChunkSize;
|
||||
let offset = 0;
|
||||
while (offset < maxOffset) {
|
||||
webcrypto.getRandomValues(fullSizeChunk);
|
||||
result.set(fullSizeChunk, offset);
|
||||
offset += maxChunkSize;
|
||||
}
|
||||
if (lastChunkSize > 0) {
|
||||
const lastChunk = webcrypto.getRandomValues(new Uint8Array(lastChunkSize));
|
||||
result.set(lastChunk, offset);
|
||||
}
|
||||
}
|
||||
return Buffer.from(result);
|
||||
},
|
||||
|
||||
digest: async (algorithm: Digest, data: Uint8Array) => {
|
||||
return Buffer.from(await webcrypto.subtle.digest(algorithm, data));
|
||||
},
|
||||
|
||||
encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, encryptionParameters: EncryptionParameters) => {
|
||||
|
||||
// Parameters in EncryptionParameters won't appear in result
|
||||
const result: EncryptionResult = {
|
||||
salt: crypto.bufferToString(salt, 'base64'),
|
||||
iv: '',
|
||||
ct: '', // cipherText
|
||||
};
|
||||
|
||||
// 96 bits IV
|
||||
// "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D
|
||||
const iv = await crypto.randomBytes(12);
|
||||
|
||||
const key = await pbkdf2Raw(password, salt, encryptionParameters.iterationCount, encryptionParameters.keyLength, encryptionParameters.digestAlgorithm);
|
||||
const encrypted = await encryptRaw(data, key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData);
|
||||
|
||||
result.iv = crypto.bufferToString(iv, 'base64');
|
||||
result.ct = crypto.bufferToString(encrypted, 'base64');
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
decrypt: async (password: string, data: EncryptionResult, encryptionParameters: EncryptionParameters) => {
|
||||
|
||||
const salt = Buffer.from(data.salt, 'base64');
|
||||
const iv = Buffer.from(data.iv, 'base64');
|
||||
|
||||
const key = await pbkdf2Raw(password, salt, encryptionParameters.iterationCount, encryptionParameters.keyLength, encryptionParameters.digestAlgorithm);
|
||||
const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData);
|
||||
|
||||
return decrypted;
|
||||
},
|
||||
|
||||
encryptString: async (password: string, salt: CryptoBuffer, data: string, encoding: CryptoBufferEncoding, encryptionParameters: EncryptionParameters) => {
|
||||
return crypto.encrypt(password, salt, Buffer.from(data, encoding), encryptionParameters);
|
||||
},
|
||||
|
||||
bufferToString: (buffer: CryptoBuffer, encoding: CryptoBufferEncoding) => {
|
||||
if (Buffer.isBuffer(buffer)) return buffer.toString(encoding);
|
||||
return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength).toString(encoding);
|
||||
},
|
||||
|
||||
generateNonce: generateNonceShared,
|
||||
|
||||
increaseNonce: increaseNonceShared,
|
||||
};
|
||||
|
||||
setRandomBytesImplementation(crypto.randomBytes);
|
||||
|
||||
export default crypto;
|
||||
在新工单中引用
屏蔽一个用户