Skip to content

Commit

Permalink
Merge branch 'jul/composite-resourceId-error-handling' into 'master'
Browse files Browse the repository at this point in the history
Rework error handling in crypto/resourceId

See merge request TankerHQ/sdk-js!967
  • Loading branch information
JMounier committed Mar 2, 2023
2 parents 237d207 + 1749520 commit 41ab0a3
Show file tree
Hide file tree
Showing 15 changed files with 103 additions and 39 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/Network/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,11 +446,11 @@ export class Client {
return challenge;
};

getResourceKey = async (resourceId: Uint8Array): Promise<b64string> => {
getResourceKey = async (resourceId: Uint8Array): Promise<b64string | null> => {
const query = `resource_ids[]=${urlize(resourceId)}`;
const { resource_keys: resourceKeys } = await this._apiCall(`/resource-keys?${query}`);
if (resourceKeys.length === 0) {
throw new InvalidArgument(`could not find key for resource: ${utils.toBase64(resourceId)}`);
return null;
}
return resourceKeys[0];
};
Expand Down
33 changes: 23 additions & 10 deletions packages/core/src/Resources/Manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Key, b64string } from '@tanker/crypto';
import { utils } from '@tanker/crypto';
import { UpgradeRequired } from '@tanker/errors';
import { errors as dbErrors } from '@tanker/datastore-base';

import { getKeyPublishEntryFromBlock } from './Serialize';
import { KeyDecryptor } from './KeyDecryptor';
Expand All @@ -13,7 +15,7 @@ import type ProvisionalIdentityManager from '../ProvisionalIdentity/Manager';

export type KeyResult = {
id: b64string;
key: Key;
key: Key | null;
};

