Skip to content

Commit

Permalink
Add exportRawSecretKey() and exportRawPublicKey() to key pair API.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Nov 4, 2023
1 parent 4809f6b commit 2e67527
Show file tree
Hide file tree
Showing 4 changed files with 69 additions and 8 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# @digitalbazaar/ecdsa-multikey ChangeLog

## 1.4.0 - 2023-11-dd

### Added
- Add `exportRawSecretKey()` and `exportRawPublicKey()` to key pair interface.

## 1.3.0 - 2023-10-31

### Added
Expand Down
18 changes: 13 additions & 5 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
import {CryptoKey, webcrypto} from './crypto.js';
import {createSigner, createVerifier} from './factory.js';
import {
exportKeyPair, importKeyPair, toPublicKeyMultibase, toSecretKeyMultibase
exportKeyPair, importKeyPair,
toPublicKeyBytes, toSecretKeyBytes,
toPublicKeyMultibase, toSecretKeyMultibase
} from './serialize.js';
import {getSecretKeySize} from './helpers.js';
import {toMultikey} from './translators.js';
Expand All @@ -23,8 +25,7 @@ export async function generate({
if(!curve) {
throw new TypeError(
'"curve" must be one of the following values: ' +
`${Object.values(ECDSA_CURVE).map(v => `'${v}'`).join(', ')}.`
);
`${Object.values(ECDSA_CURVE).map(v => `'${v}'`).join(', ')}.`);
}
const algorithm = keyAgreement ?
{name: 'ECDH', namedCurve: curve} : {name: ALGORITHM, namedCurve: curve};
Expand Down Expand Up @@ -126,6 +127,14 @@ async function _createKeyPairInterface({keyPair, keyAgreement = false}) {
}
return _deriveSecret(
{localKeyPair: this, remoteKeyPair: remotePublicKey});
},
async exportRawPublicKey() {
const jwk = await toJwk({keyPair: this});
return toPublicKeyBytes({jwk});
},
async exportRawSecretKey() {
const jwk = await toJwk({keyPair: this, secretKey: true});
return toSecretKeyBytes({jwk});
}
};

Expand All @@ -143,8 +152,7 @@ function _assertMultikey(key) {
if(key['@context'] !== MULTIKEY_CONTEXT_V1_URL) {
throw new TypeError(
'"key" must be a Multikey with context ' +
`"${MULTIKEY_CONTEXT_V1_URL}".`
);
`"${MULTIKEY_CONTEXT_V1_URL}".`);
}
}

Expand Down
33 changes: 33 additions & 0 deletions lib/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,26 @@ export async function importKeyPair({
return keyPair;
}

export function toPublicKeyBytes({jwk} = {}) {
if(jwk?.kty !== 'EC') {
throw new TypeError('"jwk.kty" must be "EC".');
}
const {crv: curve} = jwk;
const secretKeySize = getSecretKeySize({curve});
// convert `x` coordinate to compressed public key
const x = base64url.decode(jwk.x);
const y = base64url.decode(jwk.y);
// public key size is always secret key size + 1
const publicKeySize = secretKeySize + 1;
const publicKey = new Uint8Array(publicKeySize);
// use even / odd status of `y` coordinate for compressed header
const even = y[y.length - 1] % 2 === 0;
publicKey[0] = even ? 2 : 3;
// write `x` coordinate at end of multikey buffer to zero-fill it
publicKey.set(x, publicKey.length - x.length);
return publicKey;
}

export function toPublicKeyMultibase({jwk} = {}) {
if(jwk?.kty !== 'EC') {
throw new TypeError('"jwk.kty" must be "EC".');
Expand All @@ -179,6 +199,19 @@ export function toPublicKeyMultibase({jwk} = {}) {
return publicKeyMultibase;
}

export function toSecretKeyBytes({jwk} = {}) {
if(jwk?.kty !== 'EC') {
throw new TypeError('"jwk.kty" must be "EC".');
}
const {crv: curve} = jwk;
const secretKeySize = getSecretKeySize({curve});
const d = base64url.decode(jwk.d);
const secretKey = new Uint8Array(secretKeySize);
// write `d` at end of multikey buffer to zero-fill it
secretKey.set(d, secretKey.length - d.length);
return secretKey;
}

export function toSecretKeyMultibase({jwk} = {}) {
if(jwk?.kty !== 'EC') {
throw new TypeError('"jwk.kty" must be "EC".');
Expand Down
21 changes: 18 additions & 3 deletions test/EcdsaMultikey.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
* Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved.
*/
import * as base58 from 'base58-universal';
import * as EcdsaMultikey from '../lib/index.js';
import chai from 'chai';
import {CryptoKey, webcrypto} from '../lib/crypto.js';
import {getNamedCurveFromPublicMultikey} from '../lib/helpers.js';
import * as EcdsaMultikey from '../lib/index.js';
import {exportKeyPair} from '../lib/serialize.js';
import {
mockKey,
Expand Down Expand Up @@ -134,8 +134,7 @@ describe('EcdsaMultikey', () => {
it('should only export secret key if available', async () => {
const algorithm = {name: 'ECDSA', namedCurve: 'P-256'};
const keyPair = await webcrypto.subtle.generateKey(
algorithm, true, ['sign', 'verify']
);
algorithm, true, ['sign', 'verify']);
delete keyPair.privateKey;

const keyPairExported = await exportKeyPair({
Expand All @@ -147,6 +146,22 @@ describe('EcdsaMultikey', () => {

expect(keyPairExported).not.to.have.property('secretKeyMultibase');
});

it('should export raw public key', async () => {
const keyPair = await EcdsaMultikey.generate({curve: 'P-256'});
const expectedPublicKey = base58.decode(
keyPair.publicKeyMultibase.slice(1)).slice(2);
const publicKey = await keyPair.exportRawPublicKey();
expect(expectedPublicKey).to.deep.equal(publicKey);
});

it('should export raw secret key', async () => {
const keyPair = await EcdsaMultikey.generate({curve: 'P-256'});
const expectedSecretKey = base58.decode(
keyPair.secretKeyMultibase.slice(1)).slice(2);
const secretKey = await keyPair.exportRawSecretKey();
expect(expectedSecretKey).to.deep.equal(secretKey);
});
});

describe('from', () => {
Expand Down

0 comments on commit 2e67527

Please sign in to comment.