diff --git a/README.md b/README.md index 09e9b286f..d6c5711bd 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ # Decentralized Web Node (DWN) SDK Code Coverage -![Statements](https://img.shields.io/badge/statements-94.74%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-94.13%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-92.48%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.74%25-brightgreen.svg?style=flat) +![Statements](https://img.shields.io/badge/statements-94.69%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-94.15%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-93.15%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.69%25-brightgreen.svg?style=flat) ## Introduction -This repository contains a reference implementation of Decentralized Web Node (DWN) as per the [specification](https://identity.foundation/decentralized-web-node/spec/). This specification is in a draft state and very much so a WIP. For the foreseeable future, a lot of the work on DWN will be split across this repo and the repo that houses the specification, which you can find [here](https://github.com/decentralized-identity/decentralized-web-node). The current goal is to produce a beta implementation by March 2023. This won't include all interfaces described in the DWN spec, but will be enough to begin building applications. +This repository contains a reference implementation of Decentralized Web Node (DWN) as per the [specification](https://identity.foundation/decentralized-web-node/spec/). This specification is in a draft state and very much so a WIP. For the foreseeable future, a lot of the work on DWN will be split across this repo and the repo that houses the specification, which you can find [here](https://github.com/decentralized-identity/decentralized-web-node). The current implementation does not include all interfaces described in the DWN spec, but is enough to begin building test applications. This project is used as a dependency by several other projects. diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index e5d599212..364e954e5 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -29,10 +29,10 @@ export enum DwnErrorCode { RecordsProtocolsDerivationSchemeMissingProtocol = 'RecordsProtocolsDerivationSchemeMissingProtocol', RecordsSchemasDerivationSchemeMissingSchema = 'RecordsSchemasDerivationSchemeMissingSchema', RecordsWriteGetEntryIdUndefinedAuthor = 'RecordsWriteGetEntryIdUndefinedAuthor', + RecordsWriteMissingDataStream = 'RecordsWriteMissingDataStream', RecordsWriteValidateIntegrityEncryptionCidMismatch = 'RecordsWriteValidateIntegrityEncryptionCidMismatch', Secp256k1KeyNotValid = 'Secp256k1KeyNotValid', StorageControllerDataCidMismatch = 'StorageControllerDataCidMismatch', - StorageControllerDataNotFound = 'StorageControllerDataNotFound', StorageControllerDataSizeMismatch = 'StorageControllerDataSizeMismatch', UrlProtocolNotNormalized = 'UrlProtocolNotNormalized', UrlProtocolNotNormalizable = 'UrlProtocolNotNormalizable', diff --git a/src/interfaces/records/handlers/pruned-initial-records-write.ts b/src/interfaces/records/handlers/pruned-initial-records-write.ts index 864b629d3..9055d5522 100644 --- a/src/interfaces/records/handlers/pruned-initial-records-write.ts +++ b/src/interfaces/records/handlers/pruned-initial-records-write.ts @@ -1,7 +1,7 @@ -import type { BaseMessage } from '../../../core/types.js'; import type { EventLog } from '../../../event-log/event-log.js'; import type { Readable } from 'readable-stream'; -import type { DataStore, MessageStore } from '../../../index.js'; +import type { BaseMessage, TimestampedMessage } from '../../../core/types.js'; +import type { DataStore, MessageStore, RecordsWriteMessage } from '../../../index.js'; import { Message } from '../../../core/message.js'; import { RecordsWriteHandler } from './records-write.js'; @@ -11,11 +11,19 @@ import { RecordsWriteHandler } from './records-write.js'; * NOTE: This is intended to be ONLY used by sync. */ export class PrunedInitialRecordsWriteHandler extends RecordsWriteHandler { + /** + * Overriding parent behavior, `undefined` data stream is allowed. + */ + protected validateUndefinedDataStream( + _dataStream: Readable | undefined, + _newestExistingMessage: TimestampedMessage | undefined, + _incomingMessage: RecordsWriteMessage): void { } + /** * Stores the given message without storing the associated data. * Requires `dataCid` to exist in the `RecordsWrite` message given. */ - public async storeMessage( + protected async storeMessage( messageStore: MessageStore, _dataStore: DataStore, eventLog: EventLog, diff --git a/src/interfaces/records/handlers/records-write.ts b/src/interfaces/records/handlers/records-write.ts index aa16e604c..0889dd9cc 100644 --- a/src/interfaces/records/handlers/records-write.ts +++ b/src/interfaces/records/handlers/records-write.ts @@ -7,11 +7,11 @@ import type { DataStore, DidResolver, MessageStore } from '../../../index.js'; import { authenticate } from '../../../core/auth.js'; import { deleteAllOlderMessagesButKeepInitialWrite } from '../records-interface.js'; -import { DwnErrorCode } from '../../../core/dwn-error.js'; -import { DwnInterfaceName } from '../../../core/message.js'; import { MessageReply } from '../../../core/message-reply.js'; import { RecordsWrite } from '../messages/records-write.js'; import { StorageController } from '../../../store/storage-controller.js'; +import { DwnError, DwnErrorCode } from '../../../core/dwn-error.js'; +import { DwnInterfaceName, Message } from '../../../core/message.js'; export class RecordsWriteHandler implements MethodHandler { @@ -56,54 +56,72 @@ export class RecordsWriteHandler implements MethodHandler { } } - // find which message is the newest, and if the incoming message is the newest - const newestExistingMessage = await RecordsWrite.getNewestMessage(existingMessages); + const newestExistingMessage = await Message.getNewestMessage(existingMessages); let incomingMessageIsNewest = false; - let newestMessage; - // if incoming message is newest - if (newestExistingMessage === undefined || await RecordsWrite.isNewer(message, newestExistingMessage)) { + let newestMessage; // keep reference of newest message for pruning later + if (newestExistingMessage === undefined || await Message.isNewer(message, newestExistingMessage)) { incomingMessageIsNewest = true; newestMessage = message; } else { // existing message is the same age or newer than the incoming message newestMessage = newestExistingMessage; } - // write the incoming message to DB if incoming message is newest - let messageReply: MessageReply; - if (incomingMessageIsNewest) { - const isLatestBaseState = true; - const indexes = await constructRecordsWriteIndexes(recordsWrite, isLatestBaseState); + if (!incomingMessageIsNewest) { + return new MessageReply({ + status: { code: 409, detail: 'Conflict' } + }); + } - try { - await this.storeMessage(this.messageStore, this.dataStore, this.eventLog, tenant, message, indexes, dataStream); - } catch (error) { - const e = error as any; - if (e.code === DwnErrorCode.StorageControllerDataCidMismatch || - e.code === DwnErrorCode.StorageControllerDataNotFound || - e.code === DwnErrorCode.StorageControllerDataSizeMismatch) { - return MessageReply.fromError(error, 400); - } - - // else throw - throw error; + const isLatestBaseState = true; + const indexes = await constructRecordsWriteIndexes(recordsWrite, isLatestBaseState); + + try { + this.validateUndefinedDataStream(dataStream, newestExistingMessage, message); + + await this.storeMessage(this.messageStore, this.dataStore, this.eventLog, tenant, message, indexes, dataStream); + } catch (error) { + const e = error as any; + if (e.code === DwnErrorCode.StorageControllerDataCidMismatch || + e.code === DwnErrorCode.StorageControllerDataSizeMismatch || + e.code === DwnErrorCode.RecordsWriteMissingDataStream) { + return MessageReply.fromError(error, 400); } - messageReply = new MessageReply({ - status: { code: 202, detail: 'Accepted' } - }); - } else { - messageReply = new MessageReply({ - status: { code: 409, detail: 'Conflict' } - }); + // else throw + throw error; } + const messageReply = new MessageReply({ + status: { code: 202, detail: 'Accepted' } + }); + // delete all existing messages that are not newest, except for the initial write await deleteAllOlderMessagesButKeepInitialWrite(tenant, existingMessages, newestMessage, this.messageStore, this.dataStore, this.eventLog); return messageReply; }; + /** + * Further validation if data stream is undefined. + * NOTE: if data stream is not be provided but `dataCid` is provided, + * then we need to make sure that the existing record state is referencing the same data as the incoming message. + * Without this check will lead to unauthorized access of data (https://github.com/TBD54566975/dwn-sdk-js/issues/359) + */ + protected validateUndefinedDataStream( + dataStream: _Readable.Readable | undefined, + newestExistingMessage: TimestampedMessage | undefined, + incomingMessage: RecordsWriteMessage): void { + if (dataStream === undefined && incomingMessage.descriptor.dataCid !== undefined) { + if (newestExistingMessage?.descriptor.dataCid !== incomingMessage.descriptor.dataCid) { + throw new DwnError( + DwnErrorCode.RecordsWriteMissingDataStream, + 'Data stream is not provided.' + ); + } + } + } + /** * Stores the given message and its data in the underlying database(s). * NOTE: this method was created to allow a child class to override the default behavior for sync feature to work: @@ -111,7 +129,7 @@ export class RecordsWriteHandler implements MethodHandler { * a `RecordsDelete` has happened, as a result a DWN would have pruned the data associated with the original write. * This approach avoids the need to duplicate the entire handler. */ - public async storeMessage( + protected async storeMessage( messageStore: MessageStore, dataStore: DataStore, eventLog: EventLog, diff --git a/src/store/storage-controller.ts b/src/store/storage-controller.ts index 980fe21a5..98f8a8316 100644 --- a/src/store/storage-controller.ts +++ b/src/store/storage-controller.ts @@ -18,8 +18,6 @@ export class StorageController { * Puts the given message and data in storage. * @throws {DwnError} with `DwnErrorCode.StorageControllerDataCidMismatch` * if the data stream resulted in a data CID that mismatches with `dataCid` in the given message - * @throws {DwnError} with `DwnErrorCode.StorageControllerDataNotFound` - * if `dataCid` in `descriptor` is given, and `dataStream` is not given, and data for the message does not exist already * @throws {DwnError} with `DwnErrorCode.StorageControllerDataSizeMismatch` * if `dataSize` in `descriptor` given mismatches the actual data size */ @@ -35,26 +33,18 @@ export class StorageController { const messageCid = await Message.getCid(message); // if `dataCid` is given, it means there is corresponding data associated with this message - // but NOTE: it is possible that a data stream is not given in such case, for instance, - // a subsequent RecordsWrite that changes the `published` property, but the data hasn't changed, - // in this case requiring re-uploading of the data is extremely inefficient so we take care allow omission of data stream if (message.descriptor.dataCid !== undefined) { let result; - if (dataStream === undefined) { + // but NOTE: it is possible that a data stream is not given even when `dataCid` is given, for instance, + // a subsequent RecordsWrite that changes the `published` property, but the data hasn't changed, + // in this case requiring re-uploading of the data is extremely inefficient so we allow omission of data stream as a utility function, + // but this method assumes checks for the appropriate conditions already took place prior to calling this method. result = await dataStore.associate(tenant, messageCid, message.descriptor.dataCid); } else { result = await dataStore.put(tenant, messageCid, message.descriptor.dataCid, dataStream); } - // the message implies that the data is already in the DB, so we check to make sure the data already exist - if (!result) { - throw new DwnError( - DwnErrorCode.StorageControllerDataNotFound, - `data with dataCid ${message.descriptor.dataCid} not found in store` - ); - } - // MUST verify that the size of the actual data matches with the given `dataSize` // if data size is wrong, delete the data we just stored if (message.descriptor.dataSize !== result.dataSize) { diff --git a/src/utils/array.ts b/src/utils/array.ts index e4c207897..2a2f0826b 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -1,11 +1,23 @@ - /** - * Asynchronously iterates an {AsyncGenerator} to return all the values in an array. + * Array utility methods. */ -export async function asyncGeneratorToArray<T>(iterator: AsyncGenerator<T>): Promise<Array<T>> { - const array: Array<T> = [ ]; - for await (const value of iterator) { - array.push(value); +export class ArrayUtility { + /** + * Returns `true` if content of the two given byte arrays are equal; `false` otherwise. + */ + public static byteArraysEqual(array1: Uint8Array, array2:Uint8Array): boolean { + const equal = array1.length === array2.length && array1.every((value, index) => value === array2[index]); + return equal; + } + + /** + * Asynchronously iterates an {AsyncGenerator} to return all the values in an array. + */ + public static async fromAsyncGenerator<T>(iterator: AsyncGenerator<T>): Promise<Array<T>> { + const array: Array<T> = [ ]; + for await (const value of iterator) { + array.push(value); + } + return array; } - return array; -} +} \ No newline at end of file diff --git a/tests/interfaces/records/handlers/records-delete.spec.ts b/tests/interfaces/records/handlers/records-delete.spec.ts index 9f158ee91..6eaa16297 100644 --- a/tests/interfaces/records/handlers/records-delete.spec.ts +++ b/tests/interfaces/records/handlers/records-delete.spec.ts @@ -2,11 +2,10 @@ import chaiAsPromised from 'chai-as-promised'; import sinon from 'sinon'; import chai, { expect } from 'chai'; -import { asyncGeneratorToArray } from '../../../../src/utils/array.js'; +import { ArrayUtility } from '../../../../src/utils/array.js'; import { Cid } from '../../../../src/utils/cid.js'; import { DataStoreLevel } from '../../../../src/store/data-store-level.js'; import { DidKeyResolver } from '../../../../src/did/did-key-resolver.js'; -import { DwnErrorCode } from '../../../../src/core/dwn-error.js'; import { EventLogLevel } from '../../../../src/event-log/event-log-level.js'; import { Message } from '../../../../src/core/message.js'; import { MessageStoreLevel } from '../../../../src/store/message-store-level.js'; @@ -131,249 +130,6 @@ describe('RecordsDeleteHandler.handle()', () => { expect(reply.entries![0].encodedData).to.equal(expectedEncodedData); }); - it('should only write the data once even if written by multiple messages', async () => { - const alice = await DidKeyResolver.generate(); - const data = Encoder.stringToBytes('test'); - const dataCid = await Cid.computeDagPbCidFromBytes(data); - const encodedData = Encoder.bytesToBase64Url(data); - - const blockstoreForData = await dataStore.blockstore.partition('data'); - const blockstoreOfGivenTenant = await blockstoreForData.partition(alice.did); - const blockstoreOfGivenDataCid = await blockstoreOfGivenTenant.partition(dataCid); - - const aliceWrite1Data = await TestDataGenerator.generateRecordsWrite({ - requester: alice, - data - }); - const aliceWrite1Reply = await dwn.processMessage(alice.did, aliceWrite1Data.message, aliceWrite1Data.dataStream); - expect(aliceWrite1Reply.status.code).to.equal(202); - - const aliceQueryWrite1AfterAliceWrite1Data = await TestDataGenerator.generateRecordsQuery({ - requester : alice, - filter : { recordId: aliceWrite1Data.message.recordId } - }); - const aliceQueryWrite1AfterAliceWrite1Reply = await dwn.processMessage(alice.did, aliceQueryWrite1AfterAliceWrite1Data.message); - expect(aliceQueryWrite1AfterAliceWrite1Reply.status.code).to.equal(200); - expect(aliceQueryWrite1AfterAliceWrite1Reply.entries?.length).to.equal(1); - expect(aliceQueryWrite1AfterAliceWrite1Reply.entries![0].encodedData).to.equal(encodedData); - - await expect(asyncGeneratorToArray(blockstoreOfGivenDataCid.db.keys())).to.eventually.eql([ dataCid ]); - - const aliceWrite2Data = await TestDataGenerator.generateRecordsWrite({ - requester: alice, - data - }); - const aliceWrite2Reply = await dwn.processMessage(alice.did, aliceWrite2Data.message, aliceWrite2Data.dataStream); - expect(aliceWrite2Reply.status.code).to.equal(202); - - const aliceQueryWrite1AfterAliceWrite2Data = await TestDataGenerator.generateRecordsQuery({ - requester : alice, - filter : { recordId: aliceWrite1Data.message.recordId } - }); - const aliceQueryWrite1AfterAliceWrite2Reply = await dwn.processMessage(alice.did, aliceQueryWrite1AfterAliceWrite2Data.message); - expect(aliceQueryWrite1AfterAliceWrite2Reply.status.code).to.equal(200); - expect(aliceQueryWrite1AfterAliceWrite2Reply.entries?.length).to.equal(1); - expect(aliceQueryWrite1AfterAliceWrite2Reply.entries![0].encodedData).to.equal(encodedData); - - const aliceQueryWrite2AfterAliceWrite2Data = await TestDataGenerator.generateRecordsQuery({ - requester : alice, - filter : { recordId: aliceWrite2Data.message.recordId } - }); - const aliceQueryWrite2AfterAliceWrite2Reply = await dwn.processMessage(alice.did, aliceQueryWrite2AfterAliceWrite2Data.message); - expect(aliceQueryWrite2AfterAliceWrite2Reply.status.code).to.equal(200); - expect(aliceQueryWrite2AfterAliceWrite2Reply.entries?.length).to.equal(1); - expect(aliceQueryWrite2AfterAliceWrite2Reply.entries![0].encodedData).to.equal(encodedData); - - await expect(asyncGeneratorToArray(blockstoreOfGivenDataCid.db.keys())).to.eventually.eql([ dataCid ]); - }); - - it('should allow writing message by referencing existing data without supplying data again', async () => { - const alice = await DidKeyResolver.generate(); - const data = Encoder.stringToBytes('test'); - const dataCid = await Cid.computeDagPbCidFromBytes(data); - const encodedData = Encoder.bytesToBase64Url(data); - - const blockstoreForData = await dataStore.blockstore.partition('data'); - const blockstoreOfGivenTenant = await blockstoreForData.partition(alice.did); - const blockstoreOfGivenDataCid = await blockstoreOfGivenTenant.partition(dataCid); - - const aliceWriteData = await TestDataGenerator.generateRecordsWrite({ - requester: alice, - data - }); - const aliceWriteReply = await dwn.processMessage(alice.did, aliceWriteData.message, aliceWriteData.dataStream); - expect(aliceWriteReply.status.code).to.equal(202); - - const aliceQueryWriteAfterAliceWriteData = await TestDataGenerator.generateRecordsQuery({ - requester : alice, - filter : { recordId: aliceWriteData.message.recordId } - }); - const aliceQueryWriteAfterAliceWriteReply = await dwn.processMessage(alice.did, aliceQueryWriteAfterAliceWriteData.message); - expect(aliceQueryWriteAfterAliceWriteReply.status.code).to.equal(200); - expect(aliceQueryWriteAfterAliceWriteReply.entries?.length).to.equal(1); - expect(aliceQueryWriteAfterAliceWriteReply.entries![0].encodedData).to.equal(encodedData); - - await expect(asyncGeneratorToArray(blockstoreOfGivenDataCid.db.keys())).to.eventually.eql([ dataCid ]); - - // referencing existing data without supplying the data again - const aliceAssociateData = await TestDataGenerator.generateRecordsWrite({ - requester : alice, - dataCid, - dataSize : 4 - }); - const aliceAssociateReply = await dwn.processMessage(alice.did, aliceAssociateData.message, aliceAssociateData.dataStream); - expect(aliceAssociateReply.status.code).to.equal(202); - - const aliceQueryWriteAfterAliceAssociateData = await TestDataGenerator.generateRecordsQuery({ - requester : alice, - filter : { recordId: aliceWriteData.message.recordId } - }); - const aliceQueryWriteAfterAliceAssociateReply = await dwn.processMessage(alice.did, aliceQueryWriteAfterAliceAssociateData.message); - expect(aliceQueryWriteAfterAliceAssociateReply.status.code).to.equal(200); - expect(aliceQueryWriteAfterAliceAssociateReply.entries?.length).to.equal(1); - expect(aliceQueryWriteAfterAliceAssociateReply.entries![0].encodedData).to.equal(encodedData); - - const aliceQueryAssociateAfterAliceAssociateData = await TestDataGenerator.generateRecordsQuery({ - requester : alice, - filter : { recordId: aliceAssociateData.message.recordId } - }); - const aliceQueryAssociateAfterAliceAssociateReply = await dwn.processMessage(alice.did, aliceQueryAssociateAfterAliceAssociateData.message); - expect(aliceQueryAssociateAfterAliceAssociateReply.status.code).to.equal(200); - expect(aliceQueryAssociateAfterAliceAssociateReply.entries?.length).to.equal(1); - expect(aliceQueryAssociateAfterAliceAssociateReply.entries![0].encodedData).to.equal(encodedData); - - await expect(asyncGeneratorToArray(blockstoreOfGivenDataCid.db.keys())).to.eventually.eql([ dataCid ]); - }); - - it('should duplicate same data if written to different tenants', async () => { - const alice = await DidKeyResolver.generate(); - const bob = await DidKeyResolver.generate(); - - const data = Encoder.stringToBytes('test'); - const dataCid = await Cid.computeDagPbCidFromBytes(data); - const encodedData = Encoder.bytesToBase64Url(data); - - const blockstoreForData = await dataStore.blockstore.partition('data'); - const blockstoreOfAlice = await blockstoreForData.partition(alice.did); - const blockstoreOfAliceOfDataCid = await blockstoreOfAlice.partition(dataCid); - - const blockstoreOfBob = await blockstoreForData.partition(bob.did); - const blockstoreOfBobOfDataCid = await blockstoreOfBob.partition(dataCid); - - // write data to alice's DWN - const aliceWriteData = await TestDataGenerator.generateRecordsWrite({ - requester: alice, - data - }); - const aliceWriteReply = await dwn.processMessage(alice.did, aliceWriteData.message, aliceWriteData.dataStream); - expect(aliceWriteReply.status.code).to.equal(202); - - const aliceQueryWriteAfterAliceWriteData = await TestDataGenerator.generateRecordsQuery({ - requester : alice, - filter : { recordId: aliceWriteData.message.recordId } - }); - const aliceQueryWriteAfterAliceWriteReply = await dwn.processMessage(alice.did, aliceQueryWriteAfterAliceWriteData.message); - expect(aliceQueryWriteAfterAliceWriteReply.status.code).to.equal(200); - expect(aliceQueryWriteAfterAliceWriteReply.entries?.length).to.equal(1); - expect(aliceQueryWriteAfterAliceWriteReply.entries![0].encodedData).to.equal(encodedData); - - // write same data to bob's DWN - const bobWriteData = await TestDataGenerator.generateRecordsWrite({ - requester: bob, - data - }); - const bobWriteReply = await dwn.processMessage(bob.did, bobWriteData.message, bobWriteData.dataStream); - expect(bobWriteReply.status.code).to.equal(202); - - const aliceQueryWriteAfterBobWriteData = await TestDataGenerator.generateRecordsQuery({ - requester : alice, - filter : { recordId: aliceWriteData.message.recordId } - }); - const aliceQueryWriteAfterBobWriteReply = await dwn.processMessage(alice.did, aliceQueryWriteAfterBobWriteData.message); - expect(aliceQueryWriteAfterBobWriteReply.status.code).to.equal(200); - expect(aliceQueryWriteAfterBobWriteReply.entries?.length).to.equal(1); - expect(aliceQueryWriteAfterBobWriteReply.entries![0].encodedData).to.equal(encodedData); - - const bobQueryWriteAfterBobWriteData = await TestDataGenerator.generateRecordsQuery({ - requester : bob, - filter : { recordId: bobWriteData.message.recordId } - }); - const bobQueryWriteAfterBobWriteReply = await dwn.processMessage(bob.did, bobQueryWriteAfterBobWriteData.message); - expect(bobQueryWriteAfterBobWriteReply.status.code).to.equal(200); - expect(bobQueryWriteAfterBobWriteReply.entries?.length).to.equal(1); - expect(bobQueryWriteAfterBobWriteReply.entries![0].encodedData).to.equal(encodedData); - - // verify that both alice and bob's blockstore have reference to the same data CID - await expect(asyncGeneratorToArray(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); - await expect(asyncGeneratorToArray(blockstoreOfBobOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); - }); - - it('should not allow referencing data across tenants', async () => { - const alice = await DidKeyResolver.generate(); - const bob = await DidKeyResolver.generate(); - const data = Encoder.stringToBytes('test'); - const dataCid = await Cid.computeDagPbCidFromBytes(data); - const encodedData = Encoder.bytesToBase64Url(data); - - const blockstoreForData = await dataStore.blockstore.partition('data'); - const blockstoreOfAlice = await blockstoreForData.partition(alice.did); - const blockstoreOfAliceOfDataCid = await blockstoreOfAlice.partition(dataCid); - - const blockstoreOfBob = await blockstoreForData.partition(bob.did); - const blockstoreOfBobOfDataCid = await blockstoreOfBob.partition(dataCid); - - // alice writes data to her DWN - const aliceWriteData = await TestDataGenerator.generateRecordsWrite({ - requester: alice, - data - }); - const aliceWriteReply = await dwn.processMessage(alice.did, aliceWriteData.message, aliceWriteData.dataStream); - expect(aliceWriteReply.status.code).to.equal(202); - - const aliceQueryWriteAfterAliceWriteData = await TestDataGenerator.generateRecordsQuery({ - requester : alice, - filter : { recordId: aliceWriteData.message.recordId } - }); - const aliceQueryWriteAfterAliceWriteReply = await dwn.processMessage(alice.did, aliceQueryWriteAfterAliceWriteData.message); - expect(aliceQueryWriteAfterAliceWriteReply.status.code).to.equal(200); - expect(aliceQueryWriteAfterAliceWriteReply.entries?.length).to.equal(1); - expect(aliceQueryWriteAfterAliceWriteReply.entries![0].encodedData).to.equal(encodedData); - - await expect(asyncGeneratorToArray(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); - - // bob learns of the CID of data of alice and tries to gain unauthorized access by referencing it in his own DWN - const bobAssociateData = await TestDataGenerator.generateRecordsWrite({ - requester : bob, - dataCid, - dataSize : 4 - }); - const bobAssociateReply = await dwn.processMessage(bob.did, bobAssociateData.message, bobAssociateData.dataStream); - expect(bobAssociateReply.status.code).to.equal(400); // expecting an error - expect(bobAssociateReply.status.detail).to.contain(DwnErrorCode.StorageControllerDataNotFound); - - const aliceQueryWriteAfterBobAssociateData = await TestDataGenerator.generateRecordsQuery({ - requester : alice, - filter : { recordId: aliceWriteData.message.recordId } - }); - const aliceQueryWriteAfterBobAssociateReply = await dwn.processMessage(alice.did, aliceQueryWriteAfterBobAssociateData.message); - expect(aliceQueryWriteAfterBobAssociateReply.status.code).to.equal(200); - expect(aliceQueryWriteAfterBobAssociateReply.entries?.length).to.equal(1); - expect(aliceQueryWriteAfterBobAssociateReply.entries![0].encodedData).to.equal(encodedData); - - // verify that bob has not gained access to alice's data - const bobQueryAssociateAfterBobAssociateData = await TestDataGenerator.generateRecordsQuery({ - requester : bob, - filter : { recordId: bobAssociateData.message.recordId } - }); - const bobQueryAssociateAfterBobAssociateReply = await dwn.processMessage(bob.did, bobQueryAssociateAfterBobAssociateData.message); - expect(bobQueryAssociateAfterBobAssociateReply.status.code).to.equal(200); - expect(bobQueryAssociateAfterBobAssociateReply.entries?.length).to.equal(0); - - // verify that bob's blockstore does not contain alice's data - await expect(asyncGeneratorToArray(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); - await expect(asyncGeneratorToArray(blockstoreOfBobOfDataCid.db.keys())).to.eventually.eql([ ]); - }); - it('should be able to delete and rewrite the same data', async () => { const alice = await DidKeyResolver.generate(); const data = Encoder.stringToBytes('test'); @@ -401,7 +157,7 @@ describe('RecordsDeleteHandler.handle()', () => { expect(aliceQueryWriteAfterAliceWriteReply.entries?.length).to.equal(1); expect(aliceQueryWriteAfterAliceWriteReply.entries![0].encodedData).to.equal(encodedData); - await expect(asyncGeneratorToArray(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); // alice deleting the record const aliceDeleteWriteData = await TestDataGenerator.generateRecordsDelete({ @@ -419,7 +175,7 @@ describe('RecordsDeleteHandler.handle()', () => { expect(aliceQueryWriteAfterAliceDeleteReply.status.code).to.equal(200); expect(aliceQueryWriteAfterAliceDeleteReply.entries?.length).to.equal(0); - await expect(asyncGeneratorToArray(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ ]); // alice writes a new record with the same data const aliceRewriteData = await TestDataGenerator.generateRecordsWrite({ @@ -438,7 +194,7 @@ describe('RecordsDeleteHandler.handle()', () => { expect(aliceQueryWriteAfterAliceRewriteReply.entries?.length).to.equal(1); expect(aliceQueryWriteAfterAliceRewriteReply.entries![0].encodedData).to.equal(encodedData); - await expect(asyncGeneratorToArray(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); }); it('should only delete data after all messages referencing it are deleted', async () => { @@ -455,46 +211,32 @@ describe('RecordsDeleteHandler.handle()', () => { const blockstoreOfBobOfDataCid = await blockstoreOfBob.partition(dataCid); // alice writes a records with data - const aliceWriteData = await TestDataGenerator.generateRecordsWrite({ - requester: alice, - data - }); + const aliceWriteData = await TestDataGenerator.generateRecordsWrite({ requester: alice, data }); const aliceWriteReply = await dwn.processMessage(alice.did, aliceWriteData.message, aliceWriteData.dataStream); expect(aliceWriteReply.status.code).to.equal(202); - await expect(asyncGeneratorToArray(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); - // alice writes another record referencing the same data - const aliceAssociateData = await TestDataGenerator.generateRecordsWrite({ - requester : alice, - dataCid, - dataSize : 4 - }); + // alice writes another record with the same data + const aliceAssociateData = await TestDataGenerator.generateRecordsWrite({ requester: alice, data }); const aliceAssociateReply = await dwn.processMessage(alice.did, aliceAssociateData.message, aliceAssociateData.dataStream); expect(aliceAssociateReply.status.code).to.equal(202); - await expect(asyncGeneratorToArray(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); // bob writes a records with same data - const bobWriteData = await TestDataGenerator.generateRecordsWrite({ - requester: bob, - data - }); + const bobWriteData = await TestDataGenerator.generateRecordsWrite({ requester: bob, data }); const bobWriteReply = await dwn.processMessage(bob.did, bobWriteData.message, bobWriteData.dataStream); expect(bobWriteReply.status.code).to.equal(202); - await expect(asyncGeneratorToArray(blockstoreOfBobOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfBobOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); - // bob writes another record referencing the same data - const bobAssociateData = await TestDataGenerator.generateRecordsWrite({ - requester : bob, - dataCid, - dataSize : 4 - }); + // bob writes another record with the same data + const bobAssociateData = await TestDataGenerator.generateRecordsWrite({ requester: bob, data }); const bobAssociateReply = await dwn.processMessage(bob.did, bobAssociateData.message, bobAssociateData.dataStream); expect(bobAssociateReply.status.code).to.equal(202); - await expect(asyncGeneratorToArray(blockstoreOfBobOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfBobOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); // alice deletes one of the two records const aliceDeleteWriteData = await TestDataGenerator.generateRecordsDelete({ @@ -504,7 +246,7 @@ describe('RecordsDeleteHandler.handle()', () => { const aliceDeleteWriteReply = await dwn.processMessage(alice.did, aliceDeleteWriteData.message); expect(aliceDeleteWriteReply.status.code).to.equal(202); - await expect(asyncGeneratorToArray(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); // alice deletes the other record const aliceDeleteAssociateData = await TestDataGenerator.generateRecordsDelete({ @@ -515,8 +257,8 @@ describe('RecordsDeleteHandler.handle()', () => { expect(aliceDeleteAssociateReply.status.code).to.equal(202); // verify that data is deleted in alice's blockstore, but remains in bob's blockstore - await expect(asyncGeneratorToArray(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ ]); - await expect(asyncGeneratorToArray(blockstoreOfBobOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfBobOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); }); describe('event log', () => { diff --git a/tests/interfaces/records/handlers/records-query.spec.ts b/tests/interfaces/records/handlers/records-query.spec.ts index 438915c92..3e0a6c8c7 100644 --- a/tests/interfaces/records/handlers/records-query.spec.ts +++ b/tests/interfaces/records/handlers/records-query.spec.ts @@ -6,7 +6,7 @@ import emailProtocolDefinition from '../../../vectors/protocol-definitions/email import sinon from 'sinon'; import chai, { expect } from 'chai'; -import { Comparer } from '../../../utils/comparer.js'; +import { ArrayUtility } from '../../../../src/utils/array.js'; import { DataStoreLevel } from '../../../../src/store/data-store-level.js'; import { DidKeyResolver } from '../../../../src/did/did-key-resolver.js'; import { DwnConstant } from '../../../../src/core/dwn-constant.js'; @@ -718,7 +718,7 @@ describe('RecordsQueryHandler.handle()', () => { const plaintextDataStream = await Records.decrypt(unsignedRecordsWrite, rootPrivateKey, cipherStream); const plaintextBytes = await DataStream.toBytes(plaintextDataStream); - expect(Comparer.byteArraysEqual(plaintextBytes, bobMessageBytes)).to.be.true; + expect(ArrayUtility.byteArraysEqual(plaintextBytes, bobMessageBytes)).to.be.true; // test able to decrypt the message using a derived key @@ -729,7 +729,7 @@ describe('RecordsQueryHandler.handle()', () => { const plaintextDataStream2 = await Records.decrypt(unsignedRecordsWrite, derivedPrivateKey, cipherStream2); const plaintextBytes2 = await DataStream.toBytes(plaintextDataStream2); - expect(Comparer.byteArraysEqual(plaintextBytes2, bobMessageBytes)).to.be.true; + expect(ArrayUtility.byteArraysEqual(plaintextBytes2, bobMessageBytes)).to.be.true; // test able to decrypt the message using a key derived from a derived key @@ -740,7 +740,7 @@ describe('RecordsQueryHandler.handle()', () => { const plaintextDataStream3 = await Records.decrypt(unsignedRecordsWrite, derivedPrivateKey2, cipherStream3); const plaintextBytes3 = await DataStream.toBytes(plaintextDataStream3); - expect(Comparer.byteArraysEqual(plaintextBytes3, bobMessageBytes)).to.be.true; + expect(ArrayUtility.byteArraysEqual(plaintextBytes3, bobMessageBytes)).to.be.true; // test unable to decrypt the message if derived key has an unexpected path @@ -802,7 +802,7 @@ describe('RecordsQueryHandler.handle()', () => { const plaintextDataStream = await Records.decrypt(unsignedRecordsWrite, rootPrivateKey, cipherStream); const plaintextBytes = await DataStream.toBytes(plaintextDataStream); - expect(Comparer.byteArraysEqual(plaintextBytes, originalData)).to.be.true; + expect(ArrayUtility.byteArraysEqual(plaintextBytes, originalData)).to.be.true; // test able to decrypt the message using a derived key @@ -813,7 +813,7 @@ describe('RecordsQueryHandler.handle()', () => { const plaintextDataStream2 = await Records.decrypt(unsignedRecordsWrite, derivedPrivateKey, cipherStream2); const plaintextBytes2 = await DataStream.toBytes(plaintextDataStream2); - expect(Comparer.byteArraysEqual(plaintextBytes2, originalData)).to.be.true; + expect(ArrayUtility.byteArraysEqual(plaintextBytes2, originalData)).to.be.true; // test able to decrypt the message using a key derived from a derived key @@ -824,7 +824,7 @@ describe('RecordsQueryHandler.handle()', () => { const plaintextDataStream3 = await Records.decrypt(unsignedRecordsWrite, derivedPrivateKey2, cipherStream3); const plaintextBytes3 = await DataStream.toBytes(plaintextDataStream3); - expect(Comparer.byteArraysEqual(plaintextBytes3, originalData)).to.be.true; + expect(ArrayUtility.byteArraysEqual(plaintextBytes3, originalData)).to.be.true; // test unable to decrypt the message if derived key has an unexpected path diff --git a/tests/interfaces/records/handlers/records-read.spec.ts b/tests/interfaces/records/handlers/records-read.spec.ts index 1fd8f589a..3ff25289d 100644 --- a/tests/interfaces/records/handlers/records-read.spec.ts +++ b/tests/interfaces/records/handlers/records-read.spec.ts @@ -7,7 +7,7 @@ import sinon from 'sinon'; import socialMediaProtocolDefinition from '../../../vectors/protocol-definitions/social-media.json' assert { type: 'json' }; import chai, { expect } from 'chai'; -import { Comparer } from '../../../utils/comparer.js'; +import { ArrayUtility } from '../../../../src/utils/array.js'; import { DataStoreLevel } from '../../../../src/store/data-store-level.js'; import { DidKeyResolver } from '../../../../src/did/did-key-resolver.js'; import { DwnErrorCode } from '../../../../src/core/dwn-error.js'; @@ -86,7 +86,7 @@ describe('RecordsReadHandler.handle()', () => { expect(readReply.record?.descriptor).to.exist; const dataFetched = await DataStream.toBytes(readReply.record!.data!); - expect(Comparer.byteArraysEqual(dataFetched, dataBytes!)).to.be.true; + expect(ArrayUtility.byteArraysEqual(dataFetched, dataBytes!)).to.be.true; }); it('should not allow non-tenant to RecordsRead their a record data', async () => { @@ -127,7 +127,7 @@ describe('RecordsReadHandler.handle()', () => { expect(readReply.status.code).to.equal(200); const dataFetched = await DataStream.toBytes(readReply.record!.data!); - expect(Comparer.byteArraysEqual(dataFetched, dataBytes!)).to.be.true; + expect(ArrayUtility.byteArraysEqual(dataFetched, dataBytes!)).to.be.true; }); it('should allow an authenticated user to RecordRead data that is published', async () => { @@ -150,7 +150,7 @@ describe('RecordsReadHandler.handle()', () => { expect(readReply.status.code).to.equal(200); const dataFetched = await DataStream.toBytes(readReply.record!.data!); - expect(Comparer.byteArraysEqual(dataFetched, dataBytes!)).to.be.true; + expect(ArrayUtility.byteArraysEqual(dataFetched, dataBytes!)).to.be.true; }); describe('protocol based reads', () => { @@ -420,7 +420,7 @@ describe('RecordsReadHandler.handle()', () => { const plaintextDataStream = await Records.decrypt(unsignedRecordsWrite, rootPrivateKey, cipherStream); const plaintextBytes = await DataStream.toBytes(plaintextDataStream); - expect(Comparer.byteArraysEqual(plaintextBytes, originalData)).to.be.true; + expect(ArrayUtility.byteArraysEqual(plaintextBytes, originalData)).to.be.true; // test able to decrypt the message using a derived key @@ -433,7 +433,7 @@ describe('RecordsReadHandler.handle()', () => { const plaintextDataStream2 = await Records.decrypt(unsignedRecordsWrite, derivedPrivateKey, cipherStream2); const plaintextBytes2 = await DataStream.toBytes(plaintextDataStream2); - expect(Comparer.byteArraysEqual(plaintextBytes2, originalData)).to.be.true; + expect(ArrayUtility.byteArraysEqual(plaintextBytes2, originalData)).to.be.true; // test unable to decrypt the message if derived key has an unexpected path @@ -526,7 +526,7 @@ describe('RecordsReadHandler.handle()', () => { const plaintextDataStream = await Records.decrypt(unsignedRecordsWrite, descendantPrivateKey, cipherStream); const plaintextBytes = await DataStream.toBytes(plaintextDataStream); - expect(Comparer.byteArraysEqual(plaintextBytes, bobMessageBytes)).to.be.true; + expect(ArrayUtility.byteArraysEqual(plaintextBytes, bobMessageBytes)).to.be.true; // test unable to decrypt the message if derived key has an unexpected path const invalidDerivationPath = [KeyDerivationScheme.Protocols, protocol, 'invalidContextId']; diff --git a/tests/interfaces/records/handlers/records-write.spec.ts b/tests/interfaces/records/handlers/records-write.spec.ts index df0703039..3d9d4ff18 100644 --- a/tests/interfaces/records/handlers/records-write.spec.ts +++ b/tests/interfaces/records/handlers/records-write.spec.ts @@ -13,7 +13,7 @@ import sinon from 'sinon'; import socialMediaProtocolDefinition from '../../../vectors/protocol-definitions/social-media.json' assert { type: 'json' }; import chai, { expect } from 'chai'; -import { asyncGeneratorToArray } from '../../../../src/utils/array.js'; +import { ArrayUtility } from '../../../../src/utils/array.js'; import { base64url } from 'multiformats/bases/base64'; import { DataStoreLevel } from '../../../../src/store/data-store-level.js'; import { DataStream } from '../../../../src/utils/data-stream.js'; @@ -30,6 +30,7 @@ import { KeyDerivationScheme } from '../../../../src/index.js'; import { Message } from '../../../../src/core/message.js'; import { MessageStoreLevel } from '../../../../src/store/message-store-level.js'; import { ProtocolActor } from '../../../../src/interfaces/protocols/types.js'; +import { RecordsRead } from '../../../../src/interfaces/records/messages/records-read.js'; import { RecordsWrite } from '../../../../src/interfaces/records/messages/records-write.js'; import { RecordsWriteHandler } from '../../../../src/interfaces/records/handlers/records-write.js'; import { StorageController } from '../../../../src/store/storage-controller.js'; @@ -308,7 +309,7 @@ describe('RecordsWriteHandler.handle()', () => { expect(reply.status.detail).to.contain('does not match dataCid in descriptor'); }); - it('should return 400 if attempting to write a record without data stream and the data does not already exist in DWN', async () => { + it('should return 400 if attempting to write a record without data stream', async () => { const alice = await DidKeyResolver.generate(); const { message } = await TestDataGenerator.generateRecordsWrite({ @@ -318,7 +319,62 @@ describe('RecordsWriteHandler.handle()', () => { const reply = await dwn.processMessage(alice.did, message); expect(reply.status.code).to.equal(400); - expect(reply.status.detail).to.contain(DwnErrorCode.StorageControllerDataNotFound); + expect(reply.status.detail).to.contain(DwnErrorCode.RecordsWriteMissingDataStream); + }); + + it('#359 - should not allow access of data by referencing a different`dataCid` in "modify" `RecordsWrite`', async () => { + const alice = await DidKeyResolver.generate(); + + // alice writes a record + const dataString = 'private data'; + const dataSize = dataString.length; + const data = Encoder.stringToBytes(dataString); + const dataCid = await Cid.computeDagPbCidFromBytes(data); + + const write1 = await TestDataGenerator.generateRecordsWrite({ + requester: alice, + data, + }); + + const write1Reply = await dwn.processMessage(alice.did, write1.message, write1.dataStream); + expect(write1Reply.status.code).to.equal(202); + + // alice writes another record (which will be modified later) + const write2 = await TestDataGenerator.generateRecordsWrite({ requester: alice }); + const write2Reply = await dwn.processMessage(alice.did, write2.message, write2.dataStream); + expect(write2Reply.status.code).to.equal(202); + + // modify write2 by referencing the `dataCid` in write1 (which should not be allowed) + const write2Change = await TestDataGenerator.generateRecordsWrite({ + requester : alice, + // immutable properties just inherit from the message given + recipientDid : write2.message.descriptor.recipient, + recordId : write2.message.recordId, + dateCreated : write2.message.descriptor.dateCreated, + contextId : write2.message.contextId, + protocolPath : write2.message.descriptor.protocolPath, + parentId : write2.message.descriptor.parentId, + schema : write2.message.descriptor.schema, + dataFormat : write2.message.descriptor.dataFormat, + // unauthorized reference to data in write1 + dataCid, + dataSize + }); + const write2ChangeReply = await dwn.processMessage(alice.did, write2Change.message); + expect(write2ChangeReply.status.code).to.equal(400); // should be disallowed + expect(write2ChangeReply.status.detail).to.contain(DwnErrorCode.RecordsWriteMissingDataStream); + + // further sanity test to make sure the change is not written, ie. write2 still has the original data + const read = await RecordsRead.create({ + recordId : write2.message.recordId, + authorizationSignatureInput : Jws.createSignatureInput(alice) + }); + + const readReply = await dwn.handleRecordsRead(alice.did, read.message); + expect(readReply.status.code).to.equal(200); + + const readDataBytes = await DataStream.toBytes(readReply.record!.data!); + expect(ArrayUtility.byteArraysEqual(readDataBytes, write2.dataBytes!)).to.be.true; }); describe('initial write & subsequent write tests', () => { @@ -342,7 +398,7 @@ describe('RecordsWriteHandler.handle()', () => { const reply = await dwn.processMessage(tenant, message, dataStream); expect(reply.status.code).to.equal(202); - expect(asyncGeneratorToArray(dataForCid.db.keys())).to.eventually.eql([ dataCid ]); + expect(ArrayUtility.fromAsyncGenerator(dataForCid.db.keys())).to.eventually.eql([ dataCid ]); const newWrite = await RecordsWrite.createFrom({ unsignedRecordsWriteMessage : recordsWrite.message, @@ -353,7 +409,7 @@ describe('RecordsWriteHandler.handle()', () => { const newWriteReply = await dwn.processMessage(tenant, newWrite.message); expect(newWriteReply.status.code).to.equal(202); - expect(asyncGeneratorToArray(dataForCid.db.keys())).to.eventually.eql([ dataCid ]); + expect(ArrayUtility.fromAsyncGenerator(dataForCid.db.keys())).to.eventually.eql([ dataCid ]); // verify the new record state can be queried const recordsQueryMessageData = await TestDataGenerator.generateRecordsQuery({ @@ -369,7 +425,7 @@ describe('RecordsWriteHandler.handle()', () => { // very importantly verify the original data is still returned expect(recordsQueryReply.entries![0].encodedData).to.equal(encodedData); - expect(asyncGeneratorToArray(dataForCid.db.keys())).to.eventually.eql([ dataCid ]); + expect(ArrayUtility.fromAsyncGenerator(dataForCid.db.keys())).to.eventually.eql([ dataCid ]); }); it('should inherit parent published state when using createFrom() to create RecordsWrite', async () => { @@ -1543,6 +1599,253 @@ describe('RecordsWriteHandler.handle()', () => { expect(reply.status.code).to.equal(400); expect(reply.status.detail).to.contain(DwnErrorCode.UrlProtocolNotNormalized); }); + + it('#359 - should not allow access of data by referencing `dataCid` in protocol authorized `RecordsWrite`', async () => { + const alice = await DidKeyResolver.generate(); + const bob = await DidKeyResolver.generate(); + + const dataForCid = await dataStore.blockstore.partition('data'); + + // alice writes a private record + const dataString = 'private data'; + const dataSize = dataString.length; + const data = Encoder.stringToBytes(dataString); + const dataCid = await Cid.computeDagPbCidFromBytes(data); + + const { message, dataStream } = await TestDataGenerator.generateRecordsWrite({ + requester: alice, + data, + }); + + const reply = await dwn.processMessage(alice.did, message, dataStream); + expect(reply.status.code).to.equal(202); + + expect(ArrayUtility.fromAsyncGenerator(dataForCid.db.keys())).to.eventually.eql([ dataCid ]); + + const protocol = 'https://tbd.website/decentralized-web-node/protocols/social-media'; + const protocolDefinition = socialMediaProtocolDefinition; + + // alice has a social media protocol that allows anyone to write and read images + const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({ + requester: alice, + protocol, + protocolDefinition + }); + const protocolWriteReply = await dwn.processMessage(alice.did, protocolsConfig.message, protocolsConfig.dataStream); + expect(protocolWriteReply.status.code).to.equal(202); + + // bob learns of metadata (ie. dataCid) of alice's secret data, + // attempts to gain unauthorized access by writing to alice's DWN through open protocol referencing the dataCid without supplying the data + const imageRecordsWrite = await TestDataGenerator.generateRecordsWrite({ + requester : bob, + protocol, + protocolPath : 'image', + schema : protocolDefinition.types.image.schema, + dataFormat : 'image/jpeg', + dataCid, // bob learns of, and references alice's secrete data's CID + dataSize, + recipientDid : alice.did + }); + const imageReply = await dwn.processMessage(alice.did, imageRecordsWrite.message, imageRecordsWrite.dataStream); + expect(imageReply.status.code).to.equal(400); // should be disallowed + expect(imageReply.status.detail).to.contain(DwnErrorCode.RecordsWriteMissingDataStream); + + // further sanity test to make sure record is never written + const bobRecordsReadData = await RecordsRead.create({ + recordId : imageRecordsWrite.message.recordId, + authorizationSignatureInput : Jws.createSignatureInput(bob) + }); + + const bobRecordsReadReply = await dwn.handleRecordsRead(alice.did, bobRecordsReadData.message); + expect(bobRecordsReadReply.status.code).to.equal(404); + }); + }); + + describe('reference counting tests', () => { + it('should only write the data once even if written by multiple messages', async () => { + const alice = await DidKeyResolver.generate(); + const data = Encoder.stringToBytes('test'); + const dataCid = await Cid.computeDagPbCidFromBytes(data); + const encodedData = Encoder.bytesToBase64Url(data); + + const blockstoreForData = await dataStore.blockstore.partition('data'); + const blockstoreOfGivenTenant = await blockstoreForData.partition(alice.did); + const blockstoreOfGivenDataCid = await blockstoreOfGivenTenant.partition(dataCid); + + const aliceWrite1Data = await TestDataGenerator.generateRecordsWrite({ + requester: alice, + data + }); + const aliceWrite1Reply = await dwn.processMessage(alice.did, aliceWrite1Data.message, aliceWrite1Data.dataStream); + expect(aliceWrite1Reply.status.code).to.equal(202); + + const aliceQueryWrite1AfterAliceWrite1Data = await TestDataGenerator.generateRecordsQuery({ + requester : alice, + filter : { recordId: aliceWrite1Data.message.recordId } + }); + const aliceQueryWrite1AfterAliceWrite1Reply = await dwn.processMessage(alice.did, aliceQueryWrite1AfterAliceWrite1Data.message); + expect(aliceQueryWrite1AfterAliceWrite1Reply.status.code).to.equal(200); + expect(aliceQueryWrite1AfterAliceWrite1Reply.entries?.length).to.equal(1); + expect(aliceQueryWrite1AfterAliceWrite1Reply.entries![0].encodedData).to.equal(encodedData); + + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfGivenDataCid.db.keys())).to.eventually.eql([ dataCid ]); + + const aliceWrite2Data = await TestDataGenerator.generateRecordsWrite({ + requester: alice, + data + }); + const aliceWrite2Reply = await dwn.processMessage(alice.did, aliceWrite2Data.message, aliceWrite2Data.dataStream); + expect(aliceWrite2Reply.status.code).to.equal(202); + + const aliceQueryWrite1AfterAliceWrite2Data = await TestDataGenerator.generateRecordsQuery({ + requester : alice, + filter : { recordId: aliceWrite1Data.message.recordId } + }); + const aliceQueryWrite1AfterAliceWrite2Reply = await dwn.processMessage(alice.did, aliceQueryWrite1AfterAliceWrite2Data.message); + expect(aliceQueryWrite1AfterAliceWrite2Reply.status.code).to.equal(200); + expect(aliceQueryWrite1AfterAliceWrite2Reply.entries?.length).to.equal(1); + expect(aliceQueryWrite1AfterAliceWrite2Reply.entries![0].encodedData).to.equal(encodedData); + + const aliceQueryWrite2AfterAliceWrite2Data = await TestDataGenerator.generateRecordsQuery({ + requester : alice, + filter : { recordId: aliceWrite2Data.message.recordId } + }); + const aliceQueryWrite2AfterAliceWrite2Reply = await dwn.processMessage(alice.did, aliceQueryWrite2AfterAliceWrite2Data.message); + expect(aliceQueryWrite2AfterAliceWrite2Reply.status.code).to.equal(200); + expect(aliceQueryWrite2AfterAliceWrite2Reply.entries?.length).to.equal(1); + expect(aliceQueryWrite2AfterAliceWrite2Reply.entries![0].encodedData).to.equal(encodedData); + + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfGivenDataCid.db.keys())).to.eventually.eql([ dataCid ]); + }); + + it('should duplicate same data if written to different tenants', async () => { + const alice = await DidKeyResolver.generate(); + const bob = await DidKeyResolver.generate(); + + const data = Encoder.stringToBytes('test'); + const dataCid = await Cid.computeDagPbCidFromBytes(data); + const encodedData = Encoder.bytesToBase64Url(data); + + const blockstoreForData = await dataStore.blockstore.partition('data'); + const blockstoreOfAlice = await blockstoreForData.partition(alice.did); + const blockstoreOfAliceOfDataCid = await blockstoreOfAlice.partition(dataCid); + + const blockstoreOfBob = await blockstoreForData.partition(bob.did); + const blockstoreOfBobOfDataCid = await blockstoreOfBob.partition(dataCid); + + // write data to alice's DWN + const aliceWriteData = await TestDataGenerator.generateRecordsWrite({ + requester: alice, + data + }); + const aliceWriteReply = await dwn.processMessage(alice.did, aliceWriteData.message, aliceWriteData.dataStream); + expect(aliceWriteReply.status.code).to.equal(202); + + const aliceQueryWriteAfterAliceWriteData = await TestDataGenerator.generateRecordsQuery({ + requester : alice, + filter : { recordId: aliceWriteData.message.recordId } + }); + const aliceQueryWriteAfterAliceWriteReply = await dwn.processMessage(alice.did, aliceQueryWriteAfterAliceWriteData.message); + expect(aliceQueryWriteAfterAliceWriteReply.status.code).to.equal(200); + expect(aliceQueryWriteAfterAliceWriteReply.entries?.length).to.equal(1); + expect(aliceQueryWriteAfterAliceWriteReply.entries![0].encodedData).to.equal(encodedData); + + // write same data to bob's DWN + const bobWriteData = await TestDataGenerator.generateRecordsWrite({ + requester: bob, + data + }); + const bobWriteReply = await dwn.processMessage(bob.did, bobWriteData.message, bobWriteData.dataStream); + expect(bobWriteReply.status.code).to.equal(202); + + const aliceQueryWriteAfterBobWriteData = await TestDataGenerator.generateRecordsQuery({ + requester : alice, + filter : { recordId: aliceWriteData.message.recordId } + }); + const aliceQueryWriteAfterBobWriteReply = await dwn.processMessage(alice.did, aliceQueryWriteAfterBobWriteData.message); + expect(aliceQueryWriteAfterBobWriteReply.status.code).to.equal(200); + expect(aliceQueryWriteAfterBobWriteReply.entries?.length).to.equal(1); + expect(aliceQueryWriteAfterBobWriteReply.entries![0].encodedData).to.equal(encodedData); + + const bobQueryWriteAfterBobWriteData = await TestDataGenerator.generateRecordsQuery({ + requester : bob, + filter : { recordId: bobWriteData.message.recordId } + }); + const bobQueryWriteAfterBobWriteReply = await dwn.processMessage(bob.did, bobQueryWriteAfterBobWriteData.message); + expect(bobQueryWriteAfterBobWriteReply.status.code).to.equal(200); + expect(bobQueryWriteAfterBobWriteReply.entries?.length).to.equal(1); + expect(bobQueryWriteAfterBobWriteReply.entries![0].encodedData).to.equal(encodedData); + + // verify that both alice and bob's blockstore have reference to the same data CID + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfBobOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + }); + + it('should not allow referencing data across tenants', async () => { + const alice = await DidKeyResolver.generate(); + const bob = await DidKeyResolver.generate(); + const data = Encoder.stringToBytes('test'); + const dataCid = await Cid.computeDagPbCidFromBytes(data); + const encodedData = Encoder.bytesToBase64Url(data); + + const blockstoreForData = await dataStore.blockstore.partition('data'); + const blockstoreOfAlice = await blockstoreForData.partition(alice.did); + const blockstoreOfAliceOfDataCid = await blockstoreOfAlice.partition(dataCid); + + const blockstoreOfBob = await blockstoreForData.partition(bob.did); + const blockstoreOfBobOfDataCid = await blockstoreOfBob.partition(dataCid); + + // alice writes data to her DWN + const aliceWriteData = await TestDataGenerator.generateRecordsWrite({ + requester: alice, + data + }); + const aliceWriteReply = await dwn.processMessage(alice.did, aliceWriteData.message, aliceWriteData.dataStream); + expect(aliceWriteReply.status.code).to.equal(202); + + const aliceQueryWriteAfterAliceWriteData = await TestDataGenerator.generateRecordsQuery({ + requester : alice, + filter : { recordId: aliceWriteData.message.recordId } + }); + const aliceQueryWriteAfterAliceWriteReply = await dwn.processMessage(alice.did, aliceQueryWriteAfterAliceWriteData.message); + expect(aliceQueryWriteAfterAliceWriteReply.status.code).to.equal(200); + expect(aliceQueryWriteAfterAliceWriteReply.entries?.length).to.equal(1); + expect(aliceQueryWriteAfterAliceWriteReply.entries![0].encodedData).to.equal(encodedData); + + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + + // bob learns of the CID of data of alice and tries to gain unauthorized access by referencing it in his own DWN + const bobAssociateData = await TestDataGenerator.generateRecordsWrite({ + requester : bob, + dataCid, + dataSize : 4 + }); + const bobAssociateReply = await dwn.processMessage(bob.did, bobAssociateData.message, bobAssociateData.dataStream); + expect(bobAssociateReply.status.code).to.equal(400); // expecting an error + expect(bobAssociateReply.status.detail).to.contain(DwnErrorCode.RecordsWriteMissingDataStream); + + const aliceQueryWriteAfterBobAssociateData = await TestDataGenerator.generateRecordsQuery({ + requester : alice, + filter : { recordId: aliceWriteData.message.recordId } + }); + const aliceQueryWriteAfterBobAssociateReply = await dwn.processMessage(alice.did, aliceQueryWriteAfterBobAssociateData.message); + expect(aliceQueryWriteAfterBobAssociateReply.status.code).to.equal(200); + expect(aliceQueryWriteAfterBobAssociateReply.entries?.length).to.equal(1); + expect(aliceQueryWriteAfterBobAssociateReply.entries![0].encodedData).to.equal(encodedData); + + // verify that bob has not gained access to alice's data + const bobQueryAssociateAfterBobAssociateData = await TestDataGenerator.generateRecordsQuery({ + requester : bob, + filter : { recordId: bobAssociateData.message.recordId } + }); + const bobQueryAssociateAfterBobAssociateReply = await dwn.processMessage(bob.did, bobQueryAssociateAfterBobAssociateData.message); + expect(bobQueryAssociateAfterBobAssociateReply.status.code).to.equal(200); + expect(bobQueryAssociateAfterBobAssociateReply.entries?.length).to.equal(0); + + // verify that bob's blockstore does not contain alice's data + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfAliceOfDataCid.db.keys())).to.eventually.eql([ dataCid ]); + await expect(ArrayUtility.fromAsyncGenerator(blockstoreOfBobOfDataCid.db.keys())).to.eventually.eql([ ]); + }); }); }); diff --git a/tests/store/data-store.spec.ts b/tests/store/data-store.spec.ts index 1046cf018..124c71829 100644 --- a/tests/store/data-store.spec.ts +++ b/tests/store/data-store.spec.ts @@ -1,7 +1,7 @@ import chaiAsPromised from 'chai-as-promised'; import chai, { expect } from 'chai'; -import { asyncGeneratorToArray } from '../../src/utils/array.js'; +import { ArrayUtility } from '../../src/utils/array.js'; import { Cid } from '../../src/utils/cid.js'; import { DataStoreLevel } from '../../src/store/data-store-level.js'; import { DataStream } from '../../src/index.js'; @@ -88,13 +88,13 @@ describe('DataStore Test Suite', () => { const messageCid = await TestDataGenerator.randomCborSha256Cid(); const randomCid = await TestDataGenerator.randomCborSha256Cid(); - const keysBeforeAssociate = await asyncGeneratorToArray(store.blockstore.db.keys()); + const keysBeforeAssociate = await ArrayUtility.fromAsyncGenerator(store.blockstore.db.keys()); expect(keysBeforeAssociate.length).to.equal(0); const result = await store.associate(tenant, messageCid, randomCid); expect(result).to.be.undefined; - const keysAfterAssociate = await asyncGeneratorToArray(store.blockstore.db.keys()); + const keysAfterAssociate = await ArrayUtility.fromAsyncGenerator(store.blockstore.db.keys()); expect(keysAfterAssociate.length).to.equal(0); }); @@ -109,13 +109,13 @@ describe('DataStore Test Suite', () => { const { dataCid } = await store.put(tenant, messageCid, randomCid, dataStream); expect(dataCid).to.not.equal(randomCid); - const keysBeforeAssociate = await asyncGeneratorToArray(store.blockstore.db.keys()); + const keysBeforeAssociate = await ArrayUtility.fromAsyncGenerator(store.blockstore.db.keys()); expect(keysBeforeAssociate.length).to.equal(2); const result = await store.associate(tenant, messageCid, randomCid); expect(result).to.be.undefined; - const keysAfterAssociate = await asyncGeneratorToArray(store.blockstore.db.keys()); + const keysAfterAssociate = await ArrayUtility.fromAsyncGenerator(store.blockstore.db.keys()); expect(keysAfterAssociate.length).to.equal(2); }); @@ -129,14 +129,14 @@ describe('DataStore Test Suite', () => { await store.put(tenant, messageCid, dataCid, dataStream); - const keysBeforeDelete = await asyncGeneratorToArray(store.blockstore.db.keys()); + const keysBeforeDelete = await ArrayUtility.fromAsyncGenerator(store.blockstore.db.keys()); expect(keysBeforeDelete.length).to.equal(41); const result = (await store.associate(tenant, messageCid, dataCid))!; expect(result.dataCid).to.equal(dataCid); expect(result.dataSize).to.equal(10_000_000); - const keysAfterDelete = await asyncGeneratorToArray(store.blockstore.db.keys()); + const keysAfterDelete = await ArrayUtility.fromAsyncGenerator(store.blockstore.db.keys()); expect(keysAfterDelete.length).to.equal(41); }); }); @@ -152,12 +152,12 @@ describe('DataStore Test Suite', () => { await store.put(tenant, messageCid, dataCid, dataStream); - const keysBeforeDelete = await asyncGeneratorToArray(store.blockstore.db.keys()); + const keysBeforeDelete = await ArrayUtility.fromAsyncGenerator(store.blockstore.db.keys()); expect(keysBeforeDelete.length).to.equal(41); await store.delete(tenant, messageCid, dataCid); - const keysAfterDelete = await asyncGeneratorToArray(store.blockstore.db.keys()); + const keysAfterDelete = await ArrayUtility.fromAsyncGenerator(store.blockstore.db.keys()); expect(keysAfterDelete.length).to.equal(0); }); }); diff --git a/tests/store/index-level.spec.ts b/tests/store/index-level.spec.ts index 5ef1ce756..f10c94a42 100644 --- a/tests/store/index-level.spec.ts +++ b/tests/store/index-level.spec.ts @@ -1,7 +1,7 @@ import chaiAsPromised from 'chai-as-promised'; import chai, { expect } from 'chai'; -import { asyncGeneratorToArray } from '../../src/utils/array.js'; +import { ArrayUtility } from '../../src/utils/array.js'; import { IndexLevel } from '../../src/store/index-level.js'; import { Temporal } from '@js-temporal/polyfill'; import { v4 as uuid } from 'uuid'; @@ -32,7 +32,7 @@ describe('Index Level', () => { 'c' : 'd' }); - const keys = await asyncGeneratorToArray(index.db.keys()); + const keys = await ArrayUtility.fromAsyncGenerator(index.db.keys()); expect(keys.length).to.equal(4); }); diff --git a/tests/utils/comparer.ts b/tests/utils/comparer.ts deleted file mode 100644 index 2af3360ab..000000000 --- a/tests/utils/comparer.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Class containing data comparison utilities. - */ -export class Comparer { - /** - * Returns `true` if content of the two given byte arrays are equal; `false` otherwise. - */ - public static byteArraysEqual(array1: Uint8Array, array2:Uint8Array): boolean { - const equal = array1.length === array2.length && array1.every((value, index) => value === array2[index]); - return equal; - } -} \ No newline at end of file diff --git a/tests/utils/encryption.spec.ts b/tests/utils/encryption.spec.ts index 546c3d147..f6341cea7 100644 --- a/tests/utils/encryption.spec.ts +++ b/tests/utils/encryption.spec.ts @@ -1,4 +1,4 @@ -import { Comparer } from '../utils/comparer.js'; +import { ArrayUtility } from '../../src/utils/array.js'; import { DataStream } from '../../src/index.js'; import { Encryption } from '../../src/utils/encryption.js'; import { expect } from 'chai'; @@ -20,7 +20,7 @@ describe('Encryption', () => { const plaintextStream = await Encryption.aes256CtrDecrypt(key, initializationVector, cipherStream); const plaintextBytes = await DataStream.toBytes(plaintextStream); - expect(Comparer.byteArraysEqual(inputBytes, plaintextBytes)).to.be.true; + expect(ArrayUtility.byteArraysEqual(inputBytes, plaintextBytes)).to.be.true; }); it('should emit error on encrypt if the plaintext data stream emits an error', async () => { @@ -115,7 +115,7 @@ describe('Encryption', () => { const decryptionInput = { privateKey, ...encryptionOutput }; const decryptedPlaintext = await Encryption.eciesSecp256k1Decrypt(decryptionInput); - expect(Comparer.byteArraysEqual(originalPlaintext, decryptedPlaintext)).to.be.true; + expect(ArrayUtility.byteArraysEqual(originalPlaintext, decryptedPlaintext)).to.be.true; }); }); }); diff --git a/tests/utils/secp256k1.spec.ts b/tests/utils/secp256k1.spec.ts index 81a5ae001..dc77505d6 100644 --- a/tests/utils/secp256k1.spec.ts +++ b/tests/utils/secp256k1.spec.ts @@ -1,5 +1,5 @@ +import { ArrayUtility } from '../../src/utils/array.js'; import { base64url } from 'multiformats/bases/base64'; -import { Comparer } from './comparer.js'; import { DwnErrorCode } from '../../src/core/dwn-error.js'; import { expect } from 'chai'; import { Secp256k1 } from '../../src/utils/secp256k1.js'; @@ -51,7 +51,7 @@ describe('Secp256k1', () => { const derivedPublicKey = Secp256k1.deriveChildPublicKey(publicKey, tweakInput); const publicKeyFromDerivedPrivateKey = await Secp256k1.getPublicKey(derivedPrivateKey); - expect(Comparer.byteArraysEqual(derivedPublicKey, publicKeyFromDerivedPrivateKey)).to.be.true; + expect(ArrayUtility.byteArraysEqual(derivedPublicKey, publicKeyFromDerivedPrivateKey)).to.be.true; }); }); @@ -67,17 +67,17 @@ describe('Secp256k1', () => { const publicKeyG = await Secp256k1.derivePublicKey(publicKey, fullPathToG); const publicKeyD = await Secp256k1.derivePublicKey(publicKey, fullPathToD); const publicKeyGFromD = await Secp256k1.derivePublicKey(publicKeyD, relativePathFromDToG); - expect(Comparer.byteArraysEqual(publicKeyG, publicKeyGFromD)).to.be.true; + expect(ArrayUtility.byteArraysEqual(publicKeyG, publicKeyGFromD)).to.be.true; // testing private key derivation from different ancestor in the same chain const privateKeyG = await Secp256k1.derivePrivateKey(privateKey, fullPathToG); const privateKeyD = await Secp256k1.derivePrivateKey(privateKey, fullPathToD); const privateKeyGFromD = await Secp256k1.derivePrivateKey(privateKeyD, relativePathFromDToG); - expect(Comparer.byteArraysEqual(privateKeyG, privateKeyGFromD)).to.be.true; + expect(ArrayUtility.byteArraysEqual(privateKeyG, privateKeyGFromD)).to.be.true; // testing that the derived private key matches up with the derived public key const publicKeyGFromPrivateKeyG = await Secp256k1.getPublicKey(privateKeyG); - expect(Comparer.byteArraysEqual(publicKeyGFromPrivateKeyG, publicKeyG)).to.be.true; + expect(ArrayUtility.byteArraysEqual(publicKeyGFromPrivateKeyG, publicKeyG)).to.be.true; }); it('should derive the same public key using either the private or public counterpart of the same key pair', async () => { @@ -87,7 +87,7 @@ describe('Secp256k1', () => { const derivedKeyFromPublicKey = await Secp256k1.derivePublicKey(publicKey, path); const derivedKeyFromPrivateKey = await Secp256k1.derivePublicKey(privateKey, path); - expect(Comparer.byteArraysEqual(derivedKeyFromPublicKey, derivedKeyFromPrivateKey)).to.be.true; + expect(ArrayUtility.byteArraysEqual(derivedKeyFromPublicKey, derivedKeyFromPrivateKey)).to.be.true; }); it('should derive the same public key using either the private or public counterpart of the same key pair', async () => {