Skip to content

Commit

Permalink
feat: support streaming hashes for key sign/verify (#2255)
Browse files Browse the repository at this point in the history
Accept `Uint8ArrayList`s as arguments to sign or verify and use
streaming hashes internally when they are passed. This way we can
skip having to copy the `Uint8ArrayList` contents into a `Uint8Array`
first.
  • Loading branch information
achingbrain authored Nov 25, 2023
1 parent f3ec538 commit ac7bc38
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 59 deletions.
3 changes: 2 additions & 1 deletion packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"./dist/src/hmac/index.js": "./dist/src/hmac/index-browser.js",
"./dist/src/keys/ecdh.js": "./dist/src/keys/ecdh-browser.js",
"./dist/src/keys/ed25519.js": "./dist/src/keys/ed25519-browser.js",
"./dist/src/keys/rsa.js": "./dist/src/keys/rsa-browser.js"
"./dist/src/keys/rsa.js": "./dist/src/keys/rsa-browser.js",
"./dist/src/keys/secp256k1.js": "./dist/src/keys/secp256k1-browser.js"
}
}
9 changes: 5 additions & 4 deletions packages/crypto/src/keys/ed25519-browser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ed25519 as ed } from '@noble/curves/ed25519'
import type { Uint8ArrayKeyPair } from './interface'
import type { Uint8ArrayList } from 'uint8arraylist'

const PUBLIC_KEY_BYTE_LENGTH = 32
const PRIVATE_KEY_BYTE_LENGTH = 64 // private key is actually 32 bytes but for historical reasons we concat private and public keys
Expand Down Expand Up @@ -44,14 +45,14 @@ export async function generateKeyFromSeed (seed: Uint8Array): Promise<Uint8Array
}
}

export async function hashAndSign (privateKey: Uint8Array, msg: Uint8Array): Promise<Uint8Array> {
export async function hashAndSign (privateKey: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
const privateKeyRaw = privateKey.subarray(0, KEYS_BYTE_LENGTH)

return ed.sign(msg, privateKeyRaw)
return ed.sign(msg instanceof Uint8Array ? msg : msg.subarray(), privateKeyRaw)
}

export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array): Promise<boolean> {
return ed.verify(sig, msg, publicKey)
export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
return ed.verify(sig, msg instanceof Uint8Array ? msg : msg.subarray(), publicKey)
}

