Skip to content
Merged
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
33 changes: 19 additions & 14 deletions lib/tdf3/src/ciphers/aes-gcm-cipher.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Binary } from '../binary.js';
import { Algorithms } from './algorithms.js';
import { SymmetricCipher } from './symmetric-cipher-base.js';
import { decryptBufferSource } from '../crypto/core/symmetric.js';
import { concatUint8 } from '../utils/index.js';

import {
Expand All @@ -16,20 +17,17 @@ const IV_LENGTH = 12;
type ProcessGcmPayload = {
payload: Binary;
payloadIv: Binary;
payloadAuthTag: Binary;
};
// Should this be a Binary, Buffer, or... both?
function processGcmPayload(source: ArrayBuffer): ProcessGcmPayload {
// Read the 12 byte IV from the beginning of the stream
const payloadIv = Binary.fromArrayBuffer(source.slice(0, 12));

// Slice the final 16 bytes of the buffer for the authentication tag
const payloadAuthTag = Binary.fromArrayBuffer(source.slice(-16));

return {
payload: Binary.fromArrayBuffer(source.slice(12, -16)),
// WebCrypto AES-GCM expects ciphertext with the auth tag appended, so keep
// the tag attached instead of splitting and re-concatenating it later.
payload: Binary.fromArrayBuffer(source.slice(12)),
payloadIv,
payloadAuthTag,
};
}

Expand Down Expand Up @@ -64,18 +62,25 @@ export class AesGcmCipher extends SymmetricCipher {
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
override async decrypt(
buffer: ArrayBuffer,
buffer: ArrayBuffer | Uint8Array,
key: SymmetricKey,
iv?: Binary
): Promise<DecryptResult> {
const { payload, payloadIv, payloadAuthTag } = processGcmPayload(buffer);
const input = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);

if (this.cryptoService.name === 'BrowserNativeCryptoService') {
return decryptBufferSource(
input.subarray(12),
key,
input.subarray(0, 12),
Algorithms.AES_256_GCM
);
}

return this.cryptoService.decrypt(
payload,
key,
payloadIv,
Algorithms.AES_256_GCM,
payloadAuthTag
const { payload, payloadIv } = processGcmPayload(
input.buffer.slice(input.byteOffset, input.byteOffset + input.byteLength)
);

return this.cryptoService.decrypt(payload, key, payloadIv, Algorithms.AES_256_GCM);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
6 changes: 5 additions & 1 deletion lib/tdf3/src/ciphers/symmetric-cipher-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,9 @@ export abstract class SymmetricCipher {

abstract encrypt(payload: Binary, key: SymmetricKey, iv: Binary): Promise<EncryptResult>;

abstract decrypt(payload: Uint8Array, key: SymmetricKey, iv?: Binary): Promise<DecryptResult>;
abstract decrypt(
payload: ArrayBuffer | Uint8Array,
key: SymmetricKey,
iv?: Binary
): Promise<DecryptResult>;
}
62 changes: 48 additions & 14 deletions lib/tdf3/src/crypto/core/symmetric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ import { unwrapSymmetricKey, wrapSymmetricKey } from './keys.js';

const ENC_DEC_METHODS: KeyUsage[] = ['encrypt', 'decrypt'];

function asUint8ArrayView(buffer: BufferSource): Uint8Array {
if (buffer instanceof Uint8Array) {
return buffer;
}
if (ArrayBuffer.isView(buffer)) {
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
}
return new Uint8Array(buffer);
}

/**
* Generate a random symmetric key (opaque).
* @param length - Key length in bytes (default 32 for AES-256)
Expand Down Expand Up @@ -61,7 +71,23 @@ export function decrypt(
algorithm?: AlgorithmUrn,
authTag?: Binary
): Promise<DecryptResult> {
return _doDecrypt(payload, key, iv, algorithm, authTag);
return _doDecryptBufferSource(
payload.asArrayBuffer(),
key,
iv.asArrayBuffer(),
algorithm,
authTag?.asArrayBuffer()
);
}

export function decryptBufferSource(
payload: BufferSource,
key: SymmetricKey,
iv: BufferSource,
algorithm?: AlgorithmUrn,
authTag?: BufferSource
): Promise<DecryptResult> {
return _doDecryptBufferSource(payload, key, iv, algorithm, authTag);
}

/**
Expand Down Expand Up @@ -115,32 +141,33 @@ async function _doEncrypt(
};
}

async function _doDecrypt(
payload: Binary,
async function _doDecryptBufferSource(
payload: BufferSource,
key: SymmetricKey,
iv: Binary,
iv: BufferSource,
algorithm?: AlgorithmUrn,
authTag?: Binary
authTag?: BufferSource
): Promise<DecryptResult> {
console.assert(payload != null);
console.assert(key != null);
console.assert(iv != null);

let payloadBuffer = payload.asArrayBuffer();
let payloadBuffer: BufferSource = payload;

// Concat the the auth tag to the payload for decryption
if (authTag) {
const authTagBuffer = authTag.asArrayBuffer();
const gcmPayload = new Uint8Array(payloadBuffer.byteLength + authTagBuffer.byteLength);
gcmPayload.set(new Uint8Array(payloadBuffer), 0);
gcmPayload.set(new Uint8Array(authTagBuffer), payloadBuffer.byteLength);
payloadBuffer = gcmPayload.buffer;
const payloadBytes = asUint8ArrayView(payloadBuffer);
const authTagBytes = asUint8ArrayView(authTag);
const gcmPayload = new Uint8Array(payloadBytes.byteLength + authTagBytes.byteLength);
gcmPayload.set(payloadBytes, 0);
gcmPayload.set(authTagBytes, payloadBytes.byteLength);
payloadBuffer = gcmPayload;
}

const algoDomString = getSymmetricAlgoDomString(iv, algorithm);
const ivBytes = asUint8ArrayView(iv);
const algoDomString = getSymmetricAlgoDomStringFromIv(ivBytes, algorithm);
const keyBytes = unwrapSymmetricKey(key);
const importedKey = await _importKey(keyBytes, algoDomString);
algoDomString.iv = iv.asArrayBuffer();

const decrypted = await crypto.subtle
.decrypt(algoDomString, importedKey, payloadBuffer)
Expand Down Expand Up @@ -168,6 +195,13 @@ function _importKey(keyBytes: Uint8Array, algorithm: AesCbcParams | AesGcmParams
function getSymmetricAlgoDomString(
iv: Binary,
algorithm?: AlgorithmUrn
): AesCbcParams | AesGcmParams {
return getSymmetricAlgoDomStringFromIv(asUint8ArrayView(iv.asArrayBuffer()), algorithm);
}

function getSymmetricAlgoDomStringFromIv(
iv: Uint8Array,
algorithm?: AlgorithmUrn
): AesCbcParams | AesGcmParams {
let nativeAlgorithm = 'AES-CBC';
if (algorithm === Algorithms.AES_256_GCM) {
Expand All @@ -176,7 +210,7 @@ function getSymmetricAlgoDomString(

return {
name: nativeAlgorithm,
iv: iv.asArrayBuffer(),
iv,
};
}

Expand Down
2 changes: 1 addition & 1 deletion lib/tdf3/src/models/encryption-information.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class SplitKey {
return this.cipher.encrypt(contentBinary, key, ivBinary);
}

async decrypt(content: Uint8Array, key: SymmetricKey): Promise<DecryptResult> {
async decrypt(content: ArrayBuffer | Uint8Array, key: SymmetricKey): Promise<DecryptResult> {
return this.cipher.decrypt(content, key);
}

Expand Down
29 changes: 19 additions & 10 deletions lib/tdf3/src/tdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,16 +1074,17 @@ async function fetchAndDecryptChunkSlice({
specVersion: string;
slice: Chunk[];
}) {
const firstChunk = slice[0];
let buffer!: Uint8Array;
const bufferSize = slice.reduce(
(currentVal, { encryptedSegmentSize }) => currentVal + (encryptedSegmentSize as number),
0
);
try {
const bufferSize = slice.reduce(
(currentVal, { encryptedSegmentSize }) => currentVal + (encryptedSegmentSize as number),
0
);
buffer = await zipReader.getPayloadSegment(
centralDirectory,
'0.payload',
slice[0].encryptedOffset,
firstChunk.encryptedOffset,
bufferSize
);
} catch (error) {
Expand Down Expand Up @@ -1268,7 +1269,8 @@ export async function sliceAndDecrypt({
specVersion: string;
}) {
for (const index in slice) {
const { encryptedOffset, encryptedSegmentSize, plainSegmentSize } = slice[index];
const chunk = slice[index];
const { encryptedOffset, encryptedSegmentSize, plainSegmentSize } = chunk;

const offset =
slice[0].encryptedOffset === 0 ? encryptedOffset : encryptedOffset % slice[0].encryptedOffset;
Expand All @@ -1282,7 +1284,7 @@ export async function sliceAndDecrypt({
const result = await decryptChunk(
encryptedChunk,
reconstructedKey,
slice[index]['hash'],
chunk.hash,
cipher,
segmentIntegrityAlgorithm,
specVersion,
Expand All @@ -1293,9 +1295,9 @@ export async function sliceAndDecrypt({
`incorrect segment size: found [${result.payload.length()}], expected [${plainSegmentSize}]`
);
}
slice[index].decryptedChunk.set(result);
chunk.decryptedChunk.set(result);
} catch (e) {
slice[index].decryptedChunk.reject(e);
chunk.decryptedChunk.reject(e);
}
}
}
Expand Down Expand Up @@ -1470,10 +1472,17 @@ export async function decryptStreamFrom(
const chunk = chunks[nextChunkIndex];
const decryptedSegment = await chunk.decryptedChunk;
const encryptedSegmentSize = chunk.encryptedSegmentSize ?? 0;
const plainChunk = new Uint8Array(decryptedSegment.payload.asArrayBuffer());

controller.enqueue(new Uint8Array(decryptedSegment.payload.asByteArray()));
controller.enqueue(plainChunk);
progress += encryptedSegmentSize;
cfg.progressHandler?.(progress);
// Release the resolved plaintext held by the consumed mailbox so long
// browser decrypts do not retain every prior segment in memory.
chunks[nextChunkIndex] = {
...chunk,
decryptedChunk: mailbox<DecryptResult>(),
};
nextChunkIndex += 1;
scheduler?.markConsumed();
},
Expand Down
3 changes: 1 addition & 2 deletions lib/tdf3/src/utils/zip-reader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,7 @@ export class ZipReader {
cdObj.relativeOffsetOfLocalHeader + cdObj.headerLength + encrpytedSegmentOffset;
// TODO: what's the exact byte start?
const byteEnd = byteStart + encryptedSegmentSize;

return await this.getChunk(byteStart, byteEnd);
return this.getChunk(byteStart, byteEnd);
}

/**
Expand Down
36 changes: 36 additions & 0 deletions lib/tests/mocha/unit/crypto/crypto-service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { assert, expect } from 'chai';

import { Algorithms } from '../../../../tdf3/src/ciphers/index.js';
import { AesGcmCipher } from '../../../../tdf3/src/ciphers/aes-gcm-cipher.js';
import {
decrypt,
decryptWithPrivateKey,
Expand All @@ -24,6 +25,7 @@ import {
verifyHmac,
verify,
} from '../../../../tdf3/src/crypto/index.js';
import * as DefaultCryptoService from '../../../../tdf3/src/crypto/index.js';
import { hex } from '../../../../src/encodings/index.js';
import { Binary } from '../../../../tdf3/src/binary.js';
import { decodeArrayBuffer, encodeArrayBuffer } from '../../../../src/encodings/base64.js';
Expand Down Expand Up @@ -243,6 +245,40 @@ describe('Crypto Service', () => {
expect(decrypted.payload.asString()).to.be.equal(rawData);
});

it('should decrypt aes_256_gcm ciphertext from Uint8Array input', async () => {
const rawData = 'hello world';
const payload = Binary.fromString(rawData);

const keyBytes = new Uint8Array(
decodeArrayBuffer('cvR6X2vLG5ap13ssLxRjOV1KOjJfraYpD8D+97zdtY4=')
);
const key = await importSymmetricKey(keyBytes);
const iv = Binary.fromArrayBuffer(crypto.getRandomValues(new Uint8Array(12)).buffer);
const cipher = new AesGcmCipher(DefaultCryptoService);

const encrypted = await cipher.encrypt(payload, key, iv);
const decrypted = await cipher.decrypt(new Uint8Array(encrypted.payload.asArrayBuffer()), key);

expect(decrypted.payload.asString()).to.equal(rawData);
});

it('should decrypt aes_256_gcm ciphertext from ArrayBuffer input', async () => {
const rawData = 'hello world';
const payload = Binary.fromString(rawData);

const keyBytes = new Uint8Array(
decodeArrayBuffer('cvR6X2vLG5ap13ssLxRjOV1KOjJfraYpD8D+97zdtY4=')
);
const key = await importSymmetricKey(keyBytes);
const iv = Binary.fromArrayBuffer(crypto.getRandomValues(new Uint8Array(12)).buffer);
const cipher = new AesGcmCipher(DefaultCryptoService);

const encrypted = await cipher.encrypt(payload, key, iv);
const decrypted = await cipher.decrypt(encrypted.payload.asArrayBuffer(), key);

expect(decrypted.payload.asString()).to.equal(rawData);
});

describe('generateECKeyPair', () => {
it('should generate P-256 key pair', async () => {
const keyPair = await generateECKeyPair('P-256');
Expand Down
4 changes: 3 additions & 1 deletion web-app/tests/tests/acts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export const appUrl = 'http://localhost:65432/';

export const authorize = async (page: Page) => {
await page.goto('http://localhost:65432/');
await page.goto(appUrl);
// If we are logged in, return early.
const sessionState = await page.locator('#sessionState').textContent();
if (sessionState === 'loggedin') {
Expand Down
4 changes: 2 additions & 2 deletions web-app/tests/tests/huge.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect, type Page } from '@playwright/test';
import fs from 'node:fs';
import { authorize, loadFile } from './acts.js';
import { appUrl, authorize, loadFile } from './acts.js';

test.beforeEach(async ({ page }) => {
page.on('pageerror', (err) => {
Expand Down Expand Up @@ -42,7 +42,7 @@ test('Large File', async ({ page }) => {

await page.locator('#randomSelector').clear();
await loadFile(page, cipherTextPath);
const plainDownloadPromise = await page.waitForEvent('download', { timeout: 60000 });
const plainDownloadPromise = page.waitForEvent('download', { timeout: 60000 });
await page.locator('#fileSink').click();
await page.locator('#decryptButton').click();
const download2 = await plainDownloadPromise;
Expand Down
Loading