Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 43 additions & 34 deletions lib/crypto/crypto-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { KdbxError } from '../errors/kdbx-error';
import { ErrorCodes } from '../defs/consts';
import { arrayToBuffer, hexToBytes } from '../utils/byte-utils';
import { Bytes } from '../defs/bytes';
import { ChaCha20 } from './chacha20';
import * as nodeCrypto from 'crypto';

Expand All @@ -24,57 +25,61 @@ const EmptySha512 =
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
const MaxRandomQuota = 65536;

export function sha256(data: ArrayBuffer): Promise<ArrayBuffer> {
if (!data.byteLength) {
export function sha256(data: Bytes): Promise<ArrayBuffer> {
const dataArr = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
if (!dataArr.byteLength) {
return Promise.resolve(arrayToBuffer(hexToBytes(EmptySha256)));
}
if (global.crypto?.subtle) {
return global.crypto.subtle.digest({ name: 'SHA-256' }, data);
return global.crypto.subtle.digest({ name: 'SHA-256' }, arrayToBuffer(dataArr));
} else {
return new Promise((resolve) => {
const sha = nodeCrypto.createHash('sha256');
const hash = sha.update(Buffer.from(data)).digest();
const hash = sha.update(Buffer.from(dataArr)).digest();
resolve(hash.buffer);
});
}
}

export function sha512(data: ArrayBuffer): Promise<ArrayBuffer> {
if (!data.byteLength) {
export function sha512(data: Bytes): Promise<ArrayBuffer> {
const dataArr = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
if (!dataArr.byteLength) {
return Promise.resolve(arrayToBuffer(hexToBytes(EmptySha512)));
}
if (global.crypto?.subtle) {
return global.crypto.subtle.digest({ name: 'SHA-512' }, data);
return global.crypto.subtle.digest({ name: 'SHA-512' }, arrayToBuffer(dataArr));
} else {
return new Promise((resolve) => {
const sha = nodeCrypto.createHash('sha512');
const hash = sha.update(Buffer.from(data)).digest();
const hash = sha.update(Buffer.from(dataArr)).digest();
resolve(hash.buffer);
});
}
}

export function hmacSha256(key: ArrayBuffer, data: ArrayBuffer): Promise<ArrayBuffer> {
export function hmacSha256(key: Bytes, data: Bytes): Promise<ArrayBuffer> {
const keyArr = key instanceof ArrayBuffer ? new Uint8Array(key) : key;
const dataArr = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
if (global.crypto?.subtle) {
const algo = { name: 'HMAC', hash: { name: 'SHA-256' } };
return global.crypto.subtle
.importKey('raw', key, algo, false, ['sign'])
.importKey('raw', arrayToBuffer(keyArr), algo, false, ['sign'])
.then((subtleKey) => {
return global.crypto.subtle.sign(algo, subtleKey, data);
return global.crypto.subtle.sign(algo, subtleKey, arrayToBuffer(dataArr));
});
} else {
return new Promise((resolve) => {
const hmac = nodeCrypto.createHmac('sha256', Buffer.from(key));
const hash = hmac.update(Buffer.from(data)).digest();
const hmac = nodeCrypto.createHmac('sha256', Buffer.from(keyArr));
const hash = hmac.update(Buffer.from(dataArr)).digest();
resolve(hash.buffer);
});
}
}

export abstract class AesCbc {
abstract importKey(key: ArrayBuffer): Promise<void>;
abstract encrypt(data: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer>;
abstract decrypt(data: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer>;
abstract importKey(key: Bytes): Promise<void>;
abstract encrypt(data: Bytes, iv: Bytes): Promise<ArrayBuffer>;
abstract decrypt(data: Bytes, iv: Bytes): Promise<ArrayBuffer>;
}

class AesCbcSubtle extends AesCbc {
Expand All @@ -87,24 +92,29 @@ class AesCbcSubtle extends AesCbc {
return this._key;
}

importKey(key: ArrayBuffer): Promise<void> {
importKey(key: Bytes): Promise<void> {
const keyArr = key instanceof ArrayBuffer ? new Uint8Array(key) : key;
return global.crypto.subtle
.importKey('raw', key, { name: 'AES-CBC' }, false, ['encrypt', 'decrypt'])
.importKey('raw', arrayToBuffer(keyArr), { name: 'AES-CBC' }, false, ['encrypt', 'decrypt'])
.then((key) => {
this._key = key;
});
}

encrypt(data: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer> {
encrypt(data: Bytes, iv: Bytes): Promise<ArrayBuffer> {
const dataArr = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
const ivArr = iv instanceof ArrayBuffer ? new Uint8Array(iv) : iv;
return global.crypto.subtle.encrypt(
{ name: 'AES-CBC', iv },
{ name: 'AES-CBC', iv: arrayToBuffer(ivArr) },
this.key,
data
arrayToBuffer(dataArr)
) as Promise<ArrayBuffer>;
}

decrypt(data: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer> {
return global.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, this.key, data).catch(() => {
decrypt(data: Bytes, iv: Bytes): Promise<ArrayBuffer> {
const dataArr = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
const ivArr = iv instanceof ArrayBuffer ? new Uint8Array(iv) : iv;
return global.crypto.subtle.decrypt({ name: 'AES-CBC', iv: arrayToBuffer(ivArr) }, this.key, arrayToBuffer(dataArr)).catch(() => {
throw new KdbxError(ErrorCodes.InvalidKey, 'invalid key');
}) as Promise<ArrayBuffer>;
}
Expand Down Expand Up @@ -183,14 +193,13 @@ export function random(len: number): Uint8Array {
}
}

export function chacha20(
data: ArrayBuffer,
key: ArrayBuffer,
iv: ArrayBuffer
): Promise<ArrayBuffer> {
export function chacha20(data: Bytes, key: Bytes, iv: Bytes): Promise<ArrayBuffer> {
return Promise.resolve().then(() => {
const algo = new ChaCha20(new Uint8Array(key), new Uint8Array(iv));
return arrayToBuffer(algo.encrypt(new Uint8Array(data)));
const keyArr = key instanceof ArrayBuffer ? new Uint8Array(key) : key;
const ivArr = iv instanceof ArrayBuffer ? new Uint8Array(iv) : iv;
const dataArr = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
const algo = new ChaCha20(keyArr, ivArr);
return arrayToBuffer(algo.encrypt(dataArr));
});
}

Expand All @@ -201,8 +210,8 @@ export type Argon2Type = typeof Argon2TypeArgon2d | typeof Argon2TypeArgon2id;
export type Argon2Version = 0x10 | 0x13;

export type Argon2Fn = (
password: ArrayBuffer,
salt: ArrayBuffer,
password: Bytes,
salt: Bytes,
memory: number,
iterations: number,
length: number,
Expand All @@ -214,8 +223,8 @@ export type Argon2Fn = (
let argon2impl: Argon2Fn | undefined;

export function argon2(
password: ArrayBuffer,
salt: ArrayBuffer,
password: Bytes,
salt: Bytes,
memory: number,
iterations: number,
length: number,
Expand Down
19 changes: 11 additions & 8 deletions lib/crypto/hashed-block-transform.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { BinaryStream } from '../utils/binary-stream';
import * as CryptoEngine from '../crypto/crypto-engine';
import { KdbxError } from '../errors/kdbx-error';
import { arrayBufferEquals } from '../utils/byte-utils';
import { arrayBufferEquals, arrayToBuffer } from '../utils/byte-utils';
import { ErrorCodes } from '../defs/consts';

const BlockSize = 1024 * 1024;

export function decrypt(data: ArrayBuffer): Promise<ArrayBuffer> {
import { Bytes } from '../defs/bytes';

export function decrypt(data: Bytes): Promise<ArrayBuffer> {
return Promise.resolve().then(() => {
const stm = new BinaryStream(data);
const stm = new BinaryStream(arrayToBuffer(data));
const buffers: ArrayBuffer[] = [];
let // blockIndex = 0,
blockLength = 0,
Expand Down Expand Up @@ -44,9 +46,10 @@ export function decrypt(data: ArrayBuffer): Promise<ArrayBuffer> {
});
}

export function encrypt(data: ArrayBuffer): Promise<ArrayBuffer> {
export function encrypt(data: Bytes): Promise<ArrayBuffer> {
return Promise.resolve().then(() => {
let bytesLeft = data.byteLength;
const dataArr = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
let bytesLeft = dataArr.byteLength;
let currentOffset = 0,
blockIndex = 0,
totalLength = 0;
Expand All @@ -57,8 +60,8 @@ export function encrypt(data: ArrayBuffer): Promise<ArrayBuffer> {
const blockLength = Math.min(BlockSize, bytesLeft);
bytesLeft -= blockLength;

const blockData = data.slice(currentOffset, currentOffset + blockLength);
return CryptoEngine.sha256(blockData).then((blockHash) => {
const blockData = dataArr.subarray(currentOffset, currentOffset + blockLength);
return CryptoEngine.sha256(arrayToBuffer(blockData)).then((blockHash) => {
const blockBuffer = new ArrayBuffer(4 + 32 + 4);
const stm = new BinaryStream(blockBuffer);
stm.setUint32(blockIndex, true);
Expand All @@ -67,7 +70,7 @@ export function encrypt(data: ArrayBuffer): Promise<ArrayBuffer> {

buffers.push(blockBuffer);
totalLength += blockBuffer.byteLength;
buffers.push(blockData);
buffers.push(arrayToBuffer(blockData));
totalLength += blockData.byteLength;

blockIndex++;
Expand Down
32 changes: 18 additions & 14 deletions lib/crypto/hmac-block-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { Int64 } from '../utils/int64';
import { arrayBufferEquals, arrayToBuffer, zeroBuffer } from '../utils/byte-utils';
import * as CryptoEngine from '../crypto/crypto-engine';
import { BinaryStream } from '../utils/binary-stream';
import { Bytes } from '../defs/bytes';
import { KdbxError } from '../errors/kdbx-error';
import { ErrorCodes } from '../defs/consts';

const BlockSize = 1024 * 1024;

export function getHmacKey(key: ArrayBuffer, blockIndex: Int64): Promise<ArrayBuffer> {
const shaSrc = new Uint8Array(8 + key.byteLength);
shaSrc.set(new Uint8Array(key), 8);
export function getHmacKey(key: Bytes, blockIndex: Int64): Promise<ArrayBuffer> {
const keyArr = key instanceof ArrayBuffer ? new Uint8Array(key) : key;
const shaSrc = new Uint8Array(8 + keyArr.byteLength);
shaSrc.set(keyArr, 8);
const view = new DataView(shaSrc.buffer);
view.setUint32(0, blockIndex.lo, true);
view.setUint32(4, blockIndex.hi, true);
Expand All @@ -20,23 +22,24 @@ export function getHmacKey(key: ArrayBuffer, blockIndex: Int64): Promise<ArrayBu
}

function getBlockHmac(
key: ArrayBuffer,
key: Bytes,
blockIndex: number,
blockLength: number,
blockData: ArrayBuffer
blockData: Bytes
): Promise<ArrayBuffer> {
return getHmacKey(key, new Int64(blockIndex)).then((blockKey) => {
const blockDataForHash = new Uint8Array(blockData.byteLength + 4 + 8);
const blockDataArr = blockData instanceof ArrayBuffer ? new Uint8Array(blockData) : blockData;
const blockDataForHash = new Uint8Array(blockDataArr.byteLength + 4 + 8);
const blockDataForHashView = new DataView(blockDataForHash.buffer);
blockDataForHash.set(new Uint8Array(blockData), 4 + 8);
blockDataForHash.set(blockDataArr, 4 + 8);
blockDataForHashView.setInt32(0, blockIndex, true);
blockDataForHashView.setInt32(8, blockLength, true);
return CryptoEngine.hmacSha256(blockKey, blockDataForHash.buffer);
return CryptoEngine.hmacSha256(blockKey, arrayToBuffer(blockDataForHash));
});
}

export function decrypt(data: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
const stm = new BinaryStream(data);
export function decrypt(data: Bytes, key: Bytes): Promise<ArrayBuffer> {
const stm = new BinaryStream(arrayToBuffer(data));
return Promise.resolve().then(() => {
const buffers: ArrayBuffer[] = [];
let blockIndex = 0,
Expand Down Expand Up @@ -75,9 +78,10 @@ export function decrypt(data: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffe
});
}

export function encrypt(data: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffer> {
export function encrypt(data: Bytes, key: Bytes): Promise<ArrayBuffer> {
return Promise.resolve().then(() => {
let bytesLeft = data.byteLength;
const dataArr = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
let bytesLeft = dataArr.byteLength;
let currentOffset = 0,
blockIndex = 0,
totalLength = 0;
Expand All @@ -87,7 +91,7 @@ export function encrypt(data: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffe
const blockLength = Math.min(BlockSize, bytesLeft);
bytesLeft -= blockLength;

const blockData = data.slice(currentOffset, currentOffset + blockLength);
const blockData = dataArr.subarray(currentOffset, currentOffset + blockLength);
return getBlockHmac(key, blockIndex, blockLength, blockData).then((blockHash) => {
const blockBuffer = new ArrayBuffer(32 + 4);
const stm = new BinaryStream(blockBuffer);
Expand All @@ -98,7 +102,7 @@ export function encrypt(data: ArrayBuffer, key: ArrayBuffer): Promise<ArrayBuffe
totalLength += blockBuffer.byteLength;

if (blockData.byteLength > 0) {
buffers.push(blockData);
buffers.push(arrayToBuffer(blockData));
totalLength += blockData.byteLength;
blockIndex++;
currentOffset += blockLength;
Expand Down
34 changes: 19 additions & 15 deletions lib/crypto/key-encryptor-kdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import * as KeyEncryptorAes from './key-encryptor-aes';
import { VarDictionary, VarDictionaryAnyValue } from '../utils/var-dictionary';
import { KdbxError } from '../errors/kdbx-error';
import { ErrorCodes, KdfId } from '../defs/consts';
import { bytesToBase64, zeroBuffer } from '../utils/byte-utils';
import { bytesToBase64, zeroBuffer, arrayToBuffer } from '../utils/byte-utils';
import { Argon2Type } from './crypto-engine';
import { Int64 } from '../utils/int64';
import { Bytes } from '../defs/bytes';

export function encrypt(key: ArrayBuffer, kdfParams: VarDictionary): Promise<ArrayBuffer> {
export function encrypt(key: Bytes, kdfParams: VarDictionary): Promise<ArrayBuffer> {
const uuid = kdfParams.get('$UUID');
if (!uuid || !(uuid instanceof ArrayBuffer)) {
if (!uuid || (!(uuid instanceof ArrayBuffer) && !(uuid instanceof Uint8Array))) {
return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'no kdf uuid'));
}
const kdfUuid = bytesToBase64(uuid);
Expand All @@ -26,12 +27,12 @@ export function encrypt(key: ArrayBuffer, kdfParams: VarDictionary): Promise<Arr
}

function encryptArgon2(
key: ArrayBuffer,
key: Bytes,
kdfParams: VarDictionary,
argon2type: Argon2Type
): Promise<ArrayBuffer> {
const salt = kdfParams.get('S');
if (!(salt instanceof ArrayBuffer) || salt.byteLength !== 32) {
if (!(salt instanceof ArrayBuffer) && !(salt instanceof Uint8Array) || salt.byteLength !== 32) {
return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'bad argon2 salt'));
}

Expand Down Expand Up @@ -77,9 +78,9 @@ function encryptArgon2(
);
}

function encryptAes(key: ArrayBuffer, kdfParams: VarDictionary) {
function encryptAes(key: Bytes, kdfParams: VarDictionary) {
const salt = kdfParams.get('S');
if (!(salt instanceof ArrayBuffer) || salt.byteLength !== 32) {
if (!(salt instanceof ArrayBuffer) && !(salt instanceof Uint8Array) || salt.byteLength !== 32) {
return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'bad aes salt'));
}

Expand All @@ -88,14 +89,17 @@ function encryptAes(key: ArrayBuffer, kdfParams: VarDictionary) {
return Promise.reject(new KdbxError(ErrorCodes.FileCorrupt, 'bad aes rounds'));
}

return KeyEncryptorAes.encrypt(new Uint8Array(key), new Uint8Array(salt), rounds).then(
(key) => {
return CryptoEngine.sha256(key).then((hash) => {
zeroBuffer(key);
return hash;
});
}
);
const keyArr = key instanceof ArrayBuffer ? new Uint8Array(key) : key;
const saltArr = salt instanceof ArrayBuffer ? new Uint8Array(salt) : salt;
return KeyEncryptorAes.encrypt(keyArr, saltArr, rounds).then((key) => {
// sha256 expects an ArrayBuffer; convert the Uint8Array result to
// a plain ArrayBuffer to satisfy TypeScript and avoid SharedArrayBuffer
// incompatibilities.
return CryptoEngine.sha256(arrayToBuffer(key)).then((hash) => {
zeroBuffer(key);
return hash;
});
});
}

function toNumber(number: VarDictionaryAnyValue): number | undefined {
Expand Down
Loading