@libp2p/crypto
Version:
Crypto primitives for libp2p
215 lines (182 loc) • 6.34 kB
text/typescript
import { InvalidParametersError, InvalidPublicKeyError } from '@libp2p/interface'
import { sha256 } from '@noble/hashes/sha256'
import { create } from 'multiformats/hashes/digest'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
import * as pb from '../keys.js'
import { decodeDer, encodeBitString, encodeInteger, encodeSequence } from './der.js'
import { RSAPrivateKey as RSAPrivateKeyClass, RSAPublicKey as RSAPublicKeyClass } from './rsa.js'
import { generateRSAKey, rsaKeySize } from './index.js'
import type { JWKKeyPair } from '../interface.js'
import type { RSAPrivateKey, RSAPublicKey } from '@libp2p/interface'
import type { Digest } from 'multiformats/hashes/digest'
export const MAX_RSA_KEY_SIZE = 8192
const SHA2_256_CODE = 0x12
const MAX_RSA_JWK_SIZE = 1062
const RSA_ALGORITHM_IDENTIFIER = Uint8Array.from([
0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00
])
/**
* Convert a PKCS#1 in ASN1 DER format to a JWK private key
*/
export function pkcs1ToJwk (bytes: Uint8Array): JsonWebKey {
const message = decodeDer(bytes)
return pkcs1MessageToJwk(message)
}
/**
* Convert a PKCS#1 in ASN1 DER format to a JWK private key
*/
export function pkcs1MessageToJwk (message: any): JsonWebKey {
return {
n: uint8ArrayToString(message[1], 'base64url'),
e: uint8ArrayToString(message[2], 'base64url'),
d: uint8ArrayToString(message[3], 'base64url'),
p: uint8ArrayToString(message[4], 'base64url'),
q: uint8ArrayToString(message[5], 'base64url'),
dp: uint8ArrayToString(message[6], 'base64url'),
dq: uint8ArrayToString(message[7], 'base64url'),
qi: uint8ArrayToString(message[8], 'base64url'),
kty: 'RSA'
}
}
/**
* Convert a JWK private key into PKCS#1 in ASN1 DER format
*/
export function jwkToPkcs1 (jwk: JsonWebKey): Uint8Array {
if (jwk.n == null || jwk.e == null || jwk.d == null || jwk.p == null || jwk.q == null || jwk.dp == null || jwk.dq == null || jwk.qi == null) {
throw new InvalidParametersError('JWK was missing components')
}
return encodeSequence([
encodeInteger(Uint8Array.from([0])),
encodeInteger(uint8ArrayFromString(jwk.n, 'base64url')),
encodeInteger(uint8ArrayFromString(jwk.e, 'base64url')),
encodeInteger(uint8ArrayFromString(jwk.d, 'base64url')),
encodeInteger(uint8ArrayFromString(jwk.p, 'base64url')),
encodeInteger(uint8ArrayFromString(jwk.q, 'base64url')),
encodeInteger(uint8ArrayFromString(jwk.dp, 'base64url')),
encodeInteger(uint8ArrayFromString(jwk.dq, 'base64url')),
encodeInteger(uint8ArrayFromString(jwk.qi, 'base64url'))
]).subarray()
}
/**
* Convert a PKIX in ASN1 DER format to a JWK public key
*/
export function pkixToJwk (bytes: Uint8Array): JsonWebKey {
const message = decodeDer(bytes, {
offset: 0
})
return pkixMessageToJwk(message)
}
export function pkixMessageToJwk (message: any): JsonWebKey {
const keys = decodeDer(message[1], {
offset: 0
})
// this looks fragile but DER is a canonical format so we are safe to have
// deeply property chains like this
return {
kty: 'RSA',
n: uint8ArrayToString(
keys[0],
'base64url'
),
e: uint8ArrayToString(
keys[1],
'base64url'
)
}
}
/**
* Convert a JWK public key to PKIX in ASN1 DER format
*/
export function jwkToPkix (jwk: JsonWebKey): Uint8Array {
if (jwk.n == null || jwk.e == null) {
throw new InvalidParametersError('JWK was missing components')
}
const subjectPublicKeyInfo = encodeSequence([
RSA_ALGORITHM_IDENTIFIER,
encodeBitString(
encodeSequence([
encodeInteger(uint8ArrayFromString(jwk.n, 'base64url')),
encodeInteger(uint8ArrayFromString(jwk.e, 'base64url'))
])
)
])
return subjectPublicKeyInfo.subarray()
}
/**
* Turn PKCS#1 DER bytes into a PrivateKey
*/
export function pkcs1ToRSAPrivateKey (bytes: Uint8Array): RSAPrivateKey {
const message = decodeDer(bytes)
return pkcs1MessageToRSAPrivateKey(message)
}
/**
* Turn PKCS#1 DER bytes into a PrivateKey
*/
export function pkcs1MessageToRSAPrivateKey (message: any): RSAPrivateKey {
const jwk = pkcs1MessageToJwk(message)
return jwkToRSAPrivateKey(jwk)
}
/**
* Turn a PKIX message into a PublicKey
*/
export function pkixToRSAPublicKey (bytes: Uint8Array, digest?: Digest<18, number>): RSAPublicKey {
if (bytes.byteLength >= MAX_RSA_JWK_SIZE) {
throw new InvalidPublicKeyError('Key size is too large')
}
const message = decodeDer(bytes, {
offset: 0
})
return pkixMessageToRSAPublicKey(message, bytes, digest)
}
export function pkixMessageToRSAPublicKey (message: any, bytes: Uint8Array, digest?: Digest<18, number>): RSAPublicKey {
const jwk = pkixMessageToJwk(message)
if (digest == null) {
const hash = sha256(pb.PublicKey.encode({
Type: pb.KeyType.RSA,
Data: bytes
}))
digest = create(SHA2_256_CODE, hash)
}
return new RSAPublicKeyClass(jwk, digest)
}
export function jwkToRSAPrivateKey (jwk: JsonWebKey): RSAPrivateKey {
if (rsaKeySize(jwk) > MAX_RSA_KEY_SIZE) {
throw new InvalidParametersError('Key size is too large')
}
const keys = jwkToJWKKeyPair(jwk)
const hash = sha256(pb.PublicKey.encode({
Type: pb.KeyType.RSA,
Data: jwkToPkix(keys.publicKey)
}))
const digest = create(SHA2_256_CODE, hash)
return new RSAPrivateKeyClass(keys.privateKey, new RSAPublicKeyClass(keys.publicKey, digest))
}
export async function generateRSAKeyPair (bits: number): Promise<RSAPrivateKey> {
if (bits > MAX_RSA_KEY_SIZE) {
throw new InvalidParametersError('Key size is too large')
}
const keys = await generateRSAKey(bits)
const hash = sha256(pb.PublicKey.encode({
Type: pb.KeyType.RSA,
Data: jwkToPkix(keys.publicKey)
}))
const digest = create(SHA2_256_CODE, hash)
return new RSAPrivateKeyClass(keys.privateKey, new RSAPublicKeyClass(keys.publicKey, digest))
}
/**
* Takes a jwk key and returns a JWK KeyPair
*/
export function jwkToJWKKeyPair (key: JsonWebKey): JWKKeyPair {
if (key == null) {
throw new InvalidParametersError('Missing key parameter')
}
return {
privateKey: key,
publicKey: {
kty: key.kty,
n: key.n,
e: key.e
}
}
}