Skip to content

Commit

Permalink
Merge branch 'main' into c82codecov
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai authored Oct 20, 2023
2 parents 65b6b63 + eb96cbc commit e29c5ae
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 28 deletions.
39 changes: 26 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"ajv": "8.12.0",
"blockstore-core": "4.2.0",
"cross-fetch": "4.0.0",
"eciesjs": "0.4.0",
"eciesjs": "0.4.5",
"flat": "5.0.2",
"interface-blockstore": "5.2.3",
"interface-store": "5.1.2",
Expand Down
31 changes: 25 additions & 6 deletions src/utils/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import * as crypto from 'crypto';
import * as eciesjs from 'eciesjs';
import { Readable } from 'readable-stream';

// compress publicKey for message encryption
eciesjs.ECIES_CONFIG.isEphemeralKeyCompressed = true;

/**
* Utility class for performing common, non-DWN specific encryption operations.
*/
Expand Down Expand Up @@ -67,18 +70,27 @@ export class Encryption {
* with SECP256K1 for the asymmetric calculations, HKDF as the key-derivation function,
* and AES-GCM for the symmetric encryption and MAC algorithms.
*/
public static async eciesSecp256k1Encrypt(uncompressedPublicKey: Uint8Array, plaintext: Uint8Array): Promise<EciesEncryptionOutput> {
public static async eciesSecp256k1Encrypt(publicKeyBytes: Uint8Array, plaintext: Uint8Array): Promise<EciesEncryptionOutput> {
// underlying library requires Buffer as input
const publicKey = Buffer.from(uncompressedPublicKey);
const publicKey = Buffer.from(publicKeyBytes);
const plaintextBuffer = Buffer.from(plaintext);

const cryptogram = eciesjs.encrypt(publicKey, plaintextBuffer);

// split cryptogram returned into constituent parts
const ephemeralPublicKey = cryptogram.subarray(0, 65);
const initializationVector = cryptogram.subarray(65, 81);
const messageAuthenticationCode = cryptogram.subarray(81, 97);
const ciphertext = cryptogram.subarray(97);
let start = 0;
let end = Encryption.isEphemeralKeyCompressed ? 33 : 65;
const ephemeralPublicKey = cryptogram.subarray(start, end);

start = end;
end += eciesjs.ECIES_CONFIG.symmetricNonceLength;
const initializationVector = cryptogram.subarray(start, end);

start = end;
end += 16; // eciesjs.consts.AEAD_TAG_LENGTH
const messageAuthenticationCode = cryptogram.subarray(start, end);

const ciphertext = cryptogram.subarray(end);

return {
ciphertext,
Expand Down Expand Up @@ -107,6 +119,13 @@ export class Encryption {

return plaintext;
}

/**
* Expose eciesjs library configuration
*/
static get isEphemeralKeyCompressed():boolean {
return eciesjs.ECIES_CONFIG.isEphemeralKeyCompressed;
}
}

export type EciesEncryptionOutput = {
Expand Down
17 changes: 9 additions & 8 deletions src/utils/secp256k1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,16 @@ export class Secp256k1 {
}

/**
* Creates a uncompressed key in raw bytes from the given SECP256K1 JWK.
* Creates a compressed key in raw bytes from the given SECP256K1 JWK.
*/
public static publicJwkToBytes(publicJwk: PublicJwk): Uint8Array {
const x = Encoder.base64UrlToBytes(publicJwk.x);
const y = Encoder.base64UrlToBytes(publicJwk.y!);

// leading byte of 0x04 indicates that the public key is uncompressed
const publicKey = new Uint8Array([0x04, ...x, ...y]);
return publicKey;
return secp256k1.ProjectivePoint.fromAffine({
x : secp256k1.etc.bytesToNumberBE(x),
y : secp256k1.etc.bytesToNumberBE(y)
}).toRawBytes(true);
}

/**
Expand Down Expand Up @@ -129,20 +130,20 @@ export class Secp256k1 {
}

/**
* Generates key pair in raw bytes, where the `publicKey` is uncompressed.
* Generates key pair in raw bytes, where the `publicKey` is compressed.
*/
public static async generateKeyPairRaw(): Promise<{publicKey: Uint8Array, privateKey: Uint8Array}> {
const privateKey = secp256k1.utils.randomPrivateKey();
const publicKey = secp256k1.getPublicKey(privateKey, false); // `false` = uncompressed
const publicKey = secp256k1.getPublicKey(privateKey, true); // `true` = compressed

return { publicKey, privateKey };
}

/**
* Gets the uncompressed public key of the given private key.
* Gets the compressed public key of the given private key.
*/
public static async getPublicKey(privateKey: Uint8Array): Promise<Uint8Array> {
const publicKey = secp256k1.getPublicKey(privateKey, false); // `false` = uncompressed
const publicKey = secp256k1.getPublicKey(privateKey, true); // `true` = compressed
return publicKey;
}

Expand Down
16 changes: 16 additions & 0 deletions tests/utils/encryption.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Encryption } from '../../src/utils/encryption.js';
import { expect } from 'chai';
import { Readable } from 'readable-stream';
import { Secp256k1 } from '../../src/utils/secp256k1.js';
import { etc as Secp256k1Etc } from '@noble/secp256k1';
import { TestDataGenerator } from './test-data-generator.js';

describe('Encryption', () => {
Expand Down Expand Up @@ -117,6 +118,21 @@ describe('Encryption', () => {

expect(ArrayUtility.byteArraysEqual(originalPlaintext, decryptedPlaintext)).to.be.true;
});

it('should be able to accept both compressed and uncompressed publicKeys', async () => {
const originalPlaintext = TestDataGenerator.randomBytes(32);
const h2b = Secp256k1Etc.hexToBytes;
// Following test vector was taken from @noble/secp256k1 test file.
// noble-secp256k1/main/test/vectors/secp256k1/privates.json
const privateKey = h2b('9c7fc36bc106fd7df5e1078d03e34b9a045892abdd053ec69bfeb22327529f6c');
const compressed = h2b('03936cb2bd56e681d360bbce6a3a7a1ccbf72f3ab8792edbc45fb08f55b929c588');
const uncompressed = h2b('04936cb2bd56e681d360bbce6a3a7a1ccbf72f3ab8792edbc45fb08f55b929c588529b8cee53f7eff1da5fc0e6050d952b37d4de5c3b85e952dfe9d9e9b2b3b6eb');
for (const publicKey of [compressed, uncompressed]) {
const encrypted = await Encryption.eciesSecp256k1Encrypt(publicKey, originalPlaintext);
const decrypted = await Encryption.eciesSecp256k1Decrypt({ privateKey, ...encrypted });
expect(ArrayUtility.byteArraysEqual(originalPlaintext, decrypted)).to.be.true;
}
});
});
});

Expand Down
7 changes: 7 additions & 0 deletions tests/utils/secp256k1.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import { Secp256k1 } from '../../src/utils/secp256k1.js';
import { TestDataGenerator } from './test-data-generator.js';

describe('Secp256k1', () => {
describe('generateKeyPairRaw()', () => {
it('should generate compressed publicKey', async () => {
const { publicKey } = await Secp256k1.generateKeyPairRaw();
expect(publicKey.length).to.equal(33);
});
});

describe('validateKey()', () => {
it('should throw if key is not a valid SECP256K1 key', async () => {
const validKey = (await Secp256k1.generateKeyPair()).publicJwk;
Expand Down

0 comments on commit e29c5ae

Please sign in to comment.