function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array): Uint8Array {
Expand Down
5 changes: 3 additions & 2 deletions packages/crypto/src/keys/ed25519-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as crypto from './ed25519.js'
import { exporter } from './exporter.js'
import * as pbm from './keys.js'
import type { Multibase } from 'multiformats'
import type { Uint8ArrayList } from 'uint8arraylist'

export class Ed25519PublicKey {
private readonly _key: Uint8Array
Expand All @@ -15,7 +16,7 @@ export class Ed25519PublicKey {
this._key = ensureKey(key, crypto.publicKeyLength)
}

async verify (data: Uint8Array, sig: Uint8Array): Promise<boolean> {
async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise<boolean> {
return crypto.hashAndVerify(this._key, sig, data)
}

Expand Down Expand Up @@ -52,7 +53,7 @@ export class Ed25519PrivateKey {
this._publicKey = ensureKey(publicKey, crypto.publicKeyLength)
}

async sign (message: Uint8Array): Promise<Uint8Array> {
async sign (message: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
return crypto.hashAndSign(this._key, message)
}

Expand Down
23 changes: 8 additions & 15 deletions packages/crypto/src/keys/ed25519.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import crypto from 'crypto'
import { promisify } from 'util'
import { concat as uint8arrayConcat } from 'uint8arrays/concat'
import { fromString as uint8arrayFromString } from 'uint8arrays/from-string'
import { toString as uint8arrayToString } from 'uint8arrays/to-string'
import type { Uint8ArrayKeyPair } from './interface.js'
import type { Uint8ArrayList } from 'uint8arraylist'

const keypair = promisify(crypto.generateKeyPair)

Expand Down Expand Up @@ -47,7 +49,7 @@ export async function generateKey (): Promise<Uint8ArrayKeyPair> {
const publicKeyRaw = uint8arrayFromString(key.privateKey.x, 'base64url')

return {
privateKey: concatKeys(privateKeyRaw, publicKeyRaw),
privateKey: uint8arrayConcat([privateKeyRaw, publicKeyRaw], privateKeyRaw.byteLength + publicKeyRaw.byteLength),
publicKey: publicKeyRaw
}
}
Expand All @@ -66,12 +68,12 @@ export async function generateKeyFromSeed (seed: Uint8Array): Promise<Uint8Array
const publicKeyRaw = derivePublicKey(seed)

return {
privateKey: concatKeys(seed, publicKeyRaw),
privateKey: uint8arrayConcat([seed, publicKeyRaw], seed.byteLength + publicKeyRaw.byteLength),
publicKey: publicKeyRaw
}
}

export async function hashAndSign (key: Uint8Array, msg: Uint8Array): Promise<Buffer> {
export async function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<Buffer> {
if (!(key instanceof Uint8Array)) {
throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.')
}
Expand Down Expand Up @@ -99,10 +101,10 @@ export async function hashAndSign (key: Uint8Array, msg: Uint8Array): Promise<Bu
}
})

return crypto.sign(null, msg, obj)
return crypto.sign(null, msg instanceof Uint8Array ? msg : msg.subarray(), obj)
}

export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array): Promise<boolean> {
export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
if (key.byteLength !== PUBLIC_KEY_BYTE_LENGTH) {
throw new TypeError('"key" must be 32 bytes in length.')
} else if (!(key instanceof Uint8Array)) {
Expand All @@ -124,14 +126,5 @@ export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint
}
})

return crypto.verify(null, msg, obj, sig)
}

function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array): Uint8Array {
const privateKey = new Uint8Array(PRIVATE_KEY_BYTE_LENGTH)
for (let i = 0; i < KEYS_BYTE_LENGTH; i++) {
privateKey[i] = privateKeyRaw[i]
privateKey[KEYS_BYTE_LENGTH + i] = publicKey[i]
}
return privateKey
return crypto.verify(null, msg instanceof Uint8Array ? msg : msg.subarray(), obj, sig)
}
17 changes: 9 additions & 8 deletions packages/crypto/src/keys/rsa-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import webcrypto from '../webcrypto.js'
import { jwk2pub, jwk2priv } from './jwk2pem.js'
import * as utils from './rsa-utils.js'
import type { JWKKeyPair } from './interface.js'
import type { Uint8ArrayList } from 'uint8arraylist'

export { utils }

Expand Down Expand Up @@ -60,7 +61,7 @@ export async function unmarshalPrivateKey (key: JsonWebKey): Promise<JWKKeyPair>

export { randomBytes as getRandomValues }

export async function hashAndSign (key: JsonWebKey, msg: Uint8Array): Promise<Uint8Array> {
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
const privateKey = await webcrypto.get().subtle.importKey(
'jwk',
key,
Expand All @@ -75,13 +76,13 @@ export async function hashAndSign (key: JsonWebKey, msg: Uint8Array): Promise<Ui
const sig = await webcrypto.get().subtle.sign(
{ name: 'RSASSA-PKCS1-v1_5' },
privateKey,
Uint8Array.from(msg)
msg instanceof Uint8Array ? msg : msg.subarray()
)

return new Uint8Array(sig, 0, sig.byteLength)
}

export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array): Promise<boolean> {
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
const publicKey = await webcrypto.get().subtle.importKey(
'jwk',
key,
Expand All @@ -97,7 +98,7 @@ export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint
{ name: 'RSASSA-PKCS1-v1_5' },
publicKey,
sig,
msg
msg instanceof Uint8Array ? msg : msg.subarray()
)
}

Expand Down Expand Up @@ -141,18 +142,18 @@ Explanation:
*/