export class ResourceManager {
Expand All @@ -35,7 +37,7 @@ export class ResourceManager {
this._resourceStore = resourceStore;
}

async findKeyFromResourceId(resourceId: Uint8Array): Promise<Key> {
async findKeyFromResourceId(resourceId: Uint8Array): Promise<Key | null> {
const b64resourceId = utils.toBase64(resourceId);

const result = await this._keyLookupCoalescer.run(this._findKeysFromResourceIds, [b64resourceId]);
Expand All @@ -44,16 +46,27 @@ export class ResourceManager {

_findKeysFromResourceIds = (b64resourceIds: Array<b64string>): Promise<Array<KeyResult>> => Promise.all(b64resourceIds.map(async (b64resourceId) => {
const resourceId = utils.fromBase64(b64resourceId);
let resourceKey = await this._resourceStore.findResourceKey(resourceId);
try {
let resourceKey = await this._resourceStore.findResourceKey(resourceId);

if (!resourceKey) {
const keyPublishBlock = await this._client.getResourceKey(resourceId);
const keyPublish = getKeyPublishEntryFromBlock(keyPublishBlock);
resourceKey = await this._keyDecryptor.keyFromKeyPublish(keyPublish);
await this._resourceStore.saveResourceKey(resourceId, resourceKey);
}
if (!resourceKey) {
const keyPublishBlock = await this._client.getResourceKey(resourceId);
if (!keyPublishBlock) {
return { id: b64resourceId, key: null };
}
const keyPublish = getKeyPublishEntryFromBlock(keyPublishBlock);
resourceKey = await this._keyDecryptor.keyFromKeyPublish(keyPublish);
await this._resourceStore.saveResourceKey(resourceId, resourceKey);
}

return { id: b64resourceId, key: resourceKey };
} catch (e) {
if (e instanceof dbErrors.VersionError) {
throw new UpgradeRequired(e);
}

return { id: b64resourceId, key: resourceKey };
throw e;
}
}));

saveResourceKey = (resourceId: Uint8Array, key: Uint8Array): Promise<void> => this._resourceStore.saveResourceKey(resourceId, key);
Expand Down
2 changes: 2 additions & 0 deletions packages/crypto/src/EncryptionFormats/DecryptionStreamV4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { KeyMapper } from './KeyMapper';
import type { ChunkHeader } from './v4';
import { EncryptionV4 } from './v4';
import * as utils from '../utils';
import { assertKey } from '../resourceId';

const checkHeaderIntegrity = (oldHeader: ChunkHeader, currentHeader: ChunkHeader) => {
if (!utils.equalArray(oldHeader.resourceId, currentHeader.resourceId)) {
Expand Down Expand Up @@ -71,6 +72,7 @@ export class DecryptionStreamV4 extends Transform {
throw new DecryptionFailed({ message: `invalid encrypted chunk size in header v4: ${encryptedChunkSize}` });

const key = await this._mapper(resourceId);
assertKey(resourceId, key);

this._state.maxEncryptedChunkSize = encryptedChunkSize;
this._resizerStream = new ResizerStream(encryptedChunkSize);
Expand Down
2 changes: 2 additions & 0 deletions packages/crypto/src/EncryptionFormats/DecryptionStreamV8.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { removePadding } from '../padding';
import type { ChunkHeader } from './v8';
import { EncryptionV8 } from './v8';
import * as utils from '../utils';
import { assertKey } from '../resourceId';

const checkHeaderIntegrity = (oldHeader: ChunkHeader, currentHeader: ChunkHeader) => {
if (!utils.equalArray(oldHeader.resourceId, currentHeader.resourceId)) {
Expand Down Expand Up @@ -74,6 +75,7 @@ export class DecryptionStreamV8 extends Transform {
throw new DecryptionFailed({ message: `invalid encrypted chunk size in header v8: ${encryptedChunkSize}` });

const key = await this._mapper(resourceId);
assertKey(resourceId, key);

this._state.maxEncryptedChunkSize = encryptedChunkSize;
this._resizerStream = new ResizerStream(encryptedChunkSize);
Expand Down
4 changes: 3 additions & 1 deletion packages/crypto/src/EncryptionFormats/KeyMapper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Key } from '../aliases';

export type KeyMapper = (keyID: Uint8Array) => Promise<Key> | Key;
type MaybePromise<T> = T | Promise<T>;

export type KeyMapper = (keyID: Uint8Array) => MaybePromise<Key | null>;
2 changes: 2 additions & 0 deletions packages/crypto/src/EncryptionFormats/v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { InvalidArgument, DecryptionFailed } from '@tanker/errors';

import * as aead from '../aead';
import { random } from '../random';
import { assertKey } from '../resourceId';
import * as tcrypto from '../tcrypto';
import * as utils from '../utils';
import { tryDecryptAEAD } from './helpers';
Expand Down Expand Up @@ -56,6 +57,7 @@ export class EncryptionV1 {

static async decrypt(keyMapper: KeyMapper, data: EncryptionData, associatedData?: Uint8Array): Promise<Uint8Array> {
const key = await keyMapper(data.resourceId);
assertKey(data.resourceId, key);
return tryDecryptAEAD(data.resourceId, key, data.iv, data.encryptedData, associatedData);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/crypto/src/EncryptionFormats/v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { InvalidArgument, DecryptionFailed } from '@tanker/errors';

import * as aead from '../aead';
import { random } from '../random';
import { assertKey } from '../resourceId';
import * as tcrypto from '../tcrypto';
import * as utils from '../utils';
import { tryDecryptAEAD } from './helpers';
Expand Down Expand Up @@ -55,6 +56,7 @@ export class EncryptionV2 {

static decrypt = async (keyMapper: KeyMapper, data: EncryptionData, additionalData?: Uint8Array): Promise<Uint8Array> => {
const key = await keyMapper(data.resourceId);
assertKey(data.resourceId, key);
return tryDecryptAEAD(data.resourceId, key, data.iv, data.encryptedData, additionalData);
};

Expand Down
2 changes: 2 additions & 0 deletions packages/crypto/src/EncryptionFormats/v3.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { InvalidArgument, DecryptionFailed } from '@tanker/errors';

import * as aead from '../aead';
import { assertKey } from '../resourceId';
import * as tcrypto from '../tcrypto';
import * as utils from '../utils';
import { tryDecryptAEAD } from './helpers';
Expand Down Expand Up @@ -55,6 +56,7 @@ export class EncryptionV3 {

static decrypt = async (keyMapper: KeyMapper, data: EncryptionData): Promise<Uint8Array> => {
const key = await keyMapper(data.resourceId);
assertKey(data.resourceId, key);
return tryDecryptAEAD(data.resourceId, key, data.iv, data.encryptedData);
};

Expand Down
2 changes: 2 additions & 0 deletions packages/crypto/src/EncryptionFormats/v5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { InvalidArgument, DecryptionFailed } from '@tanker/errors';

import * as aead from '../aead';
import { random } from '../random';
import { assertKey } from '../resourceId';
import * as tcrypto from '../tcrypto';
import * as utils from '../utils';
import { tryDecryptAEAD } from './helpers';
Expand Down Expand Up @@ -64,6 +65,7 @@ export class EncryptionV5 {

static decrypt = async (keyMapper: KeyMapper, data: EncryptionData): Promise<Uint8Array> => {
const key = await keyMapper(data.resourceId);
assertKey(data.resourceId, key);
return tryDecryptAEAD(data.resourceId, key, data.iv, data.encryptedData, data.resourceId);
};

Expand Down
2 changes: 2 additions & 0 deletions packages/crypto/src/EncryptionFormats/v6.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as tcrypto from '../tcrypto';
import * as utils from '../utils';
import type { KeyMapper } from './KeyMapper';
import { tryDecryptAEAD } from './helpers';
import { assertKey } from '../resourceId';

type EncryptionData = {
encryptedData: Uint8Array,
Expand Down Expand Up @@ -58,6 +59,7 @@ export class EncryptionV6 {

static decrypt = async (keyMapper: KeyMapper, data: EncryptionData): Promise<Uint8Array> => {
const key = await keyMapper(data.resourceId);
assertKey(data.resourceId, key);

const associatedData = new Uint8Array([this.version]);
return removePadding(tryDecryptAEAD(data.resourceId, key, data.iv, data.encryptedData, associatedData));
Expand Down
2 changes: 2 additions & 0 deletions packages/crypto/src/EncryptionFormats/v7.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as tcrypto from '../tcrypto';
import * as utils from '../utils';
import type { KeyMapper } from './KeyMapper';
import { tryDecryptAEAD } from './helpers';
import { assertKey } from '../resourceId';

type EncryptionData = {
encryptedData: Uint8Array;
Expand Down Expand Up @@ -67,6 +68,7 @@ export class EncryptionV7 {

static decrypt = async (keyMapper: KeyMapper, data: EncryptionData): Promise<Uint8Array> => {
const key = await keyMapper(data.resourceId);
assertKey(data.resourceId, key);

const associatedData = utils.concatArrays(new Uint8Array([this.version]), data.resourceId);
return removePadding(tryDecryptAEAD(data.resourceId, key, data.iv, data.encryptedData, associatedData));
Expand Down
2 changes: 1 addition & 1 deletion packages/crypto/src/__tests__/DecryptionStream.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ describe('DecryptionStream', () => {
(id) => {
if (utils.equalArray(id, streamHeader.resourceId))
return resourceKey;
throw new Error('key not found');
return null;
},
);
sync = watchStream(stream);
Expand Down
2 changes: 1 addition & 1 deletion packages/crypto/src/__tests__/EncryptionFormats.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,7 +711,7 @@ describe('Simple Encryption', () => {
(id) => {
if (utils.equalArray(id, data.resourceId))
return resourceKey;
throw new Error('key not found');
return null;
},
data,
);
Expand Down
38 changes: 38 additions & 0 deletions packages/crypto/src/__tests__/resourceId.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { InvalidArgument } from '@tanker/errors';
import { assert, expect, sinon } from '@tanker/test-utils';

import { random } from '../random';
import { ready } from '../ready';
import { getKeyFromCompositeResourceId } from '../resourceId';
import { MAC_SIZE, SESSION_ID_SIZE } from '../tcrypto';
import type { CompositeResourceId } from '../resourceId';

describe('getKeyFromCompositeResourceId', () => {
let resourceId: Uint8Array;
let sessionId: Uint8Array;
let compositeResourceId: CompositeResourceId;

before(async () => {
await ready;
resourceId = random(MAC_SIZE);
sessionId = random(SESSION_ID_SIZE);
compositeResourceId = {
sessionId,
resourceId,
};
});

it('aborts lookup when keyMapper throws', async () => {
const keyMapper = sinon.fake.throws(new Error());

await expect(getKeyFromCompositeResourceId(compositeResourceId, keyMapper)).to.be.rejected;
assert(keyMapper.calledOnce);
});

it('throws InvalidArgument when key cannot be found', async () => {
const keyMapper = sinon.fake.returns(null);

await expect(getKeyFromCompositeResourceId(compositeResourceId, keyMapper)).to.be.rejectedWith(InvalidArgument);
assert(keyMapper.calledTwice);
});
});
43 changes: 19 additions & 24 deletions packages/crypto/src/resourceId.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InvalidArgument, TankerError } from '@tanker/errors';
import { InvalidArgument } from '@tanker/errors';
import { assertString } from '@tanker/types';
import type { b64string, Key } from './aliases';
import type { KeyMapper } from './EncryptionFormats/KeyMapper';
Expand Down Expand Up @@ -61,39 +61,34 @@ export function assertResourceId(arg: unknown): asserts arg is string {

export const deriveSessionKey = (sessionKey: Key, seed: Uint8Array): Key => generichash(utils.concatArrays(sessionKey, seed));

const safeGetKey = async (resourceId: Uint8Array, getter: () => Key | Promise<Key>) => {
try {
return await getter();
} catch (e) {
if (e instanceof TankerError)
throw e;
export function assertKey(resourceId: Uint8Array, key: Key | null): asserts key is Key {
if (!key) {
throw new InvalidArgument(`could not find key for resource: ${utils.toBase64(resourceId)}`);
}
};
}

export const getKeyFromCompositeResourceId = async (resourceId: CompositeResourceId, keyMapper: KeyMapper) => safeGetKey(
serializeCompositeResourceId(resourceId),
async () => {
try {
const sessionKey = await keyMapper(resourceId.sessionId);
return deriveSessionKey(sessionKey, resourceId.resourceId);
} catch (e) {
return await keyMapper(resourceId.resourceId);
}
},
);
export const getKeyFromCompositeResourceId = async (resourceId: CompositeResourceId, keyMapper: KeyMapper) => {
let key: Key | null;
const sessionKey = await keyMapper(resourceId.sessionId);
if (sessionKey) {
key = deriveSessionKey(sessionKey, resourceId.resourceId);
} else {
key = await keyMapper(resourceId.resourceId);
}

assertKey(serializeCompositeResourceId(resourceId), key);
return key;
};

export const getKeyFromResourceId = async (b64resourceId: b64string, keyMapper: KeyMapper) => {
const resourceId = parseResourceId(b64resourceId);

let key: Uint8Array;
let key: Key | null;
if ('sessionId' in resourceId) {
key = await getKeyFromCompositeResourceId(resourceId, keyMapper);
} else {
key = await safeGetKey(
resourceId.resourceId,
() => keyMapper(getSimpleResourceId(resourceId)),
);
key = await keyMapper(getSimpleResourceId(resourceId));
assertKey(resourceId.resourceId, key);
}

return {
Expand Down

0 comments on commit 41ab0a3

Please sign in to comment.