Skip to content

Commit 67dd57a

Browse files
authored
#319 - implemented 'protocols' key derivation scheme for encryption
1 parent 5fa931c commit 67dd57a

File tree

13 files changed

+121
-40
lines changed

13 files changed

+121
-40
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Decentralized Web Node (DWN) SDK
44

55
Code Coverage
6-
![Statements](https://img.shields.io/badge/statements-94.03%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.54%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.75%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.03%25-brightgreen.svg?style=flat)
6+
![Statements](https://img.shields.io/badge/statements-94.07%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.58%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.8%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.07%25-brightgreen.svg?style=flat)
77

88
## Introduction
99

json-schemas/records/records-write.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"derivationScheme": {
4343
"type": "string",
4444
"enum": [
45-
"protocol-context"
45+
"protocols"
4646
]
4747
},
4848
"algorithm": {

src/core/dwn-error.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ export enum DwnErrorCode {
2727
RecordsDeriveLeafPrivateKeyUnSupportedCurve = 'RecordsDeriveLeafPrivateKeyUnSupportedCurve',
2828
RecordsDeriveLeafPublicKeyUnSupportedCurve = 'RecordsDeriveLeafPublicKeyUnSupportedCurve',
2929
RecordsInvalidAncestorKeyDerivationSegment = 'RecordsInvalidAncestorKeyDerivationSegment',
30+
RecordsProtocolsDerivationSchemeMissingProtocol = 'RecordsProtocolsDerivationSchemeMissingProtocol',
3031
RecordsWriteGetEntryIdUndefinedAuthor = 'RecordsWriteGetEntryIdUndefinedAuthor',
3132
RecordsWriteValidateIntegrityEncryptionCidMismatch = 'RecordsWriteValidateIntegrityEncryptionCidMismatch',
3233
Secp256k1KeyNotValid = 'Secp256k1KeyNotValid',
3334
UrlProtocolNotNormalized = 'UrlProtocolNotNormalized',
34-
UrlPrococolNotNormalizable = 'UriPrococolNotNormalizable'
35+
UrlProtocolNotNormalizable = 'UrlProtocolNotNormalizable'
3536
};

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { EncryptionAlgorithm } from '../../../utils/encryption.js';
1818
import { GeneralJwsSigner } from '../../../jose/jws/general/signer.js';
1919
import { getCurrentTimeInHighPrecision } from '../../../utils/time.js';
2020
import { Jws } from '../../../utils/jws.js';
21-
import { KeyDerivationScheme } from '../../../utils/hd-key.js';
2221
import { Message } from '../../../core/message.js';
2322
import { ProtocolAuthorization } from '../../../core/protocol-authorization.js';
2423
import { Records } from '../../../utils/records.js';
@@ -204,7 +203,7 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
204203
// `attestation` generation
205204
const descriptorCid = await computeCid(descriptor);
206205
const attestation = await RecordsWrite.createAttestation(descriptorCid, options.attestationSignatureInputs);
207-
const encryption = await RecordsWrite.createEncryptionProperty(options.encryptionInput, descriptor, contextId);
206+
const encryption = await RecordsWrite.createEncryptionProperty(recordId, contextId, descriptor, options.encryptionInput);
208207

209208
// `authorization` generation
210209
const authorization = await RecordsWrite.createAuthorization(
@@ -456,9 +455,10 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
456455
* Creates the `encryption` property if encryption input is given. Else `undefined` is returned.
457456
*/
458457
private static async createEncryptionProperty(
459-
encryptionInput: EncryptionInput | undefined,
458+
recordId: string,
459+
contextId: string | undefined,
460460
descriptor: RecordsWriteDescriptor,
461-
contextId: string | undefined // there is opportunity here to streamline the arguments, e.g. `contextId` feels very specialized
461+
encryptionInput: EncryptionInput | undefined
462462
): Promise<EncryptionProperty | undefined> {
463463
if (encryptionInput === undefined) {
464464
return undefined;
@@ -467,13 +467,12 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
467467
// encrypt the data encryption key once per key derivation scheme
468468
const keyEncryption: EncryptedKey[] = [];
469469
for (const keyEncryptionInput of encryptionInput.keyEncryptionInputs) {
470-
// NOTE: right now only `protocol-context` scheme is supported so we will assume that's the scheme without additional switch/if statements
471-
// derive the leaf public key
472-
const leafDerivationPath = [KeyDerivationScheme.ProtocolContext, descriptor.protocol!, contextId!];
470+
471+
const fullDerivationPath = Records.constructKeyDerivationPath(keyEncryptionInput.publicKey.derivationScheme, recordId, contextId, descriptor);
473472

474473
// NOTE: right now only `ECIES-ES256K` algorithm is supported for asymmetric encryption,
475474
// so we will assume that's the algorithm without additional switch/if statements
476-
const leafPublicKey = await Records.deriveLeafPublicKey(keyEncryptionInput.publicKey, leafDerivationPath);
475+
const leafPublicKey = await Records.deriveLeafPublicKey(keyEncryptionInput.publicKey, fullDerivationPath);
477476
const keyEncryptionOutput = await Encryption.eciesSecp256k1Encrypt(leafPublicKey, encryptionInput.key);
478477

479478
const encryptedKey = Encoder.bytesToBase64Url(keyEncryptionOutput.ciphertext);

src/utils/encryption.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as eccrypto from 'eccrypto';
33
import { Readable } from 'readable-stream';
44

55
/**
6-
* Utility class for performing common encryption operations.
6+
* Utility class for performing common, non-DWN specific encryption operations.
77
*/
88
export class Encryption {
99
/**

src/utils/hd-key.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { PrivateJwk, PublicJwk } from '../jose/types.js';
33
import { Secp256k1 } from './secp256k1.js';
44

55
export enum KeyDerivationScheme {
6-
ProtocolContext = 'protocol-context'
6+
Protocols = 'protocols'
77
}
88

99
export type DerivedPublicJwk = {

src/utils/records.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Readable } from 'readable-stream';
2-
import type { UnsignedRecordsWriteMessage } from '../interfaces/records/types.js';
32
import type { DerivedPrivateJwk, DerivedPublicJwk } from './hd-key.js';
3+
import type { RecordsWriteDescriptor, UnsignedRecordsWriteMessage } from '../interfaces/records/types.js';
44

55
import { Encoder } from './encoder.js';
66
import { Encryption } from './encryption.js';
@@ -20,7 +20,7 @@ export class Records {
2020
ancestorPrivateKey: DerivedPrivateJwk,
2121
cipherStream: Readable
2222
): Promise<Readable> {
23-
const { encryption, contextId, descriptor } = recordsWrite;
23+
const { recordId, contextId, descriptor, encryption } = recordsWrite;
2424

2525
// look for an encrypted symmetric key that is encrypted using the same scheme as the given derived private key
2626
const matchingEncryptedKey = encryption!.keyEncryption.find(key => key.derivationScheme === ancestorPrivateKey.derivationScheme);
@@ -31,13 +31,11 @@ export class Records {
3131
);
3232
}
3333

34-
// NOTE: right now only `protocol-context` scheme is supported so we will assume that's the scheme without additional switch/if statements
35-
// derive the leaf private key
36-
const leafDerivationPath = [KeyDerivationScheme.ProtocolContext, descriptor.protocol!, contextId!];
34+
const fullDerivationPath = Records.constructKeyDerivationPath(matchingEncryptedKey.derivationScheme, recordId, contextId, descriptor);
3735

3836
// NOTE: right now only `ECIES-ES256K` algorithm is supported for asymmetric encryption,
3937
// so we will assume that's the algorithm without additional switch/if statements
40-
const leafPrivateKey = await Records.deriveLeafPrivateKey(ancestorPrivateKey, leafDerivationPath);
38+
const leafPrivateKey = await Records.deriveLeafPrivateKey(ancestorPrivateKey, fullDerivationPath);
4139
const encryptedKeyBytes = Encoder.base64UrlToBytes(matchingEncryptedKey.encryptedKey);
4240
const ephemeralPublicKey = Secp256k1.publicJwkToBytes(matchingEncryptedKey.ephemeralPublicKey);
4341
const keyEncryptionInitializationVector = Encoder.base64UrlToBytes(matchingEncryptedKey.initializationVector);
@@ -58,6 +56,51 @@ export class Records {
5856
return plaintextStream;
5957
}
6058

59+
/**
60+
* Constructs full key derivation path using the specified scheme.
61+
*/
62+
public static constructKeyDerivationPath(
63+
_keyDerivationScheme: KeyDerivationScheme,
64+
recordId: string,
65+
contextId: string | undefined,
66+
descriptor: RecordsWriteDescriptor
67+
): string[] {
68+
69+
// NOTE: right now only `protocols` derivation scheme is supported so we will assume that's the scheme without additional switch/if statements
70+
const fullDerivationPath = Records.constructKeyDerivationPathUsingProtocolsScheme(recordId, contextId, descriptor);
71+
return fullDerivationPath;
72+
}
73+
74+
/**
75+
* Constructs the full key derivation path using `protocols` scheme.
76+
*/
77+
private static constructKeyDerivationPathUsingProtocolsScheme(
78+
recordId: string,
79+
contextId: string | undefined,
80+
descriptor: RecordsWriteDescriptor
81+
): string[] {
82+
// ensure `protocol` is defined
83+
// NOTE: no need to check `protocolPath` and `contextId` because earlier code ensures that if `protocol` is defined, those are defined also
84+
if (descriptor.protocol === undefined) {
85+
throw new DwnError(
86+
DwnErrorCode.RecordsProtocolsDerivationSchemeMissingProtocol,
87+
'Unable to construct key derivation path using `protocols` scheme because `protocol` is missing.'
88+
);
89+
}
90+
91+
const protocolPathSegments = descriptor.protocolPath!.split('/');
92+
const fullDerivationPath = [
93+
KeyDerivationScheme.Protocols,
94+
descriptor.protocol,
95+
contextId!,
96+
...protocolPathSegments,
97+
descriptor.dataFormat,
98+
recordId
99+
];
100+
101+
return fullDerivationPath;
102+
}
103+
61104
/**
62105
* Derives a descendant public key given an ancestor public key.
63106
* NOTE: right now only `ECIES-ES256K` algorithm is supported for asymmetric encryption,

src/utils/url.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function normalizeProtocolUri(url: string): string {
2828
result.hash = '';
2929
return removeTrailingSlash(result.href);
3030
} catch (e) {
31-
throw new DwnError(DwnErrorCode.UrlPrococolNotNormalizable, 'Could not normalize protocol URI');
31+
throw new DwnError(DwnErrorCode.UrlProtocolNotNormalizable, 'Could not normalize protocol URI');
3232
}
3333
}
3434

tests/interfaces/records/handlers/records-query.spec.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ describe('RecordsQueryHandler.handle()', () => {
652652
key : dataEncryptionKey,
653653
keyEncryptionInputs : [{
654654
publicKey: {
655-
derivationScheme : KeyDerivationScheme.ProtocolContext,
655+
derivationScheme : KeyDerivationScheme.Protocols,
656656
derivationPath : [],
657657
derivedPublicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
658658
}
@@ -685,11 +685,16 @@ describe('RecordsQueryHandler.handle()', () => {
685685

686686
// test able to decrypt the message using a derived key
687687
const rootPrivateKey: DerivedPrivateJwk = {
688-
derivationScheme : KeyDerivationScheme.ProtocolContext,
688+
derivationScheme : KeyDerivationScheme.Protocols,
689689
derivationPath : [],
690690
derivedPrivateKey : alice.keyPair.privateJwk
691691
};
692-
const relativeDescendantDerivationPath = [KeyDerivationScheme.ProtocolContext, protocol, message.contextId!];
692+
const relativeDescendantDerivationPath = Records.constructKeyDerivationPath(
693+
KeyDerivationScheme.Protocols,
694+
message.recordId,
695+
message.contextId,
696+
message.descriptor
697+
);
693698
const descendantPrivateKey: DerivedPrivateJwk = await HdKey.derivePrivateKey(rootPrivateKey, relativeDescendantDerivationPath);
694699

695700
const cipherStream = DataStream.fromBytes(Encoder.base64UrlToBytes(unsignedRecordsWrite.encodedData!));

tests/interfaces/records/handlers/records-read.spec.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { DerivedPrivateJwk } from '../../../../src/utils/hd-key.js';
2-
import emailProtocolDefinition from '../../../vectors/protocol-definitions/email.json' assert { type: 'json' };
32
import type { EncryptionInput } from '../../../../src/interfaces/records/messages/records-write.js';
43
import type { ProtocolDefinition } from '../../../../src/index.js';
5-
import socialMediaProtocolDefinition from '../../../vectors/protocol-definitions/social-media.json' assert { type: 'json' };
64

75
import chaiAsPromised from 'chai-as-promised';
6+
import emailProtocolDefinition from '../../../vectors/protocol-definitions/email.json' assert { type: 'json' };
87
import sinon from 'sinon';
8+
import socialMediaProtocolDefinition from '../../../vectors/protocol-definitions/social-media.json' assert { type: 'json' };
99
import chai, { expect } from 'chai';
1010

1111
import { Comparer } from '../../../utils/comparer.js';
@@ -401,7 +401,7 @@ describe('RecordsReadHandler.handle()', () => {
401401
key : dataEncryptionKey,
402402
keyEncryptionInputs : [{
403403
publicKey: {
404-
derivationScheme : KeyDerivationScheme.ProtocolContext,
404+
derivationScheme : KeyDerivationScheme.Protocols,
405405
derivationPath : [],
406406
derivedPublicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
407407
}
@@ -431,11 +431,16 @@ describe('RecordsReadHandler.handle()', () => {
431431

432432
// test able to decrypt the message using a derived key
433433
const rootPrivateKey: DerivedPrivateJwk = {
434-
derivationScheme : KeyDerivationScheme.ProtocolContext,
434+
derivationScheme : KeyDerivationScheme.Protocols,
435435
derivationPath : [],
436436
derivedPrivateKey : alice.keyPair.privateJwk
437437
};
438-
const relativeDescendantDerivationPath = [KeyDerivationScheme.ProtocolContext, protocol, message.contextId!];
438+
const relativeDescendantDerivationPath = Records.constructKeyDerivationPath(
439+
KeyDerivationScheme.Protocols,
440+
message.recordId,
441+
message.contextId,
442+
message.descriptor
443+
);
439444
const descendantPrivateKey: DerivedPrivateJwk = await HdKey.derivePrivateKey(rootPrivateKey, relativeDescendantDerivationPath);
440445

441446
const unsignedRecordsWrite = readReply.record!;
@@ -446,7 +451,7 @@ describe('RecordsReadHandler.handle()', () => {
446451
expect(Comparer.byteArraysEqual(plaintextBytes, bobMessageBytes)).to.be.true;
447452

448453
// test unable to decrypt the message if derived key has an unexpected path
449-
const invalidDerivationPath = [KeyDerivationScheme.ProtocolContext, protocol, 'invalidContextId'];
454+
const invalidDerivationPath = [KeyDerivationScheme.Protocols, protocol, 'invalidContextId'];
450455
const inValidDescendantPrivateKey: DerivedPrivateJwk = await HdKey.derivePrivateKey(rootPrivateKey, invalidDerivationPath);
451456
await expect(Records.decrypt(unsignedRecordsWrite, inValidDescendantPrivateKey, cipherStream)).to.be.rejectedWith(
452457
DwnErrorCode.RecordsInvalidAncestorKeyDerivationSegment

0 commit comments

Comments
 (0)