function convertKey (key: JsonWebKey, pub: boolean, msg: Uint8Array, handle: (msg: string, key: { encrypt(msg: string): string, decrypt(msg: string): string }) => string): Uint8Array {
function convertKey (key: JsonWebKey, pub: boolean, msg: Uint8Array | Uint8ArrayList, handle: (msg: string, key: { encrypt(msg: string): string, decrypt(msg: string): string }) => string): Uint8Array {
const fkey = pub ? jwk2pub(key) : jwk2priv(key)
const fmsg = uint8ArrayToString(Uint8Array.from(msg), 'ascii')
const fmsg = uint8ArrayToString(msg instanceof Uint8Array ? msg : msg.subarray(), 'ascii')
const fomsg = handle(fmsg, fkey)
return uint8ArrayFromString(fomsg, 'ascii')
}

export function encrypt (key: JsonWebKey, msg: Uint8Array): Uint8Array {
export function encrypt (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Uint8Array {
return convertKey(key, true, msg, (msg, key) => key.encrypt(msg))
}

export function decrypt (key: JsonWebKey, msg: Uint8Array): Uint8Array {
export function decrypt (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Uint8Array {
return convertKey(key, false, msg, (msg, key) => key.decrypt(msg))
}

Expand Down
9 changes: 5 additions & 4 deletions packages/crypto/src/keys/rsa-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { exporter } from './exporter.js'
import * as pbm from './keys.js'
import * as crypto from './rsa.js'
import type { Multibase } from 'multiformats'
import type { Uint8ArrayList } from 'uint8arraylist'

export const MAX_KEY_SIZE = 8192

Expand All @@ -19,7 +20,7 @@ export class RsaPublicKey {
this._key = key
}

async verify (data: Uint8Array, sig: Uint8Array): Promise<boolean> {
async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise<boolean> {
return crypto.hashAndVerify(this._key, sig, data)
}

Expand All @@ -34,7 +35,7 @@ export class RsaPublicKey {
}).subarray()
}

encrypt (bytes: Uint8Array): Uint8Array {
encrypt (bytes: Uint8Array | Uint8ArrayList): Uint8Array {
return crypto.encrypt(this._key, bytes)
}

Expand Down Expand Up @@ -62,7 +63,7 @@ export class RsaPrivateKey {
return crypto.getRandomValues(16)
}

async sign (message: Uint8Array): Promise<Uint8Array> {
async sign (message: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
return crypto.hashAndSign(this._key, message)
}

Expand All @@ -74,7 +75,7 @@ export class RsaPrivateKey {
return new RsaPublicKey(this._publicKey)
}

decrypt (bytes: Uint8Array): Uint8Array {
decrypt (bytes: Uint8Array | Uint8ArrayList): Uint8Array {
return crypto.decrypt(this._key, bytes)
}

Expand Down
59 changes: 43 additions & 16 deletions packages/crypto/src/keys/rsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { CodeError } from '@libp2p/interface/errors'
import randomBytes from '../random-bytes.js'
import * as utils from './rsa-utils.js'
import type { JWKKeyPair } from './interface.js'
import type { Uint8ArrayList } from 'uint8arraylist'

const keypair = promisify(crypto.generateKeyPair)

Expand Down Expand Up @@ -42,30 +43,56 @@ export async function unmarshalPrivateKey (key: JsonWebKey): Promise<JWKKeyPair>

export { randomBytes as getRandomValues }

export async function hashAndSign (key: JsonWebKey, msg: Uint8Array): Promise<Uint8Array> {
return crypto.createSign('RSA-SHA256')
.update(msg)
// @ts-expect-error node types are missing jwk as a format
.sign({ format: 'jwk', key })
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
const hash = crypto.createSign('RSA-SHA256')

if (msg instanceof Uint8Array) {
hash.update(msg)
} else {
for (const buf of msg) {
hash.update(buf)
}
}

// @ts-expect-error node types are missing jwk as a format
return hash.sign({ format: 'jwk', key })
}

export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array): Promise<boolean> {
return crypto.createVerify('RSA-SHA256')
.update(msg)
// @ts-expect-error node types are missing jwk as a format
.verify({ format: 'jwk', key }, sig)
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
const hash = crypto.createVerify('RSA-SHA256')

if (msg instanceof Uint8Array) {
hash.update(msg)
} else {
for (const buf of msg) {
hash.update(buf)
}
}

// @ts-expect-error node types are missing jwk as a format
return hash.verify({ format: 'jwk', key }, sig)
}

const padding = crypto.constants.RSA_PKCS1_PADDING

export function encrypt (key: JsonWebKey, bytes: Uint8Array): Uint8Array {
// @ts-expect-error node types are missing jwk as a format
return crypto.publicEncrypt({ format: 'jwk', key, padding }, bytes)
export function encrypt (key: JsonWebKey, bytes: Uint8Array | Uint8ArrayList): Uint8Array {
if (bytes instanceof Uint8Array) {
// @ts-expect-error node types are missing jwk as a format
return crypto.publicEncrypt({ format: 'jwk', key, padding }, bytes)
} else {
// @ts-expect-error node types are missing jwk as a format
return crypto.publicEncrypt({ format: 'jwk', key, padding }, bytes.subarray())
}
}

export function decrypt (key: JsonWebKey, bytes: Uint8Array): Uint8Array {
// @ts-expect-error node types are missing jwk as a format
return crypto.privateDecrypt({ format: 'jwk', key, padding }, bytes)
export function decrypt (key: JsonWebKey, bytes: Uint8Array | Uint8ArrayList): Uint8Array {
if (bytes instanceof Uint8Array) {
// @ts-expect-error node types are missing jwk as a format
return crypto.privateDecrypt({ format: 'jwk', key, padding }, bytes)
} else {
// @ts-expect-error node types are missing jwk as a format
return crypto.privateDecrypt({ format: 'jwk', key, padding }, bytes.subarray())
}
}

export function keySize (jwk: JsonWebKey): number {
Expand Down
71 changes: 71 additions & 0 deletions packages/crypto/src/keys/secp256k1-browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { CodeError } from '@libp2p/interface/errors'
import { secp256k1 as secp } from '@noble/curves/secp256k1'
import { sha256 } from 'multiformats/hashes/sha2'
import type { Uint8ArrayList } from 'uint8arraylist'

const PRIVATE_KEY_BYTE_LENGTH = 32

export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength }

export function generateKey (): Uint8Array {
return secp.utils.randomPrivateKey()
}

/**
* Hash and sign message with private key
*/
export async function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
const { digest } = await sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray())
try {
const signature = secp.sign(digest, key)
return signature.toDERRawBytes()
} catch (err) {
throw new CodeError(String(err), 'ERR_INVALID_INPUT')
}
}

/**
* Hash message and verify signature with public key
*/
export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
try {
const { digest } = await sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray())
return secp.verify(sig, digest, key)
} catch (err) {
throw new CodeError(String(err), 'ERR_INVALID_INPUT')
}
}

export function compressPublicKey (key: Uint8Array): Uint8Array {
const point = secp.ProjectivePoint.fromHex(key).toRawBytes(true)
return point
}

export function decompressPublicKey (key: Uint8Array): Uint8Array {
const point = secp.ProjectivePoint.fromHex(key).toRawBytes(false)
return point
}

export function validatePrivateKey (key: Uint8Array): void {
try {
secp.getPublicKey(key, true)
} catch (err) {
throw new CodeError(String(err), 'ERR_INVALID_PRIVATE_KEY')
}
}

export function validatePublicKey (key: Uint8Array): void {
try {
secp.ProjectivePoint.fromHex(key)
} catch (err) {
throw new CodeError(String(err), 'ERR_INVALID_PUBLIC_KEY')
}
}

export function computePublicKey (privateKey: Uint8Array): Uint8Array {
try {
return secp.getPublicKey(privateKey, true)
} catch (err) {
throw new CodeError(String(err), 'ERR_INVALID_PRIVATE_KEY')
}
}
Loading

0 comments on commit ac7bc38

Please sign in to comment.