Skip to content

Commit 03bcb1c

Browse files
authored
#359 - fixed unauthorized access of data through dataCid reference in RecordsWrite
1 parent f8c0fa7 commit 03bcb1c

File tree

15 files changed

+448
-387
lines changed

15 files changed

+448
-387
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
# Decentralized Web Node (DWN) SDK
44

55
Code Coverage
6-
![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)
6+
![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)
77

88
## Introduction
99

10-
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.
10+
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.
1111

1212
This project is used as a dependency by several other projects.
1313

src/core/dwn-error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ export enum DwnErrorCode {
2929
RecordsProtocolsDerivationSchemeMissingProtocol = 'RecordsProtocolsDerivationSchemeMissingProtocol',
3030
RecordsSchemasDerivationSchemeMissingSchema = 'RecordsSchemasDerivationSchemeMissingSchema',
3131
RecordsWriteGetEntryIdUndefinedAuthor = 'RecordsWriteGetEntryIdUndefinedAuthor',
32+
RecordsWriteMissingDataStream = 'RecordsWriteMissingDataStream',
3233
RecordsWriteValidateIntegrityEncryptionCidMismatch = 'RecordsWriteValidateIntegrityEncryptionCidMismatch',
3334
Secp256k1KeyNotValid = 'Secp256k1KeyNotValid',
3435
StorageControllerDataCidMismatch = 'StorageControllerDataCidMismatch',
35-
StorageControllerDataNotFound = 'StorageControllerDataNotFound',
3636
StorageControllerDataSizeMismatch = 'StorageControllerDataSizeMismatch',
3737
UrlProtocolNotNormalized = 'UrlProtocolNotNormalized',
3838
UrlProtocolNotNormalizable = 'UrlProtocolNotNormalizable',

src/interfaces/records/handlers/pruned-initial-records-write.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { BaseMessage } from '../../../core/types.js';
21
import type { EventLog } from '../../../event-log/event-log.js';
32
import type { Readable } from 'readable-stream';
4-
import type { DataStore, MessageStore } from '../../../index.js';
3+
import type { BaseMessage, TimestampedMessage } from '../../../core/types.js';
4+
import type { DataStore, MessageStore, RecordsWriteMessage } from '../../../index.js';
55

66
import { Message } from '../../../core/message.js';
77
import { RecordsWriteHandler } from './records-write.js';
@@ -11,11 +11,19 @@ import { RecordsWriteHandler } from './records-write.js';
1111
* NOTE: This is intended to be ONLY used by sync.
1212
*/
1313
export class PrunedInitialRecordsWriteHandler extends RecordsWriteHandler {
14+
/**
15+
* Overriding parent behavior, `undefined` data stream is allowed.
16+
*/
17+
protected validateUndefinedDataStream(
18+
_dataStream: Readable | undefined,
19+
_newestExistingMessage: TimestampedMessage | undefined,
20+
_incomingMessage: RecordsWriteMessage): void { }
21+
1422
/**
1523
* Stores the given message without storing the associated data.
1624
* Requires `dataCid` to exist in the `RecordsWrite` message given.
1725
*/
18-
public async storeMessage(
26+
protected async storeMessage(
1927
messageStore: MessageStore,
2028
_dataStore: DataStore,
2129
eventLog: EventLog,

src/interfaces/records/handlers/records-write.ts

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ import type { DataStore, DidResolver, MessageStore } from '../../../index.js';
77

88
import { authenticate } from '../../../core/auth.js';
99
import { deleteAllOlderMessagesButKeepInitialWrite } from '../records-interface.js';
10-
import { DwnErrorCode } from '../../../core/dwn-error.js';
11-
import { DwnInterfaceName } from '../../../core/message.js';
1210
import { MessageReply } from '../../../core/message-reply.js';
1311
import { RecordsWrite } from '../messages/records-write.js';
1412
import { StorageController } from '../../../store/storage-controller.js';
13+
import { DwnError, DwnErrorCode } from '../../../core/dwn-error.js';
14+
import { DwnInterfaceName, Message } from '../../../core/message.js';
1515

1616
export class RecordsWriteHandler implements MethodHandler {
1717

@@ -56,62 +56,80 @@ export class RecordsWriteHandler implements MethodHandler {
5656
}
5757
}
5858

59-
// find which message is the newest, and if the incoming message is the newest
60-
const newestExistingMessage = await RecordsWrite.getNewestMessage(existingMessages);
59+
const newestExistingMessage = await Message.getNewestMessage(existingMessages);
6160

6261
let incomingMessageIsNewest = false;
63-
let newestMessage;
64-
// if incoming message is newest
65-
if (newestExistingMessage === undefined || await RecordsWrite.isNewer(message, newestExistingMessage)) {
62+
let newestMessage; // keep reference of newest message for pruning later
63+
if (newestExistingMessage === undefined || await Message.isNewer(message, newestExistingMessage)) {
6664
incomingMessageIsNewest = true;
6765
newestMessage = message;
6866
} else { // existing message is the same age or newer than the incoming message
6967
newestMessage = newestExistingMessage;
7068
}
7169

72-
// write the incoming message to DB if incoming message is newest
73-
let messageReply: MessageReply;
74-
if (incomingMessageIsNewest) {
75-
const isLatestBaseState = true;
76-
const indexes = await constructRecordsWriteIndexes(recordsWrite, isLatestBaseState);
70+
if (!incomingMessageIsNewest) {
71+
return new MessageReply({
72+
status: { code: 409, detail: 'Conflict' }
73+
});
74+
}
7775

78-
try {
79-
await this.storeMessage(this.messageStore, this.dataStore, this.eventLog, tenant, message, indexes, dataStream);
80-
} catch (error) {
81-
const e = error as any;
82-
if (e.code === DwnErrorCode.StorageControllerDataCidMismatch ||
83-
e.code === DwnErrorCode.StorageControllerDataNotFound ||
84-
e.code === DwnErrorCode.StorageControllerDataSizeMismatch) {
85-
return MessageReply.fromError(error, 400);
86-
}
87-
88-
// else throw
89-
throw error;
76+
const isLatestBaseState = true;
77+
const indexes = await constructRecordsWriteIndexes(recordsWrite, isLatestBaseState);
78+
79+
try {
80+
this.validateUndefinedDataStream(dataStream, newestExistingMessage, message);
81+
82+
await this.storeMessage(this.messageStore, this.dataStore, this.eventLog, tenant, message, indexes, dataStream);
83+
} catch (error) {
84+
const e = error as any;
85+
if (e.code === DwnErrorCode.StorageControllerDataCidMismatch ||
86+
e.code === DwnErrorCode.StorageControllerDataSizeMismatch ||
87+
e.code === DwnErrorCode.RecordsWriteMissingDataStream) {
88+
return MessageReply.fromError(error, 400);
9089
}
9190

92-
messageReply = new MessageReply({
93-
status: { code: 202, detail: 'Accepted' }
94-
});
95-
} else {
96-
messageReply = new MessageReply({
97-
status: { code: 409, detail: 'Conflict' }
98-
});
91+
// else throw
92+
throw error;
9993
}
10094

95+
const messageReply = new MessageReply({
96+
status: { code: 202, detail: 'Accepted' }
97+
});
98+
10199
// delete all existing messages that are not newest, except for the initial write
102100
await deleteAllOlderMessagesButKeepInitialWrite(tenant, existingMessages, newestMessage, this.messageStore, this.dataStore, this.eventLog);
103101

104102
return messageReply;
105103
};
106104

105+
/**
106+
* Further validation if data stream is undefined.
107+
* NOTE: if data stream is not be provided but `dataCid` is provided,
108+
* then we need to make sure that the existing record state is referencing the same data as the incoming message.
109+
* Without this check will lead to unauthorized access of data (https://github.com/TBD54566975/dwn-sdk-js/issues/359)
110+
*/
111+
protected validateUndefinedDataStream(
112+
dataStream: _Readable.Readable | undefined,
113+
newestExistingMessage: TimestampedMessage | undefined,
114+
incomingMessage: RecordsWriteMessage): void {
115+
if (dataStream === undefined && incomingMessage.descriptor.dataCid !== undefined) {
116+
if (newestExistingMessage?.descriptor.dataCid !== incomingMessage.descriptor.dataCid) {
117+
throw new DwnError(
118+
DwnErrorCode.RecordsWriteMissingDataStream,
119+
'Data stream is not provided.'
120+
);
121+
}
122+
}
123+
}
124+
107125
/**
108126
* Stores the given message and its data in the underlying database(s).
109127
* NOTE: this method was created to allow a child class to override the default behavior for sync feature to work:
110128
* ie. allow `RecordsWrite` to be written even if data stream is not provided to handle the case that:
111129
* a `RecordsDelete` has happened, as a result a DWN would have pruned the data associated with the original write.
112130
* This approach avoids the need to duplicate the entire handler.
113131
*/
114-
public async storeMessage(
132+
protected async storeMessage(
115133
messageStore: MessageStore,
116134
dataStore: DataStore,
117135
eventLog: EventLog,

src/store/storage-controller.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ export class StorageController {
1818
* Puts the given message and data in storage.
1919
* @throws {DwnError} with `DwnErrorCode.StorageControllerDataCidMismatch`
2020
* if the data stream resulted in a data CID that mismatches with `dataCid` in the given message
21-
* @throws {DwnError} with `DwnErrorCode.StorageControllerDataNotFound`
22-
* if `dataCid` in `descriptor` is given, and `dataStream` is not given, and data for the message does not exist already
2321
* @throws {DwnError} with `DwnErrorCode.StorageControllerDataSizeMismatch`
2422
* if `dataSize` in `descriptor` given mismatches the actual data size
2523
*/
@@ -35,26 +33,18 @@ export class StorageController {
3533
const messageCid = await Message.getCid(message);
3634

3735
// if `dataCid` is given, it means there is corresponding data associated with this message
38-
// but NOTE: it is possible that a data stream is not given in such case, for instance,
39-
// a subsequent RecordsWrite that changes the `published` property, but the data hasn't changed,
40-
// in this case requiring re-uploading of the data is extremely inefficient so we take care allow omission of data stream
4136
if (message.descriptor.dataCid !== undefined) {
4237
let result;
43-
4438
if (dataStream === undefined) {
39+
// but NOTE: it is possible that a data stream is not given even when `dataCid` is given, for instance,
40+
// a subsequent RecordsWrite that changes the `published` property, but the data hasn't changed,
41+
// in this case requiring re-uploading of the data is extremely inefficient so we allow omission of data stream as a utility function,
42+
// but this method assumes checks for the appropriate conditions already took place prior to calling this method.
4543
result = await dataStore.associate(tenant, messageCid, message.descriptor.dataCid);
4644
} else {
4745
result = await dataStore.put(tenant, messageCid, message.descriptor.dataCid, dataStream);
4846
}
4947

50-
// the message implies that the data is already in the DB, so we check to make sure the data already exist
51-
if (!result) {
52-
throw new DwnError(
53-
DwnErrorCode.StorageControllerDataNotFound,
54-
`data with dataCid ${message.descriptor.dataCid} not found in store`
55-
);
56-
}
57-
5848
// MUST verify that the size of the actual data matches with the given `dataSize`
5949
// if data size is wrong, delete the data we just stored
6050
if (message.descriptor.dataSize !== result.dataSize) {

src/utils/array.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
21
/**
3-
* Asynchronously iterates an {AsyncGenerator} to return all the values in an array.
2+
* Array utility methods.
43
*/
5-
export async function asyncGeneratorToArray<T>(iterator: AsyncGenerator<T>): Promise<Array<T>> {
6-
const array: Array<T> = [ ];
7-
for await (const value of iterator) {
8-
array.push(value);
4+
export class ArrayUtility {
5+
/**
6+
* Returns `true` if content of the two given byte arrays are equal; `false` otherwise.
7+
*/
8+
public static byteArraysEqual(array1: Uint8Array, array2:Uint8Array): boolean {
9+
const equal = array1.length === array2.length && array1.every((value, index) => value === array2[index]);
10+
return equal;
11+
}
12+
13+
/**
14+
* Asynchronously iterates an {AsyncGenerator} to return all the values in an array.
15+
*/
16+
public static async fromAsyncGenerator<T>(iterator: AsyncGenerator<T>): Promise<Array<T>> {
17+
const array: Array<T> = [ ];
18+
for await (const value of iterator) {
19+
array.push(value);
20+
}
21+
return array;
922
}
10-
return array;
11-
}
23+
}

0 commit comments

Comments
 (0)