Skip to content

Commit 93a8eaa

Browse files
authored
Quality-of-life improvements for devs using the SDK for encryption
1. When using root private key, derivation path of `[]` is no longer required to be specified 2. Encryption input used to create an encrypted message now only accepts the root public key
1 parent 67dd57a commit 93a8eaa

File tree

10 files changed

+67
-71
lines changed

10 files changed

+67
-71
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.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)
6+
![Statements](https://img.shields.io/badge/statements-94.07%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.6%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

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export { DataStore } from './store/data-store.js';
1616
export { DataStoreLevel } from './store/data-store-level.js';
1717
export { DateSort } from './interfaces/records/messages/records-query.js';
1818
export { DataStream } from './utils/data-stream.js';
19-
export { DerivedPrivateJwk, DerivedPublicJwk, HdKey, KeyDerivationScheme } from './utils/hd-key.js';
19+
export { DerivedPrivateJwk, HdKey, KeyDerivationScheme } from './utils/hd-key.js';
2020
export { DidKeyResolver } from './did/did-key-resolver.js';
2121
export { DidIonResolver } from './did/did-ion-resolver.js';
2222
export { DidResolver, DidMethodResolver } from './did/did-resolver.js';

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { BaseMessage } from '../../../core/types.js';
2-
import type { DerivedPublicJwk } from '../../../utils/hd-key.js';
2+
import type { KeyDerivationScheme } from '../../../index.js';
33
import type { MessageStore } from '../../../store/message-store.js';
4+
import type { PublicJwk } from '../../../jose/types.js';
45
import type {
56
EncryptedKey,
67
EncryptionProperty,
@@ -81,9 +82,14 @@ export type EncryptionInput = {
8182
*/
8283
export type KeyEncryptionInput = {
8384
/**
84-
* Public key used to encrypt the symmetric key.
85+
* Key derivation scheme to derive the descendant public key to encrypt the symmetric key.
8586
*/
86-
publicKey: DerivedPublicJwk;
87+
derivationScheme: KeyDerivationScheme;
88+
89+
/**
90+
* Root public key used derive the descendant public key to encrypt the symmetric key.
91+
*/
92+
publicKey: PublicJwk;
8793

8894
/**
8995
* Algorithm used for encrypting the symmetric key. Uses {EncryptionAlgorithm.EciesSecp256k1} if not given.
@@ -468,7 +474,7 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
468474
const keyEncryption: EncryptedKey[] = [];
469475
for (const keyEncryptionInput of encryptionInput.keyEncryptionInputs) {
470476

471-
const fullDerivationPath = Records.constructKeyDerivationPath(keyEncryptionInput.publicKey.derivationScheme, recordId, contextId, descriptor);
477+
const fullDerivationPath = Records.constructKeyDerivationPath(keyEncryptionInput.derivationScheme, recordId, contextId, descriptor);
472478

473479
// NOTE: right now only `ECIES-ES256K` algorithm is supported for asymmetric encryption,
474480
// so we will assume that's the algorithm without additional switch/if statements
@@ -481,7 +487,7 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
481487
const messageAuthenticationCode = Encoder.bytesToBase64Url(keyEncryptionOutput.messageAuthenticationCode);
482488
const encryptedKeyData: EncryptedKey = {
483489
algorithm : keyEncryptionInput.algorithm ?? EncryptionAlgorithm.EciesSecp256k1,
484-
derivationScheme : keyEncryptionInput.publicKey.derivationScheme,
490+
derivationScheme : keyEncryptionInput.derivationScheme,
485491
encryptedKey,
486492
ephemeralPublicKey,
487493
initializationVector : keyEncryptionInitializationVector,

src/utils/hd-key.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,14 @@
1-
import type { PrivateJwk, PublicJwk } from '../jose/types.js';
1+
import type { PrivateJwk } from '../jose/types.js';
22

33
import { Secp256k1 } from './secp256k1.js';
44

55
export enum KeyDerivationScheme {
66
Protocols = 'protocols'
77
}
88

9-
export type DerivedPublicJwk = {
10-
derivationScheme: KeyDerivationScheme;
11-
derivationPath: string[];
12-
derivedPublicKey: PublicJwk,
13-
};
14-
159
export type DerivedPrivateJwk = {
1610
derivationScheme: KeyDerivationScheme;
17-
derivationPath: string[];
11+
derivationPath?: string[];
1812
derivedPrivateKey: PrivateJwk,
1913
};
2014

@@ -28,10 +22,11 @@ export class HdKey {
2822
*/
2923
public static async derivePrivateKey(ancestorKey: DerivedPrivateJwk, subDerivationPath: string[]): Promise<DerivedPrivateJwk> {
3024
const ancestorPrivateKey = Secp256k1.privateJwkToBytes(ancestorKey.derivedPrivateKey);
25+
const ancestorPrivateKeyDerivationPath = ancestorKey.derivationPath ?? [];
3126
const derivedPrivateKeyBytes = await Secp256k1.derivePrivateKey(ancestorPrivateKey, subDerivationPath);
3227
const derivedPrivateJwk = await Secp256k1.privateKeyToJwk(derivedPrivateKeyBytes);
3328
const derivedDescendantPrivateKey: DerivedPrivateJwk = {
34-
derivationPath : [...ancestorKey.derivationPath, ...subDerivationPath],
29+
derivationPath : [...ancestorPrivateKeyDerivationPath, ...subDerivationPath],
3530
derivationScheme : ancestorKey.derivationScheme,
3631
derivedPrivateKey : derivedPrivateJwk
3732
};

src/utils/records.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import type { DerivedPrivateJwk } from './hd-key.js';
2+
import type { PublicJwk } from '../jose/types.js';
13
import type { Readable } from 'readable-stream';
2-
import type { DerivedPrivateJwk, DerivedPublicJwk } from './hd-key.js';
34
import type { RecordsWriteDescriptor, UnsignedRecordsWriteMessage } from '../interfaces/records/types.js';
45

56
import { Encoder } from './encoder.js';
@@ -14,6 +15,7 @@ import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
1415
export class Records {
1516
/**
1617
* Decrypts the encrypted data in a message reply using the given ancestor private key.
18+
* @param ancestorPrivateKey Any ancestor private key in the key derivation path.
1719
*/
1820
public static async decrypt(
1921
recordsWrite: UnsignedRecordsWriteMessage,
@@ -106,19 +108,16 @@ export class Records {
106108
* NOTE: right now only `ECIES-ES256K` algorithm is supported for asymmetric encryption,
107109
* so we will assume that's the algorithm without additional switch/if statements
108110
*/
109-
public static async deriveLeafPublicKey(ancestorPublicKey: DerivedPublicJwk, fullDescendantDerivationPath: string[]): Promise<Uint8Array> {
110-
if (ancestorPublicKey.derivedPublicKey.crv !== 'secp256k1') {
111+
public static async deriveLeafPublicKey(rootPublicKey: PublicJwk, fullDescendantDerivationPath: string[]): Promise<Uint8Array> {
112+
if (rootPublicKey.crv !== 'secp256k1') {
111113
throw new DwnError(
112114
DwnErrorCode.RecordsDeriveLeafPublicKeyUnSupportedCurve,
113-
`Curve ${ancestorPublicKey.derivedPublicKey.crv} is not supported.`
115+
`Curve ${rootPublicKey.crv} is not supported.`
114116
);
115117
}
116118

117-
Records.validateAncestorKeyAndDescentKeyDerivationPathsMatch(ancestorPublicKey.derivationPath, fullDescendantDerivationPath);
118-
119-
const subDerivationPath = fullDescendantDerivationPath.slice(ancestorPublicKey.derivationPath.length);
120-
const ancestorPublicKeyBytes = Secp256k1.publicJwkToBytes(ancestorPublicKey.derivedPublicKey);
121-
const leafPublicKey = await Secp256k1.derivePublicKey(ancestorPublicKeyBytes, subDerivationPath);
119+
const ancestorPublicKeyBytes = Secp256k1.publicJwkToBytes(rootPublicKey);
120+
const leafPublicKey = await Secp256k1.derivePublicKey(ancestorPublicKeyBytes, fullDescendantDerivationPath);
122121

123122
return leafPublicKey;
124123
}
@@ -136,9 +135,11 @@ export class Records {
136135
);
137136
}
138137

139-
Records.validateAncestorKeyAndDescentKeyDerivationPathsMatch(ancestorPrivateKey.derivationPath, fullDescendantDerivationPath);
138+
const ancestorPrivateKeyDerivationPath = ancestorPrivateKey.derivationPath ?? [];
139+
140+
Records.validateAncestorKeyAndDescentKeyDerivationPathsMatch(ancestorPrivateKeyDerivationPath, fullDescendantDerivationPath);
140141

141-
const subDerivationPath = fullDescendantDerivationPath.slice(ancestorPrivateKey.derivationPath.length);
142+
const subDerivationPath = fullDescendantDerivationPath.slice(ancestorPrivateKeyDerivationPath.length);
142143
const ancestorPrivateKeyBytes = Secp256k1.privateJwkToBytes(ancestorPrivateKey.derivedPrivateKey);
143144
const leafPrivateKey = await Secp256k1.derivePrivateKey(ancestorPrivateKeyBytes, subDerivationPath);
144145

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

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { RecordsQueryReplyEntry } from '../../../../src/interfaces/records/types.js';
22
import type { DerivedPrivateJwk, EncryptionInput, ProtocolDefinition, RecordsWriteMessage } from '../../../../src/index.js';
33

4-
54
import chaiAsPromised from 'chai-as-promised';
65
import emailProtocolDefinition from '../../../vectors/protocol-definitions/email.json' assert { type: 'json' };
76
import sinon from 'sinon';
@@ -618,7 +617,7 @@ describe('RecordsQueryHandler.handle()', () => {
618617
});
619618

620619
describe('encryption scenarios', () => {
621-
it('should only be able to decrypt record with a correct derived private key', async () => {
620+
it('should only be able to decrypt record with a correct derived private key - protocols derivation scheme', async () => {
622621
// scenario, Bob writes into Alice's DWN an encrypted "email", alice is able to decrypt it
623622

624623
// creating Alice and Bob persona and setting up a stub DID resolver
@@ -651,11 +650,8 @@ describe('RecordsQueryHandler.handle()', () => {
651650
initializationVector : dataEncryptionInitializationVector,
652651
key : dataEncryptionKey,
653652
keyEncryptionInputs : [{
654-
publicKey: {
655-
derivationScheme : KeyDerivationScheme.Protocols,
656-
derivationPath : [],
657-
derivedPublicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
658-
}
653+
derivationScheme : KeyDerivationScheme.Protocols,
654+
publicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
659655
}]
660656
};
661657

@@ -683,25 +679,39 @@ describe('RecordsQueryHandler.handle()', () => {
683679

684680
const unsignedRecordsWrite = queryReply.entries![0] as RecordsQueryReplyEntry;
685681

686-
// test able to decrypt the message using a derived key
682+
683+
// test able to decrypt the message using the root key
687684
const rootPrivateKey: DerivedPrivateJwk = {
688685
derivationScheme : KeyDerivationScheme.Protocols,
689-
derivationPath : [],
690686
derivedPrivateKey : alice.keyPair.privateJwk
691687
};
692-
const relativeDescendantDerivationPath = Records.constructKeyDerivationPath(
693-
KeyDerivationScheme.Protocols,
694-
message.recordId,
695-
message.contextId,
696-
message.descriptor
697-
);
698-
const descendantPrivateKey: DerivedPrivateJwk = await HdKey.derivePrivateKey(rootPrivateKey, relativeDescendantDerivationPath);
699-
700688
const cipherStream = DataStream.fromBytes(Encoder.base64UrlToBytes(unsignedRecordsWrite.encodedData!));
701689

702-
const plaintextDataStream = await Records.decrypt(unsignedRecordsWrite, descendantPrivateKey, cipherStream);
690+
const plaintextDataStream = await Records.decrypt(unsignedRecordsWrite, rootPrivateKey, cipherStream);
703691
const plaintextBytes = await DataStream.toBytes(plaintextDataStream);
704692
expect(Comparer.byteArraysEqual(plaintextBytes, bobMessageBytes)).to.be.true;
693+
694+
695+
// test able to decrypt the message using a derived key
696+
const derivationPath = [KeyDerivationScheme.Protocols]; // the first path segment of `protocol` derivation scheme
697+
const derivedPrivateKey: DerivedPrivateJwk = await HdKey.derivePrivateKey(rootPrivateKey, derivationPath);
698+
699+
const cipherStream2 = DataStream.fromBytes(Encoder.base64UrlToBytes(unsignedRecordsWrite.encodedData!));
700+
701+
const plaintextDataStream2 = await Records.decrypt(unsignedRecordsWrite, derivedPrivateKey, cipherStream2);
702+
const plaintextBytes2 = await DataStream.toBytes(plaintextDataStream2);
703+
expect(Comparer.byteArraysEqual(plaintextBytes2, bobMessageBytes)).to.be.true;
704+
705+
706+
// test able to decrypt the message using a key derived from a derived key
707+
const protocolsUriDerivationPathSegment = [message.descriptor.protocol!]; // the 2nd path segment of `protocol` derivation scheme
708+
const derivedPrivateKey2: DerivedPrivateJwk = await HdKey.derivePrivateKey(derivedPrivateKey, protocolsUriDerivationPathSegment);
709+
710+
const cipherStream3 = DataStream.fromBytes(Encoder.base64UrlToBytes(unsignedRecordsWrite.encodedData!));
711+
712+
const plaintextDataStream3 = await Records.decrypt(unsignedRecordsWrite, derivedPrivateKey2, cipherStream3);
713+
const plaintextBytes3 = await DataStream.toBytes(plaintextDataStream3);
714+
expect(Comparer.byteArraysEqual(plaintextBytes3, bobMessageBytes)).to.be.true;
705715
});
706716
});
707717
});

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -400,11 +400,8 @@ describe('RecordsReadHandler.handle()', () => {
400400
initializationVector : dataEncryptionInitializationVector,
401401
key : dataEncryptionKey,
402402
keyEncryptionInputs : [{
403-
publicKey: {
404-
derivationScheme : KeyDerivationScheme.Protocols,
405-
derivationPath : [],
406-
derivedPublicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
407-
}
403+
derivationScheme : KeyDerivationScheme.Protocols,
404+
publicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
408405
}]
409406
};
410407

@@ -432,7 +429,6 @@ describe('RecordsReadHandler.handle()', () => {
432429
// test able to decrypt the message using a derived key
433430
const rootPrivateKey: DerivedPrivateJwk = {
434431
derivationScheme : KeyDerivationScheme.Protocols,
435-
derivationPath : [],
436432
derivedPrivateKey : alice.keyPair.privateJwk
437433
};
438434
const relativeDescendantDerivationPath = Records.constructKeyDerivationPath(
@@ -460,7 +456,6 @@ describe('RecordsReadHandler.handle()', () => {
460456
// test unable to decrypt the message if there no derivation scheme(s) used by the message matches the scheme used by the given private key
461457
const privateKeyWithMismatchingDerivationScheme: DerivedPrivateJwk = {
462458
derivationScheme : 'scheme-that-is-not-protocol-context' as any,
463-
derivationPath : [],
464459
derivedPrivateKey : alice.keyPair.privateJwk
465460
};
466461
await expect(Records.decrypt(unsignedRecordsWrite, privateKeyWithMismatchingDerivationScheme, cipherStream)).to.be.rejectedWith(

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,12 +1437,9 @@ describe('RecordsWriteHandler.handle()', () => {
14371437
initializationVector : dataEncryptionInitializationVector,
14381438
key : dataEncryptionKey,
14391439
keyEncryptionInputs : [{
1440-
algorithm : EncryptionAlgorithm.EciesSecp256k1,
1441-
publicKey : {
1442-
derivationScheme : KeyDerivationScheme.Protocols,
1443-
derivationPath : [],
1444-
derivedPublicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
1445-
}
1440+
algorithm : EncryptionAlgorithm.EciesSecp256k1,
1441+
derivationScheme : KeyDerivationScheme.Protocols,
1442+
publicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
14461443
}]
14471444
};
14481445
const { message, dataStream } = await TestDataGenerator.generateRecordsWrite({

tests/interfaces/records/messages/records-write.spec.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,11 +186,8 @@ describe('RecordsWrite', () => {
186186
initializationVector : dataEncryptionInitializationVector,
187187
key : dataEncryptionKey,
188188
keyEncryptionInputs : [{
189-
publicKey: {
190-
derivationScheme : KeyDerivationScheme.Protocols,
191-
derivationPath : [],
192-
derivedPublicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
193-
}
189+
derivationScheme : KeyDerivationScheme.Protocols,
190+
publicKey : alice.keyPair.publicJwk // reusing signing key for encryption purely as a convenience
194191
}]
195192
};
196193

tests/utils/records.spec.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DerivedPrivateJwk, DerivedPublicJwk } from '../../src/index.js';
1+
import type { DerivedPrivateJwk } from '../../src/index.js';
22

33
import { DwnErrorCode } from '../../src/core/dwn-error.js';
44
import { ed25519 } from '../../src/jose/algorithms/signing/ed25519.js';
@@ -8,19 +8,14 @@ import { KeyDerivationScheme, Records } from '../../src/index.js';
88
describe('Records', () => {
99
describe('deriveLeafPublicKey()', () => {
1010
it('should throw if given public key is not supported', async () => {
11-
const derivedKey: DerivedPublicJwk = {
12-
derivationPath : [],
13-
derivationScheme : KeyDerivationScheme.Protocols,
14-
derivedPublicKey : (await ed25519.generateKeyPair()).publicJwk
15-
};
16-
await expect(Records.deriveLeafPublicKey(derivedKey, ['a'])).to.be.rejectedWith(DwnErrorCode.RecordsDeriveLeafPublicKeyUnSupportedCurve);
11+
const rootPublicKey = (await ed25519.generateKeyPair()).publicJwk;
12+
await expect(Records.deriveLeafPublicKey(rootPublicKey, ['a'])).to.be.rejectedWith(DwnErrorCode.RecordsDeriveLeafPublicKeyUnSupportedCurve);
1713
});
1814
});
1915

2016
describe('deriveLeafPrivateKey()', () => {
2117
it('should throw if given private key is not supported', async () => {
2218
const derivedKey: DerivedPrivateJwk = {
23-
derivationPath : [],
2419
derivationScheme : KeyDerivationScheme.Protocols,
2520
derivedPrivateKey : (await ed25519.generateKeyPair()).privateJwk
2621
};

0 commit comments

Comments
 (0)