diff --git a/packages/crypto/src/__tests__/resourceId.spec.ts b/packages/crypto/src/__tests__/resourceId.spec.ts index f92c9ad1..6ccad463 100644 --- a/packages/crypto/src/__tests__/resourceId.spec.ts +++ b/packages/crypto/src/__tests__/resourceId.spec.ts @@ -1,10 +1,10 @@ -import { InvalidArgument } from '@tanker/errors'; +import { InternalError, 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 { assertKey, getKeyFromCompositeResourceId } from '../resourceId'; +import { MAC_SIZE, SESSION_ID_SIZE, SYMMETRIC_KEY_SIZE } from '../tcrypto'; import type { CompositeResourceId } from '../resourceId'; describe('getKeyFromCompositeResourceId', () => { @@ -36,3 +36,30 @@ describe('getKeyFromCompositeResourceId', () => { assert(keyMapper.calledTwice); }); }); + +describe('assertKey', () => { + let resourceId: Uint8Array; + + before(async () => { + await ready; + resourceId = random(MAC_SIZE); + }); + + it('succeeds when the key is a Uint8Array', () => { + expect(() => assertKey(resourceId, new Uint8Array())).to.not.throw(); + expect(() => assertKey(resourceId, random(SYMMETRIC_KEY_SIZE))).to.not.throw(); + }); + + it('throws InvalidArgument when the key is null', () => { + expect(() => assertKey(resourceId, null)).to.throw(InvalidArgument); + }); + + it('throws InternalError when the key is falsy but not null', () => { + //@ts-expect-error + expect(() => assertKey(resourceId, undefined)).to.throw(InternalError); + //@ts-expect-error + expect(() => assertKey(resourceId, '')).to.throw(InternalError); + //@ts-expect-error + expect(() => assertKey(resourceId, 0)).to.throw(InternalError); + }); +}); diff --git a/packages/crypto/src/resourceId.ts b/packages/crypto/src/resourceId.ts index 68793976..9569fa34 100644 --- a/packages/crypto/src/resourceId.ts +++ b/packages/crypto/src/resourceId.ts @@ -1,4 +1,4 @@ -import { InvalidArgument } from '@tanker/errors'; +import { InternalError, InvalidArgument } from '@tanker/errors'; import { assertString } from '@tanker/types'; import type { b64string, Key } from './aliases'; import type { KeyMapper } from './EncryptionFormats/KeyMapper'; @@ -63,12 +63,17 @@ export const deriveSessionKey = (sessionKey: Key, seed: Uint8Array): Key => gene export function assertKey(resourceId: Uint8Array, key: Key | null): asserts key is Key { if (!key) { + if (key !== null) { + // This is a safeguard for unexpected behavior between local-storage, network and the resource coalescer: + // We suspect that a falsy value different from null is mixed in keys somehow + throw new InternalError(`Unreachable code during resource key look-up: ${utils.toBase64(resourceId)}, key found: ${key}`); + } throw new InvalidArgument(`could not find key for resource: ${utils.toBase64(resourceId)}`); } } export const getKeyFromCompositeResourceId = async (resourceId: CompositeResourceId, keyMapper: KeyMapper) => { - let key: Key | null; + let key: Key | null = null; const sessionKey = await keyMapper(resourceId.sessionId); if (sessionKey) { key = deriveSessionKey(sessionKey, resourceId.resourceId); @@ -83,7 +88,7 @@ export const getKeyFromCompositeResourceId = async (resourceId: CompositeResourc export const getKeyFromResourceId = async (b64resourceId: b64string, keyMapper: KeyMapper) => { const resourceId = parseResourceId(b64resourceId); - let key: Key | null; + let key: Key | null = null; if ('sessionId' in resourceId) { key = await getKeyFromCompositeResourceId(resourceId, keyMapper); } else { diff --git a/packages/datastore/dexie-base/src/index.ts b/packages/datastore/dexie-base/src/index.ts index 6bcd95af..6435c869 100644 --- a/packages/datastore/dexie-base/src/index.ts +++ b/packages/datastore/dexie-base/src/index.ts @@ -308,9 +308,9 @@ export const dexieStoreBase = ((DexieClass: Class): DataStoreAdapter => // - either withLimit is a Dexie Collection or Table to convert to a Promise> // - or sortBy() has been called and withLimit is already a Promise>, if (this._isTable(withLimit) || this._isCollection(withLimit)) { - res = (withLimit as ITable | ICollection).toArray(); + res = withLimit.toArray(); } else { - res = withLimit as typeof res; + res = withLimit; } return res.then(fromDB); @@ -323,12 +323,12 @@ export const dexieStoreBase = ((DexieClass: Class): DataStoreAdapter => delete = this.withReopen((table: string, id: string) => this._db.table(table).delete(id)); - _isTable(obj: any): boolean { + _isTable(obj: any): obj is ITable { // @ts-expect-error this._db.Table is a Class (has a prototype) return obj instanceof this._db.Table; } - _isCollection(obj: any): boolean { + _isCollection(obj: any): obj is ICollection { // @ts-expect-error this._db.Collection is a Class (has a prototype) return obj instanceof this._db.Collection; } @@ -457,7 +457,7 @@ export const dexieStoreBase = ((DexieClass: Class): DataStoreAdapter => this._isTable(q) && (index === sortKey || !index && this.isIndexed(table, sortKey)) ) { - res = (q as ITable).orderBy(sortKey); // ICollection (Dexie) + res = q.orderBy(sortKey); // ICollection (Dexie) } else { res = (q as ICollection).sortBy(sortKey); // Promise> } @@ -469,9 +469,9 @@ export const dexieStoreBase = ((DexieClass: Class): DataStoreAdapter => let res: ICollection | Promise>>; if (this._isTable(query) || this._isCollection(query)) { - res = (query as ITable | ICollection).limit(limit); + res = query.limit(limit); } else { - res = (query as Promise>>).then((array) => array.slice(0, limit)); + res = query.then((array) => array.slice(0, limit)); } return res;