mirror of
https://github.com/hanzoai/jose.git
synced 2026-09-24 17:00:01 +00:00
250 lines
6.9 KiB
TypeScript
250 lines
6.9 KiB
TypeScript
import crypto, { isCryptoKey } from './webcrypto.ts'
|
|
import type { PEMExportFunction, PEMImportFunction } from './interfaces.d.ts'
|
|
import invalidKeyInput from '../lib/invalid_key_input.ts'
|
|
import { encodeBase64, decodeBase64 } from './base64url.ts'
|
|
import formatPEM from '../lib/format_pem.ts'
|
|
import { JOSENotSupported } from '../util/errors.ts'
|
|
import { types } from './is_key_like.ts'
|
|
|
|
import type { PEMImportOptions } from '../key/import.ts'
|
|
|
|
const genericExport = async (
|
|
keyType: 'private' | 'public',
|
|
keyFormat: 'spki' | 'pkcs8',
|
|
key: unknown,
|
|
) => {
|
|
if (!isCryptoKey(key)) {
|
|
throw new TypeError(invalidKeyInput(key, ...types))
|
|
}
|
|
|
|
if (!key.extractable) {
|
|
throw new TypeError('CryptoKey is not extractable')
|
|
}
|
|
|
|
if (key.type !== keyType) {
|
|
throw new TypeError(`key is not a ${keyType} key`)
|
|
}
|
|
|
|
return formatPEM(
|
|
encodeBase64(new Uint8Array(await crypto.subtle.exportKey(keyFormat, key))),
|
|
`${keyType.toUpperCase()} KEY`,
|
|
)
|
|
}
|
|
|
|
export const toSPKI: PEMExportFunction = (key) => {
|
|
return genericExport('public', 'spki', key)
|
|
}
|
|
|
|
export const toPKCS8: PEMExportFunction = (key) => {
|
|
return genericExport('private', 'pkcs8', key)
|
|
}
|
|
|
|
const findOid = (keyData: Uint8Array, oid: number[], from = 0): boolean => {
|
|
if (from === 0) {
|
|
oid.unshift(oid.length)
|
|
oid.unshift(0x06)
|
|
}
|
|
let i = keyData.indexOf(oid[0], from)
|
|
if (i === -1) return false
|
|
const sub = keyData.subarray(i, i + oid.length)
|
|
if (sub.length !== oid.length) return false
|
|
return sub.every((value, index) => value === oid[index]) || findOid(keyData, oid, i + 1)
|
|
}
|
|
|
|
const getNamedCurve = (keyData: Uint8Array): string => {
|
|
switch (true) {
|
|
case findOid(keyData, [0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07]):
|
|
return 'P-256'
|
|
case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x22]):
|
|
return 'P-384'
|
|
case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x23]):
|
|
return 'P-521'
|
|
case findOid(keyData, [0x2b, 0x65, 0x6e]):
|
|
return 'X25519'
|
|
case findOid(keyData, [0x2b, 0x65, 0x6f]):
|
|
return 'X448'
|
|
case findOid(keyData, [0x2b, 0x65, 0x70]):
|
|
return 'Ed25519'
|
|
case findOid(keyData, [0x2b, 0x65, 0x71]):
|
|
return 'Ed448'
|
|
default:
|
|
throw new JOSENotSupported('Invalid or unsupported EC Key Curve or OKP Key Sub Type')
|
|
}
|
|
}
|
|
|
|
const genericImport = async (
|
|
replace: RegExp,
|
|
keyFormat: 'spki' | 'pkcs8',
|
|
pem: string,
|
|
alg: string,
|
|
options?: PEMImportOptions,
|
|
) => {
|
|
let algorithm: RsaHashedImportParams | EcKeyAlgorithm | Algorithm
|
|
let keyUsages: KeyUsage[]
|
|
|
|
const keyData = new Uint8Array(
|
|
atob(pem.replace(replace, ''))
|
|
.split('')
|
|
.map((c) => c.charCodeAt(0)),
|
|
)
|
|
|
|
const isPublic = keyFormat === 'spki'
|
|
|
|
switch (alg) {
|
|
case 'PS256':
|
|
case 'PS384':
|
|
case 'PS512':
|
|
algorithm = { name: 'RSA-PSS', hash: `SHA-${alg.slice(-3)}` }
|
|
keyUsages = isPublic ? ['verify'] : ['sign']
|
|
break
|
|
case 'RS256':
|
|
case 'RS384':
|
|
case 'RS512':
|
|
algorithm = { name: 'RSASSA-PKCS1-v1_5', hash: `SHA-${alg.slice(-3)}` }
|
|
keyUsages = isPublic ? ['verify'] : ['sign']
|
|
break
|
|
case 'RSA-OAEP':
|
|
case 'RSA-OAEP-256':
|
|
case 'RSA-OAEP-384':
|
|
case 'RSA-OAEP-512':
|
|
algorithm = {
|
|
name: 'RSA-OAEP',
|
|
hash: `SHA-${parseInt(alg.slice(-3), 10) || 1}`,
|
|
}
|
|
keyUsages = isPublic ? ['encrypt', 'wrapKey'] : ['decrypt', 'unwrapKey']
|
|
break
|
|
case 'ES256':
|
|
algorithm = { name: 'ECDSA', namedCurve: 'P-256' }
|
|
keyUsages = isPublic ? ['verify'] : ['sign']
|
|
break
|
|
case 'ES384':
|
|
algorithm = { name: 'ECDSA', namedCurve: 'P-384' }
|
|
keyUsages = isPublic ? ['verify'] : ['sign']
|
|
break
|
|
case 'ES512':
|
|
algorithm = { name: 'ECDSA', namedCurve: 'P-521' }
|
|
keyUsages = isPublic ? ['verify'] : ['sign']
|
|
break
|
|
case 'ECDH-ES':
|
|
case 'ECDH-ES+A128KW':
|
|
case 'ECDH-ES+A192KW':
|
|
case 'ECDH-ES+A256KW': {
|
|
const namedCurve = getNamedCurve(keyData)
|
|
algorithm = namedCurve.startsWith('P-') ? { name: 'ECDH', namedCurve } : { name: namedCurve }
|
|
keyUsages = isPublic ? [] : ['deriveBits']
|
|
break
|
|
}
|
|
case 'EdDSA':
|
|
algorithm = { name: getNamedCurve(keyData) }
|
|
keyUsages = isPublic ? ['verify'] : ['sign']
|
|
break
|
|
default:
|
|
throw new JOSENotSupported('Invalid or unsupported "alg" (Algorithm) value')
|
|
}
|
|
|
|
return crypto.subtle.importKey(
|
|
keyFormat,
|
|
keyData,
|
|
algorithm,
|
|
options?.extractable ?? false,
|
|
keyUsages,
|
|
)
|
|
}
|
|
|
|
export const fromPKCS8: PEMImportFunction = (pem, alg, options?) => {
|
|
return genericImport(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g, 'pkcs8', pem, alg, options)
|
|
}
|
|
|
|
export const fromSPKI: PEMImportFunction = (pem, alg, options?) => {
|
|
return genericImport(/(?:-----(?:BEGIN|END) PUBLIC KEY-----|\s)/g, 'spki', pem, alg, options)
|
|
}
|
|
|
|
function getElement(seq: Uint8Array) {
|
|
let result = []
|
|
let next = 0
|
|
|
|
while (next < seq.length) {
|
|
let nextPart = parseElement(seq.subarray(next))
|
|
result.push(nextPart)
|
|
next += nextPart.byteLength
|
|
}
|
|
return result
|
|
}
|
|
|
|
function parseElement(bytes: Uint8Array) {
|
|
let position = 0
|
|
|
|
// tag
|
|
let tag = bytes[0] & 0x1f
|
|
position++
|
|
if (tag === 0x1f) {
|
|
tag = 0
|
|
while (bytes[position] >= 0x80) {
|
|
tag = tag * 128 + bytes[position] - 0x80
|
|
position++
|
|
}
|
|
tag = tag * 128 + bytes[position] - 0x80
|
|
position++
|
|
}
|
|
|
|
// length
|
|
let length = 0
|
|
if (bytes[position] < 0x80) {
|
|
length = bytes[position]
|
|
position++
|
|
} else if (length === 0x80) {
|
|
length = 0
|
|
|
|
while (bytes[position + length] !== 0 || bytes[position + length + 1] !== 0) {
|
|
if (length > bytes.byteLength) {
|
|
throw new TypeError('invalid indefinite form length')
|
|
}
|
|
length++
|
|
}
|
|
|
|
const byteLength = position + length + 2
|
|
return {
|
|
byteLength,
|
|
contents: bytes.subarray(position, position + length),
|
|
raw: bytes.subarray(0, byteLength),
|
|
}
|
|
} else {
|
|
let numberOfDigits = bytes[position] & 0x7f
|
|
position++
|
|
length = 0
|
|
for (let i = 0; i < numberOfDigits; i++) {
|
|
length = length * 256 + bytes[position]
|
|
position++
|
|
}
|
|
}
|
|
|
|
const byteLength = position + length
|
|
return {
|
|
byteLength,
|
|
contents: bytes.subarray(position, byteLength),
|
|
raw: bytes.subarray(0, byteLength),
|
|
}
|
|
}
|
|
|
|
function spkiFromX509(buf: Uint8Array) {
|
|
const tbsCertificate = getElement(getElement(parseElement(buf).contents)[0].contents)
|
|
return encodeBase64(tbsCertificate[tbsCertificate[0].raw[0] === 0xa0 ? 6 : 5].raw)
|
|
}
|
|
|
|
function getSPKI(x509: string): string {
|
|
const pem = x509.replace(/(?:-----(?:BEGIN|END) CERTIFICATE-----|\s)/g, '')
|
|
const raw = decodeBase64(pem)
|
|
return formatPEM(spkiFromX509(raw), 'PUBLIC KEY')
|
|
}
|
|
|
|
export const fromX509: PEMImportFunction = (pem, alg, options?) => {
|
|
let spki: string
|
|
try {
|
|
spki = getSPKI(pem)
|
|
} catch (cause) {
|
|
// @ts-ignore
|
|
throw new TypeError('Failed to parse the X.509 certificate', { cause })
|
|
}
|
|
return fromSPKI(spki, alg, options)
|
|
}
|