Skip to content

Commit

Permalink
Redesigned hashing
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexanderOMara committed Jan 22, 2025
1 parent 6d1c950 commit eea45ce
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 135 deletions.
35 changes: 33 additions & 2 deletions blob/codedirectorybuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,41 @@ import {
PLATFORM_MACOS,
UINT32_MAX,
} from '../const.ts';
import type { Reader } from '../util/reader.ts';
import { CodeDirectory } from './codedirectory.ts';
import { CodeDirectoryBuilder } from './codedirectorybuilder.ts';
import { CodeDirectoryScatter } from './codedirectoryscatter.ts';

class ErrorReader implements Reader {
#size: number;

#type: string;

constructor(size: number, type: string = '') {
this.#size = size;
this.#type = type;
}

public get size(): number {
return this.#size;
}

public get type(): string {
return this.#type;
}

slice(start?: number, end?: number, contentType?: string): Reader {
start ??= 0;
end ??= this.#size;
return new ErrorReader(start < end ? end - start : 0, contentType);
}

// deno-lint-ignore require-await
public async arrayBuffer(): Promise<ArrayBuffer> {
throw new Error('BadReader');
}
}

Deno.test('hashType', () => {
let builder = new CodeDirectoryBuilder(kSecCodeSignatureHashSHA1);
assertEquals(builder.hashType(), kSecCodeSignatureHashSHA1);
Expand Down Expand Up @@ -201,6 +232,6 @@ Deno.test('generatePreEncryptHashes', async () => {

Deno.test('Read valiation', async () => {
const builder = new CodeDirectoryBuilder(kSecCodeSignatureHashSHA1);
builder.executable(new Blob([]), 1024, 0, UINT32_MAX + 1);
await assertRejects(() => builder.build(), Error, 'Read from: 0 ');
builder.executable(new ErrorReader(1024), 1024, 0, UINT32_MAX + 1);
await assertRejects(() => builder.build(), Error, 'BadReader');
});
14 changes: 2 additions & 12 deletions blob/codedirectorybuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,7 @@ async function generateHash(
offset: number,
length: number,
): Promise<ArrayBuffer> {
const data = await reader.slice(offset, offset + length).arrayBuffer();
const { byteLength } = data;
if (byteLength !== length) {
throw new RangeError(
`Read from: ${offset} (${byteLength} != ${length})`,
);
}
await hasher.update(data, true);
return await hasher.finish();
return await hasher.digest(reader.slice(offset, offset + length));
}

/**
Expand Down Expand Up @@ -227,9 +219,7 @@ export class CodeDirectoryBuilder {
data: ArrayBufferReal | BufferView,
): Promise<void> {
slot = specialSlot(slot);
const hash = this.getHash();
await hash.update(data);
this.mSpecial.set(slot, await hash.finish());
this.mSpecial.set(slot, await this.getHash().digest(data));
if (slot > this.mSpecialSlots) {
this.mSpecialSlots = slot;
}
Expand Down
126 changes: 85 additions & 41 deletions hash/cchashinstance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,69 @@ import {
kCCDigestSkein512,
} from '../const.ts';
import { hex } from '../spec/hex.ts';
import type { Reader } from '../util/reader.ts';
import { CCHashInstance } from './cchashinstance.ts';

class ShortReader implements Reader {
#size: number;

#type: string;

constructor(size: number, type: string = '') {
this.#size = size;
this.#type = type;
}

public get size(): number {
return this.#size;
}

public get type(): string {
return this.#type;
}

public slice(start?: number, end?: number, contentType?: string): Reader {
start ??= 0;
end ??= this.#size;
return new ShortReader(start < end ? end - start : 0, contentType);
}

// deno-lint-ignore require-await
public async arrayBuffer(): Promise<ArrayBuffer> {
return new ArrayBuffer(this.#size - 1);
}
}

class LongReader implements Reader {
#size: number;

#type: string;

constructor(size: number, type: string = '') {
this.#size = size;
this.#type = type;
}

public get size(): number {
return this.#size;
}

public get type(): string {
return this.#type;
}

public slice(start?: number, end?: number, contentType?: string): Reader {
start ??= 0;
end ??= this.#size;
return new LongReader(start < end ? end - start : 0, contentType);
}

// deno-lint-ignore require-await
public async arrayBuffer(): Promise<ArrayBuffer> {
return new ArrayBuffer(this.#size + 1);
}
}

// 'ABCD':
const expected = [
[
Expand Down Expand Up @@ -63,16 +124,24 @@ const unsupported = [
kCCDigestSkein512,
];

Deno.test('CCHashInstance unsupported', () => {
for (const alg of unsupported) {
const tag = `alg=${alg}`;
assertThrows(
() => new CCHashInstance(alg),
RangeError,
`Unsupported hash algorithm: ${alg}`,
tag,
);
}
});

Deno.test('CCHashInstance full', async () => {
for (const [alg, expt] of expected) {
const tag = `alg=${alg}`;
const hash = new CCHashInstance(alg);
// deno-lint-ignore no-await-in-loop
await hash.update(new TextEncoder().encode('AB'));
// deno-lint-ignore no-await-in-loop
await hash.update(new TextEncoder().encode('CD'));
// deno-lint-ignore no-await-in-loop
const result = await hash.finish();
const result = await hash.digest(new TextEncoder().encode('ABCD'));
assertEquals(result.byteLength, hash.digestLength(), tag);
assertEquals(hex(new Uint8Array(result)), expt, tag);
}
Expand All @@ -85,51 +154,26 @@ Deno.test('CCHashInstance truncate', async () => {
const exptHex = expt.slice(0, truncate * 2);
const hash = new CCHashInstance(alg, truncate);
// deno-lint-ignore no-await-in-loop
await hash.update(new TextEncoder().encode('ABCD'));
// deno-lint-ignore no-await-in-loop
const result = await hash.finish();
const result = await hash.digest(new TextEncoder().encode('ABCD'));
assertEquals(result.byteLength, truncate, tag);
assertEquals(hex(new Uint8Array(result)), exptHex, tag);
}
});

Deno.test('CCHashInstance unsupported', () => {
for (const alg of unsupported) {
const tag = `alg=${alg}`;
assertThrows(
() => new CCHashInstance(alg),
RangeError,
`Unsupported hash algorithm: ${alg}`,
tag,
);
}
});

Deno.test('CCHashInstance finish finished', async () => {
Deno.test('CCHashInstance short read', async () => {
const hash = new CCHashInstance(kCCDigestSHA1);
await hash.finish();
await assertRejects(() => hash.finish(), Error, 'Digest finished');
});

Deno.test('CCHashInstance update finished', async () => {
const hash = new CCHashInstance(kCCDigestSHA1);
await hash.finish();
await assertRejects(
() => hash.update(new Uint8Array()),
Error,
'Digest finished',
() => hash.digest(new ShortReader(1024)),
RangeError,
'Read size off by: -1',
);
});

Deno.test('CCHashInstance transfer', async () => {
Deno.test('CCHashInstance long read', async () => {
const hash = new CCHashInstance(kCCDigestSHA1);
const copied = new ArrayBuffer(4);
await hash.update(copied);
assertEquals(copied.byteLength, 4);
const transfered = new ArrayBuffer(4);
await hash.update(transfered, true);
assertEquals(transfered.byteLength, 0);
const transferedSlice = new ArrayBuffer(8);
await hash.update(new Uint8Array(transferedSlice, 2, 4), true);
assertEquals(transferedSlice.byteLength, 0);
await assertRejects(
() => hash.digest(new LongReader(1024)),
RangeError,
'Read size off by: 1',
);
});
107 changes: 40 additions & 67 deletions hash/cchashinstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,24 @@ import {
kCCDigestSHA512,
} from '../const.ts';
import { DynamicHash } from './dynamichash.ts';
import type { Reader } from '../util/reader.ts';

// Workaround for missing types.
declare const crypto: {
subtle: {
digest: (alg: string, data: ArrayBuffer) => Promise<ArrayBuffer>;
digest: (
alg: string,
data: ArrayBufferView | ArrayBuffer,
) => Promise<ArrayBuffer>;
};
};
declare function structuredClone<T>(
value: T,
options?: {
transfer?: ArrayBufferReal[];
},
): T;

// Supported hash algorithms with their Web Crypto names and lengths.
const algorithims = new Map<number, [string, number]>([
[kCCDigestSHA1, ['SHA-1', 20]],
[kCCDigestSHA256, ['SHA-256', 32]],
[kCCDigestSHA384, ['SHA-384', 48]],
[kCCDigestSHA512, ['SHA-512', 64]],
// Supported hash algorithms with their names and lengths.
const algorithims = new Map<number, [number, string]>([
[kCCDigestSHA1, [20, 'SHA-1']],
[kCCDigestSHA256, [32, 'SHA-256']],
[kCCDigestSHA384, [48, 'SHA-384']],
[kCCDigestSHA512, [64, 'SHA-512']],
]);

/**
Expand All @@ -42,13 +40,6 @@ export class CCHashInstance extends DynamicHash {
*/
private mTruncate: number;

/**
* Data buffer.
* Web Crypto digest lacks streaming support.
* No choice but to buffer all the data.
*/
private mData: ArrayBuffer[] | null;

/**
* CCHashInstance constructor.
*
Expand All @@ -62,60 +53,42 @@ export class CCHashInstance extends DynamicHash {
super();
this.mDigest = alg;
this.mTruncate = truncate;
this.mData = [];
}

public digestLength(): number {
return this.mTruncate || algorithims.get(this.mDigest)![1];
return this.mTruncate || algorithims.get(this.mDigest)![0];
}

// deno-lint-ignore require-await
public async update(
data: ArrayBufferReal | BufferView,
transfer?: boolean,
): Promise<void> {
const mData = this.mData;
if (!mData) {
throw new Error('Digest finished');
}
let buffer, byteOffset, byteLength;
if ('buffer' in data) {
buffer = data.buffer;
byteLength = data.byteLength;
byteOffset = data.byteOffset;
} else {
buffer = data;
byteLength = data.byteLength;
byteOffset = 0;
}
if (!transfer) {
mData.push(buffer.slice(byteOffset, byteOffset + byteLength));
return;
}
let b = structuredClone(buffer, { transfer: [buffer] });
if (byteOffset || byteLength !== b.byteLength) {
b = b.slice(byteOffset, byteOffset + byteLength);
}
mData.push(b);
}

public async finish(): Promise<ArrayBuffer> {
const [name] = algorithims.get(this.mDigest)!;
public async digest(
source: Reader | ArrayBufferReal | BufferView,
): Promise<ArrayBuffer> {
const { mTruncate } = this;
const mData = this.mData;
if (!mData) {
throw new Error('Digest finished');
}
let total = 0;
for (const data of mData) {
total += data.byteLength;
}
const buffer = new Uint8Array(total);
this.mData = null;
for (let b, offset = 0; mData.length; offset += b.byteLength) {
buffer.set(new Uint8Array(b = mData.shift()!), offset);
const [, name] = algorithims.get(this.mDigest)!;
let digest;
if ('arrayBuffer' in source) {
const { size } = source;
digest = await crypto.subtle.digest(
name,
await source.arrayBuffer().then((d) => {
const diff = d.byteLength - size;
if (diff) {
throw new RangeError(`Read size off by: ${diff}`);
}
return d;
}),
);
} else {
digest = await crypto.subtle.digest(
name,
'buffer' in source
? source = new Uint8Array(
source.buffer,
source.byteOffset,
source.byteLength,
)
: source,
);
}
const digest = await crypto.subtle.digest(name, buffer);
return mTruncate ? digest.slice(0, mTruncate) : digest;
}
}
Loading

0 comments on commit eea45ce

Please sign in to comment.