@digitalcredentials/x25519-key-agreement-key-2020
Version:
An X25519 (Curve25519) DH (Diffie-Hellman) key implementation to work with the X25519 2020 Crypto suite.
448 lines (400 loc) • 15.4 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var base58btc = require('@digitalcredentials/base58-universal');
var cryptoLd = require('crypto-ld');
var ed25519 = require('@noble/ed25519');
var crypto = require('./crypto.js');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () {
return e[k];
}
});
}
});
}
n['default'] = e;
return Object.freeze(n);
}
var base58btc__namespace = /*#__PURE__*/_interopNamespace(base58btc);
/*!
* Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved.
*/
const SUITE_ID = 'X25519KeyAgreementKey2020';
// multibase base58-btc header
const MULTIBASE_BASE58BTC_HEADER = 'z';
// multicodec ed25519-pub header as varint
const MULTICODEC_ED25519_PUB_HEADER = new Uint8Array([0xed, 0x01]);
// multicodec ed25519-priv header as varint
const MULTICODEC_ED25519_PRIV_HEADER = new Uint8Array([0x80, 0x26]);
// multicodec x25519-pub header as varint
const MULTICODEC_X25519_PUB_HEADER = new Uint8Array([0xec, 0x01]);
// multicodec x25519-priv header as varint
const MULTICODEC_X25519_PRIV_HEADER = new Uint8Array([0x82, 0x26]);
class X25519KeyAgreementKey2020 extends cryptoLd.LDKeyPair {
/**
* @param {object} options - Options hashmap.
* @param {string} options.controller - Controller DID or document url.
* @param {string} [options.id] - Key ID, typically composed of controller
* URL and key fingerprint as hash fragment.
* @param {string} options.publicKeyMultibase - Multibase encoded public key.
* @param {string} [options.privateKeyMultibase] - Multibase private key.
* @param {string} [options.revoked] - Timestamp of when the key has been
* revoked, in RFC3339 format. If not present, the key itself is considered
* not revoked. Note that this mechanism is slightly different than DID
* Document key revocation, where a DID controller can revoke a key from
* that DID by removing it from the DID Document.
*/
constructor(options = {}) {
super(options);
this.type = SUITE_ID;
const {publicKeyMultibase, privateKeyMultibase} = options;
if(!publicKeyMultibase) {
throw new TypeError('The "publicKeyMultibase" property is required.');
}
if(!publicKeyMultibase || !_isValidKeyHeader(
publicKeyMultibase, MULTICODEC_X25519_PUB_HEADER)) {
throw new Error(
'"publicKeyMultibase" has invalid header bytes: ' +
`"${publicKeyMultibase}".`);
}
if(privateKeyMultibase && !_isValidKeyHeader(
privateKeyMultibase, MULTICODEC_X25519_PRIV_HEADER)) {
throw new Error('"privateKeyMultibase" has invalid header bytes.');
}
// assign valid key values
this.publicKeyMultibase = publicKeyMultibase;
this.privateKeyMultibase = privateKeyMultibase;
if(this.controller && !this.id) {
this.id = `${this.controller}#${this.fingerprint()}`;
}
}
/**
* Generates a new public/private X25519 Key Pair.
*
* @param {object} [options={}] - Keypair options (see controller docstring).
*
* @returns {Promise<X25519KeyAgreementKey2020>} Generated key pair.
*/
static async generate(options = {}) {
const {publicKey, privateKey} = await crypto.generateKeyPair();
return new X25519KeyAgreementKey2020({
publicKeyMultibase:
_multibaseEncode(MULTICODEC_X25519_PUB_HEADER, publicKey),
privateKeyMultibase:
_multibaseEncode(MULTICODEC_X25519_PRIV_HEADER, privateKey),
...options
});
}
/**
* Creates an X25519KeyAgreementKey2020 Key Pair from an existing key
* (constructor method).
*
* @param {object} [options={}] - Keypair options (see controller docstring).
*
* @returns {X25519KeyAgreementKey2020} An X25519 Key Pair.
*/
static async from(options = {}) {
// Check to see if this is an X25519KeyAgreementKey2019
if(options.publicKeyBase58) {
// Convert it to a 2020 key pair instance
return this.fromX25519KeyAgreementKey2019(options);
}
return new X25519KeyAgreementKey2020(options);
}
/**
* Creates an X25519KeyAgreementKey2020 Key Pair from an existing 2019 key
* (backwards compatibility method).
*
* @param {object} [options={}] - Options hashmap.
* @param {string} options.publicKeyBase58 - Base58btc encoded public key.
* @param {string} [options.privateKeyBase58] - Base58btc encoded private key.
* @param {object} [options.keyPairOptions] - Other options.
*
* @returns {Promise<X25519KeyAgreementKey2020>} 2020 Crypto suite key pair.
*/
static async fromX25519KeyAgreementKey2019({
publicKeyBase58, privateKeyBase58, ...keyPairOptions
} = {}) {
let publicKeyMultibase;
let privateKeyMultibase;
if(publicKeyBase58) {
// prefix with `z` to indicate multi-base base58btc encoding
publicKeyMultibase = _multibaseEncode(
MULTICODEC_X25519_PUB_HEADER, base58btc__namespace.decode(publicKeyBase58));
}
if(privateKeyBase58) {
// prefix with `z` to indicate multi-base base58btc encoding
privateKeyMultibase = _multibaseEncode(
MULTICODEC_X25519_PRIV_HEADER, base58btc__namespace.decode(privateKeyBase58));
}
return new X25519KeyAgreementKey2020({
publicKeyMultibase, privateKeyMultibase, ...keyPairOptions
});
}
/**
* Converts a keypair instance of type Ed25519VerificationKey2020 to an
* instance of this class.
*
* @see https://github.com/digitalbazaar/ed25519-verification-key-2020
*
* @param {object} [options={}] - Options hashmap.
*
* @typedef {object} Ed25519VerificationKey2020
* @param {Ed25519VerificationKey2020} options.keyPair - Source key pair.
*
* @returns {X25519KeyAgreementKey2020} A derived/converted key agreement
* key pair.
*/
static fromEd25519VerificationKey2020({keyPair} = {}) {
if(!keyPair.publicKeyMultibase) {
throw new Error('Source public key is required to convert.');
}
if(!keyPair.publicKeyMultibase.startsWith(MULTIBASE_BASE58BTC_HEADER)) {
throw new TypeError(
'Expecting "publicKeyMultibase" value to be multibase base58btc ' +
'encoded (must start with "z").'
);
}
const xKey = new X25519KeyAgreementKey2020({
controller: keyPair.controller,
publicKeyMultibase: X25519KeyAgreementKey2020
.convertFromEdPublicKey(keyPair)
});
if(keyPair.privateKeyMultibase) {
if(!keyPair.privateKeyMultibase.startsWith(MULTIBASE_BASE58BTC_HEADER)) {
throw new TypeError(
'Expecting "privateKeyMultibase" value to be multibase base58btc ' +
'encoded (must start with "z").'
);
}
xKey.privateKeyMultibase = X25519KeyAgreementKey2020
.convertFromEdPrivateKey(keyPair);
}
return xKey;
}
/**
* @param {object} [options={}] - Options hashmap.
* @param {string} options.publicKeyMultibase - Multibase encoded Ed25519
* public key.
*
* @returns {string} Multibase encoded converted X25519 Public key.
*/
static convertFromEdPublicKey({publicKeyMultibase} = {}) {
if(!publicKeyMultibase) {
throw new Error('Source public key is required to convert.');
}
const edPubkeyBytes =
_multibaseDecode(MULTICODEC_ED25519_PUB_HEADER, publicKeyMultibase);
// Converts a 32-byte Ed25519 public key into a 32-byte Curve25519 key
// Returns null if the given public key in not a valid Ed25519 public key.
const dhPubkeyBytes = ed25519.Point.fromHex(edPubkeyBytes).toX25519();
if(!dhPubkeyBytes) {
throw new Error(
'Error converting to X25519; Invalid Ed25519 public key.');
}
return _multibaseEncode(MULTICODEC_X25519_PUB_HEADER, dhPubkeyBytes);
}
/**
* @param {object} [options={}] - Options hashmap.
* @param {string} options.privateKeyMultibase - Multibase encoded Ed25519
* private key.
*
* @returns {string} Multibase encoded converted X25519 Private key.
*/
static convertFromEdPrivateKey({privateKeyMultibase} = {}) {
if(!privateKeyMultibase) {
throw new Error('Source private key is required to convert.');
}
const edPrivkeyBytes =
_multibaseDecode(MULTICODEC_ED25519_PRIV_HEADER, privateKeyMultibase);
// Converts a 64-byte Ed25519 secret key (or just the first 32-byte part of
// it, which is the secret value) into a 32-byte Curve25519 secret key
const dhPrivkeyBytes = crypto.ed25519SecretKeyToX25519(edPrivkeyBytes);
// note: a future version should make this method async to allow use of
// noble to convert private keys -- but the tweetnacl version used
// internally is much faster (~ x100):
// const {head: dhPrivkeyBytes} = await utils.getExtendedPublicKey(
// edPrivkeyBytes.slice(0, 32));
if(!dhPrivkeyBytes) {
throw new Error(
'Error converting to X25519; Invalid Ed25519 private key.');
}
return _multibaseEncode(MULTICODEC_X25519_PRIV_HEADER, dhPrivkeyBytes);
}
/**
* Exports the serialized representation of the KeyPair.
*
* @param {object} [options={}] - Options hashmap.
* @param {boolean} [options.publicKey] - Export public key material?
* @param {boolean} [options.privateKey] - Export private key material?
* @param {boolean} [options.includeContext] - Include JSON-LD context?
*
* @returns {object} A plain js object that's ready for serialization
* (to JSON, etc), for use in DIDs etc.
*/
export({publicKey = false, privateKey = false, includeContext = false} = {}) {
if(!(publicKey || privateKey)) {
throw new TypeError(
'Export requires specifying either "publicKey" or "privateKey".');
}
const exportedKey = {
id: this.id,
type: this.type
};
if(includeContext) {
exportedKey['@context'] = X25519KeyAgreementKey2020.SUITE_CONTEXT;
}
if(this.controller) {
exportedKey.controller = this.controller;
}
if(publicKey) {
exportedKey.publicKeyMultibase = this.publicKeyMultibase;
}
if(privateKey) {
exportedKey.privateKeyMultibase = this.privateKeyMultibase;
}
if(this.revoked) {
exportedKey.revoked = this.revoked;
}
return exportedKey;
}
/**
* Generates and returns a base58btc multibase encoded value of a multicodec
* X25519 public key fingerprint (for use with cryptonyms, for example).
*
* @see https://github.com/multiformats/multicodec
* @see https://github.com/multiformats/multibase
*
* @param {object} [options={}] - Options hashmap.
* @param {string} options.publicKeyMultibase - Multibase encoded public key.
*
* @returns {string} The fingerprint.
*/
static fingerprintFromPublicKey({publicKeyMultibase} = {}) {
if(!publicKeyMultibase) {
throw new Error('Source public key is required.');
}
return publicKeyMultibase;
}
/**
* Creates an instance of X25519KeyAgreementKey2020 from a key fingerprint.
*
* @param {object} [options={}] - Options hashmap.
* @param {string} options.fingerprint - Public key fingerprint.
*
* @returns {X25519KeyAgreementKey2020} Key pair instance (public key material
* only) created from the fingerprint.
*/
static fromFingerprint({fingerprint} = {}) {
return new X25519KeyAgreementKey2020({
publicKeyMultibase: fingerprint
});
}
/**
* Derives a shared secret via a given public key, typically for use
* as one parameter for computing a shared key. It should not be used as
* a shared key itself, but rather as an input into a key derivation function
* (KDF) to produce a shared key.
*
* @param {object} [options={}] - Options hashmap.
* @param {LDKeyPair} options.publicKey - Remote key pair.
*
* @returns {Promise<Uint8Array>} Derived secret.
*/
async deriveSecret({publicKey}) {
const remotePublicKey = _multibaseDecode(
MULTICODEC_X25519_PUB_HEADER, publicKey.publicKeyMultibase);
const privateKey = _multibaseDecode(
MULTICODEC_X25519_PRIV_HEADER, this.privateKeyMultibase);
return crypto.deriveSecret({privateKey, remotePublicKey});
}
/**
* Generates and returns a multiformats encoded
* X25519 public key fingerprint (for use with cryptonyms, for example).
*
* @see https://github.com/multiformats/multicodec
*
* @returns {string} The fingerprint.
*/
fingerprint() {
return this.publicKeyMultibase;
}
/**
* Tests whether the fingerprint was generated from a given key pair.
*
* @example
* xKeyPair.verifyFingerprint('...');
* // {valid: true};
*
* @param {object} [options={}] - Options hashmap.
* @param {string} options.fingerprint - An x25519 key fingerprint (typically
* from a key id).
*
* @returns {object} An object indicating valid is true or false.
*/
verifyFingerprint({fingerprint} = {}) {
// fingerprint should have `z` prefix indicating
// that it's base58btc multibase encoded
if(!_isValidKeyHeader(fingerprint, MULTICODEC_X25519_PUB_HEADER)) {
throw new Error(
`"fingerprint" has invalid header bytes: "${fingerprint}".`);
}
return {valid: true};
}
}
// Used by CryptoLD harness for dispatching.
X25519KeyAgreementKey2020.suite = SUITE_ID;
// Used by CryptoLD harness's fromKeyId() method.
X25519KeyAgreementKey2020.SUITE_CONTEXT =
'https://w3id.org/security/suites/x25519-2020/v1';
/**
* Checks to see if the given value is a valid multibase encoded key.
*
* @param {Uint8Array} multibaseKey - The multibase-encoded key value.
* @param {Uint8Array} expectedHeader - The expected header for the key value.
* @returns {boolean} Returns true if the header is valid, false otherwise.
*/
function _isValidKeyHeader(multibaseKey, expectedHeader) {
if(!(typeof multibaseKey === 'string' &&
multibaseKey[0] === MULTIBASE_BASE58BTC_HEADER)) {
return false;
}
const keyBytes = base58btc__namespace.decode(multibaseKey.slice(1));
return expectedHeader.every((val, i) => keyBytes[i] === val);
}
/**
* Encodes a given Uint8Array to multibase-encoded string.
*
* @param {Uint8Array} header - Multicodec header to prepend to the bytes.
* @param {Uint8Array} bytes - Bytes to encode.
* @returns {string} Multibase-encoded string.
*/
function _multibaseEncode(header, bytes) {
const mcBytes = new Uint8Array(header.length + bytes.length);
mcBytes.set(header);
mcBytes.set(bytes, header.length);
return MULTIBASE_BASE58BTC_HEADER + base58btc__namespace.encode(mcBytes);
}
/**
* Decodes a given string as a multibase-encoded multicodec value.
*
* @param {Uint8Array} header - Expected header bytes for the multicodec value.
* @param {string} text - Multibase encoded string to decode.
* @returns {Uint8Array} Decoded bytes.
*/
function _multibaseDecode(header, text) {
const mcValue = base58btc__namespace.decode(text.substr(1));
if(!header.every((val, i) => mcValue[i] === val)) {
throw new Error('Multibase value does not have expected header.');
}
return mcValue.slice(header.length);
}
exports.X25519KeyAgreementKey2020 = X25519KeyAgreementKey2020;