@didtools/key-webcrypto
Version:
This is a DID Provider which implements [EIP2844](https://eips.ethereum.org/EIPS/eip-2844) for `did:key:` using webcrypto with non-extractable keys. Currently P-256 is supported.
168 lines (167 loc) • 5.45 kB
JavaScript
/**
* # Webcrypto Key Did Provider
* This is a DID Provider which implements [EIP2844](https://eips.ethereum.org/EIPS/eip-2844) for `did:key:` using webcrypto with non-extractable keys.
*
* ## Installation
*
* ```
* npm install --save @didtools/key-webcrypto
* ```
*
* ## Usage
*
* ```js
* import { CryptoKeyProvider, generateP256KeyPair } from '@didtools/key-webcrypto'
* import KeyResolver from 'key-did-resolver'
* import { DID } from 'dids'
*
* const keyPair = generateP256KeyPair()
* const provider = new WebcryptoProvider(keypair)
* const did = new DID({ provider, resolver: KeyResolver.getResolver() })
* await did.authenticate()
*
* // log the DID
* console.log(did.id)
*
* // create JWS
* const { jws, linkedBlock } = await did.createDagJWS({ hello: 'world' })
*
* // verify JWS
* await did.verifyJWS(jws)
* ```
*
* @module @didtools/key-webcrypto
*/ function _define_property(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
import stringify from 'fast-json-stable-stringify';
import { RPCError, createHandler } from 'rpc-utils';
import * as u8a from 'uint8arrays';
import varint from 'varint';
function toStableObject(obj) {
return JSON.parse(stringify(obj));
}
/**
* compress a public key with points x,y expressed as UintArrays
* source: https://stackoverflow.com/questions/17171542/algorithm-for-elliptic-curve-point-compression
*
* @param {Uint8Array} x x point of public key
* @param {Uint8Array} y y point of public key
* @return {Uint8Array} compressed form of public key as Uint8Array
*/ export function ecPointCompress(x, y) {
const out = new Uint8Array(x.length + 1);
out[0] = 2 + (y[y.length - 1] & 1);
out.set(x, 1);
return out;
}
// export the raw public key from the CryptoKeyPair using the webcrypto api
export async function getPublicKey({ publicKey }) {
const rawKey = await globalThis.crypto.subtle.exportKey('raw', publicKey);
// convert raw key with x,y to a compressed key
const compressedKey = ecPointCompress(new Uint8Array(rawKey.slice(1, 33)), new Uint8Array(rawKey.slice(33, 65)));
return compressedKey;
}
export function encodeDIDFromPub(publicKey) {
const CODE = new Uint8Array(varint.encode(0x1200)) // p-256 multicodec
;
const bytes = u8a.concat([
CODE,
publicKey
]);
return `did:key:z${u8a.toString(bytes, 'base58btc')}`;
}
// A method that generates a new key using webcrypto, p-256, extractable false.
export async function generateP256KeyPair() {
const { privateKey, publicKey } = await globalThis.crypto.subtle.generateKey({
name: 'ECDSA',
namedCurve: 'P-256'
}, false, [
'sign'
]);
return {
privateKey,
publicKey
};
}
function toGeneralJWS(jws) {
const [protectedHeader, payload, signature] = jws.split('.');
return {
payload,
signatures: [
{
protected: protectedHeader,
signature
}
]
};
}
function jsonToBase64Url(obj) {
return u8a.toString(u8a.fromString(JSON.stringify(obj)), 'base64url');
}
const sign = async (payload, did, cryptoKeyPair, protectedHeader = {})=>{
const kid = `${did}#${did.split(':')[2]}`;
const header = toStableObject(Object.assign(protectedHeader, {
kid,
alg: 'ES256'
}));
const encodedHeader = jsonToBase64Url(header);
const actualPayload = typeof payload === 'string' ? payload : jsonToBase64Url(toStableObject(payload));
const data = `${encodedHeader}.${actualPayload}`;
const signature = await globalThis.crypto.subtle.sign({
name: 'ECDSA',
hash: 'SHA-256'
}, cryptoKeyPair.privateKey, u8a.fromString(data));
const encodedSignature = u8a.toString(new Uint8Array(signature), 'base64url');
return `${data}.${encodedSignature}`;
};
const didMethods = {
did_authenticate: async ({ keyPair }, params)=>{
const did = encodeDIDFromPub(await getPublicKey(keyPair));
const response = await sign({
did,
aud: params.aud,
nonce: params.nonce,
paths: params.paths,
exp: Math.floor(Date.now() / 1000) + 600
}, did, keyPair);
return toGeneralJWS(response);
},
did_createJWS: async ({ keyPair }, params)=>{
const did = encodeDIDFromPub(await getPublicKey(keyPair));
const requestDid = params.did.split('#')[0];
if (requestDid !== did) throw new RPCError(4100, `Unknown DID: ${did}`);
const jws = await sign(params.payload, did, keyPair, params.protected);
return {
jws: toGeneralJWS(jws)
};
},
// eslint-disable-next-line @typescript-eslint/require-await
did_decryptJWE: async ()=>{
throw new RPCError(4100, 'Decryption not supported');
}
};
export class WebcryptoProvider {
get isDidProvider() {
return true;
}
async send(msg) {
return await this._handle(msg);
}
constructor(keyPair){
_define_property(this, "_handle", void 0);
const handler = createHandler(didMethods);
this._handle = async (msg)=>await handler({
keyPair
}, msg);
}
}