@sphereon/ssi-sdk-ext.key-utils
Version:
Sphereon SSI-SDK plugin for key creation.
187 lines (176 loc) • 5.94 kB
text/typescript
import { JsonWebKey, JWK } from '@sphereon/ssi-types'
// @ts-ignore
import type { ByteView } from 'multiformats/codecs/interface'
// @ts-ignore
import { TextDecoder, TextEncoder } from 'web-encoding'
const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()
/**
* Checks if the value is a non-empty string.
*
* @param value - The value to check.
* @param description - Description of the value to check.
* @param optional
*/
function check(value: unknown, description: string, optional: boolean = false) {
if (optional && !value) {
return
}
if (typeof value !== 'string' || !value) {
throw new Error(`${description} missing or invalid`)
}
}
/**
* Checks if the value is a valid JSON object.
*
* @param value - The value to check.
*/
function assertObject(value: unknown) {
if (!value || typeof value !== 'object') {
throw new Error('Value must be an object')
}
}
/**
* Checks if the JWK is valid. It must contain all the required members.
*
* @see https://www.rfc-editor.org/rfc/rfc7518#section-6
* @see https://www.rfc-editor.org/rfc/rfc8037#section-2
*
* @param jwk - The JWK to check.
* @param opts
*/
export function validateJwk(jwk: any, opts?: { crvOptional?: boolean }) {
assertObject(jwk)
const { crvOptional = false } = opts ?? {}
check(jwk.kty, '"kty" (Key Type) Parameter', false)
// Check JWK required members based on the key type
switch (jwk.kty) {
/**
* @see https://www.rfc-editor.org/rfc/rfc7518#section-6.2.1
*/
case 'EC':
check(jwk.crv, '"crv" (Curve) Parameter', crvOptional)
check(jwk.x, '"x" (X Coordinate) Parameter')
check(jwk.y, '"y" (Y Coordinate) Parameter')
break
/**
* @see https://www.rfc-editor.org/rfc/rfc8037#section-2
*/
case 'OKP':
check(jwk.crv, '"crv" (Subtype of Key Pair) Parameter', crvOptional) // Shouldn't this one always be true as crv is not always present?
check(jwk.x, '"x" (Public Key) Parameter')
break
/**
* @see https://www.rfc-editor.org/rfc/rfc7518#section-6.3.1
*/
case 'RSA':
check(jwk.e, '"e" (Exponent) Parameter')
check(jwk.n, '"n" (Modulus) Parameter')
break
default:
throw new Error('"kty" (Key Type) Parameter missing or unsupported')
}
}
/**
* Extracts the required members of the JWK and canonicalizes it.
*
* @param jwk - The JWK to canonicalize.
* @returns The JWK with only the required members, ordered lexicographically.
*/
export function minimalJwk(jwk: any): JWK {
// "default" case is not needed
// eslint-disable-next-line default-case
switch (jwk.kty) {
case 'EC':
return { ...(jwk.crv && { crv: jwk.crv }), kty: jwk.kty, x: jwk.x, y: jwk.y }
case 'OKP':
return { ...(jwk.crv && { crv: jwk.crv }), kty: jwk.kty, x: jwk.x }
case 'RSA':
return { e: jwk.e, kty: jwk.kty, n: jwk.n }
}
throw Error(`Unsupported key type (kty) provided: ${jwk.kty}`)
}
/**
* Encodes a JWK into a Uint8Array. Only the required JWK members are encoded.
*
* @see https://www.rfc-editor.org/rfc/rfc7518#section-6
* @see https://www.rfc-editor.org/rfc/rfc8037#section-2
* @see https://github.com/panva/jose/blob/3b8aa47b92d07a711bf5c3125276cc9a011794a4/src/jwk/thumbprint.ts#L37
*
* @param jwk - JSON Web Key.
* @returns Uint8Array-encoded JWK.
*/
export function jwkJcsEncode(jwk: unknown): Uint8Array {
validateJwk(jwk)
const strippedJwk = minimalJwk(jwk)
return textEncoder.encode(jcsCanonicalize(strippedJwk))
}
/**
* Decodes an array of bytes into a JWK. Throws an error if the JWK is not valid.
*
* @param bytes - The array of bytes to decode.
* @returns The corresponding JSON Web Key.
*/
export function jwkJcsDecode(bytes: ByteView<JsonWebKey>): JsonWebKey {
const jwk = JSON.parse(textDecoder.decode(bytes))
validateJwk(jwk)
if (JSON.stringify(jwk) !== jcsCanonicalize(minimalJwk(jwk))) {
throw new Error('The JWK embedded in the DID is not correctly formatted')
}
return jwk
}
// From: https://github.com/cyberphone/json-canonicalization
export function jcsCanonicalize(object: any) {
let buffer = ''
serialize(object)
return buffer
function serialize(object: any) {
if (object === null || typeof object !== 'object' || object.toJSON != null) {
/////////////////////////////////////////////////
// Primitive type or toJSON - Use ES6/JSON //
/////////////////////////////////////////////////
buffer += JSON.stringify(object)
} else if (Array.isArray(object)) {
/////////////////////////////////////////////////
// Array - Maintain element order //
/////////////////////////////////////////////////
buffer += '['
let next = false
object.forEach((element) => {
if (next) {
buffer += ','
}
next = true
/////////////////////////////////////////
// Array element - Recursive expansion //
/////////////////////////////////////////
serialize(element)
})
buffer += ']'
} else {
/////////////////////////////////////////////////
// Object - Sort properties before serializing //
/////////////////////////////////////////////////
buffer += '{'
let next = false
Object.keys(object)
.sort()
.forEach((property) => {
if (next) {
buffer += ','
}
next = true
///////////////////////////////////////////////
// Property names are strings - Use ES6/JSON //
///////////////////////////////////////////////
buffer += JSON.stringify(property)
buffer += ':'
//////////////////////////////////////////
// Property value - Recursive expansion //
//////////////////////////////////////////
serialize(object[property])
})
buffer += '}'
}
}
}