Skip to content

Commit 1dc83ed

Browse files
achingbrainmaschad
authored andcommitted
feat: support streaming hashes for key sign/verify (libp2p#2255)
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.
1 parent 2cb85e5 commit 1dc83ed

File tree

14 files changed

+232
-59
lines changed

14 files changed

+232
-59
lines changed

packages/crypto/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
"./dist/src/hmac/index.js": "./dist/src/hmac/index-browser.js",
116116
"./dist/src/keys/ecdh.js": "./dist/src/keys/ecdh-browser.js",
117117
"./dist/src/keys/ed25519.js": "./dist/src/keys/ed25519-browser.js",
118-
"./dist/src/keys/rsa.js": "./dist/src/keys/rsa-browser.js"
118+
"./dist/src/keys/rsa.js": "./dist/src/keys/rsa-browser.js",
119+
"./dist/src/keys/secp256k1.js": "./dist/src/keys/secp256k1-browser.js"
119120
}
120121
}

packages/crypto/src/keys/ed25519-browser.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ed25519 as ed } from '@noble/curves/ed25519'
22
import type { Uint8ArrayKeyPair } from './interface'
3+
import type { Uint8ArrayList } from 'uint8arraylist'
34

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

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

50-
return ed.sign(msg, privateKeyRaw)
51+
return ed.sign(msg instanceof Uint8Array ? msg : msg.subarray(), privateKeyRaw)
5152
}
5253

53-
export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array): Promise<boolean> {
54-
return ed.verify(sig, msg, publicKey)
54+
export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
55+
return ed.verify(sig, msg instanceof Uint8Array ? msg : msg.subarray(), publicKey)
5556
}
5657

5758
function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array): Uint8Array {

packages/crypto/src/keys/ed25519-class.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as crypto from './ed25519.js'
77
import { exporter } from './exporter.js'
88
import * as pbm from './keys.js'
99
import type { Multibase } from 'multiformats'
10+
import type { Uint8ArrayList } from 'uint8arraylist'
1011

1112
export class Ed25519PublicKey {
1213
private readonly _key: Uint8Array
@@ -15,7 +16,7 @@ export class Ed25519PublicKey {
1516
this._key = ensureKey(key, crypto.publicKeyLength)
1617
}
1718

18-
async verify (data: Uint8Array, sig: Uint8Array): Promise<boolean> {
19+
async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise<boolean> {
1920
return crypto.hashAndVerify(this._key, sig, data)
2021
}
2122

@@ -52,7 +53,7 @@ export class Ed25519PrivateKey {
5253
this._publicKey = ensureKey(publicKey, crypto.publicKeyLength)
5354
}
5455

55-
async sign (message: Uint8Array): Promise<Uint8Array> {
56+
async sign (message: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
5657
return crypto.hashAndSign(this._key, message)
5758
}
5859

packages/crypto/src/keys/ed25519.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import crypto from 'crypto'
22
import { promisify } from 'util'
3+
import { concat as uint8arrayConcat } from 'uint8arrays/concat'
34
import { fromString as uint8arrayFromString } from 'uint8arrays/from-string'
45
import { toString as uint8arrayToString } from 'uint8arrays/to-string'
56
import type { Uint8ArrayKeyPair } from './interface.js'
7+
import type { Uint8ArrayList } from 'uint8arraylist'
68

79
const keypair = promisify(crypto.generateKeyPair)
810

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

4951
return {
50-
privateKey: concatKeys(privateKeyRaw, publicKeyRaw),
52+
privateKey: uint8arrayConcat([privateKeyRaw, publicKeyRaw], privateKeyRaw.byteLength + publicKeyRaw.byteLength),
5153
publicKey: publicKeyRaw
5254
}
5355
}
@@ -66,12 +68,12 @@ export async function generateKeyFromSeed (seed: Uint8Array): Promise<Uint8Array
6668
const publicKeyRaw = derivePublicKey(seed)
6769

6870
return {
69-
privateKey: concatKeys(seed, publicKeyRaw),
71+
privateKey: uint8arrayConcat([seed, publicKeyRaw], seed.byteLength + publicKeyRaw.byteLength),
7072
publicKey: publicKeyRaw
7173
}
7274
}
7375

74-
export async function hashAndSign (key: Uint8Array, msg: Uint8Array): Promise<Buffer> {
76+
export async function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<Buffer> {
7577
if (!(key instanceof Uint8Array)) {
7678
throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.')
7779
}
@@ -99,10 +101,10 @@ export async function hashAndSign (key: Uint8Array, msg: Uint8Array): Promise<Bu
99101
}
100102
})
101103

102-
return crypto.sign(null, msg, obj)
104+
return crypto.sign(null, msg instanceof Uint8Array ? msg : msg.subarray(), obj)
103105
}
104106

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

127-
return crypto.verify(null, msg, obj, sig)
128-
}
129-
130-
function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array): Uint8Array {
131-
const privateKey = new Uint8Array(PRIVATE_KEY_BYTE_LENGTH)
132-
for (let i = 0; i < KEYS_BYTE_LENGTH; i++) {
133-
privateKey[i] = privateKeyRaw[i]
134-
privateKey[KEYS_BYTE_LENGTH + i] = publicKey[i]
135-
}
136-
return privateKey
129+
return crypto.verify(null, msg instanceof Uint8Array ? msg : msg.subarray(), obj, sig)
137130
}

packages/crypto/src/keys/rsa-browser.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import webcrypto from '../webcrypto.js'
66
import { jwk2pub, jwk2priv } from './jwk2pem.js'
77
import * as utils from './rsa-utils.js'
88
import type { JWKKeyPair } from './interface.js'
9+
import type { Uint8ArrayList } from 'uint8arraylist'
910

1011
export { utils }
1112

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

6162
export { randomBytes as getRandomValues }
6263

63-
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array): Promise<Uint8Array> {
64+
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
6465
const privateKey = await webcrypto.get().subtle.importKey(
6566
'jwk',
6667
key,
@@ -75,13 +76,13 @@ export async function hashAndSign (key: JsonWebKey, msg: Uint8Array): Promise<Ui
7576
const sig = await webcrypto.get().subtle.sign(
7677
{ name: 'RSASSA-PKCS1-v1_5' },
7778
privateKey,
78-
Uint8Array.from(msg)
79+
msg instanceof Uint8Array ? msg : msg.subarray()
7980
)
8081

8182
return new Uint8Array(sig, 0, sig.byteLength)
8283
}
8384

84-
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array): Promise<boolean> {
85+
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
8586
const publicKey = await webcrypto.get().subtle.importKey(
8687
'jwk',
8788
key,
@@ -97,7 +98,7 @@ export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint
9798
{ name: 'RSASSA-PKCS1-v1_5' },
9899
publicKey,
99100
sig,
100-
msg
101+
msg instanceof Uint8Array ? msg : msg.subarray()
101102
)
102103
}
103104

@@ -141,18 +142,18 @@ Explanation:
141142
142143
*/
143144

144-
function convertKey (key: JsonWebKey, pub: boolean, msg: Uint8Array, handle: (msg: string, key: { encrypt(msg: string): string, decrypt(msg: string): string }) => string): Uint8Array {
145+
function convertKey (key: JsonWebKey, pub: boolean, msg: Uint8Array | Uint8ArrayList, handle: (msg: string, key: { encrypt(msg: string): string, decrypt(msg: string): string }) => string): Uint8Array {
145146
const fkey = pub ? jwk2pub(key) : jwk2priv(key)
146-
const fmsg = uint8ArrayToString(Uint8Array.from(msg), 'ascii')
147+
const fmsg = uint8ArrayToString(msg instanceof Uint8Array ? msg : msg.subarray(), 'ascii')
147148
const fomsg = handle(fmsg, fkey)
148149
return uint8ArrayFromString(fomsg, 'ascii')
149150
}
150151

151-
export function encrypt (key: JsonWebKey, msg: Uint8Array): Uint8Array {
152+
export function encrypt (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Uint8Array {
152153
return convertKey(key, true, msg, (msg, key) => key.encrypt(msg))
153154
}
154155

155-
export function decrypt (key: JsonWebKey, msg: Uint8Array): Uint8Array {
156+
export function decrypt (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Uint8Array {
156157
return convertKey(key, false, msg, (msg, key) => key.decrypt(msg))
157158
}
158159

packages/crypto/src/keys/rsa-class.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { exporter } from './exporter.js'
99
import * as pbm from './keys.js'
1010
import * as crypto from './rsa.js'
1111
import type { Multibase } from 'multiformats'
12+
import type { Uint8ArrayList } from 'uint8arraylist'
1213

1314
export const MAX_KEY_SIZE = 8192
1415

@@ -19,7 +20,7 @@ export class RsaPublicKey {
1920
this._key = key
2021
}
2122

22-
async verify (data: Uint8Array, sig: Uint8Array): Promise<boolean> {
23+
async verify (data: Uint8Array | Uint8ArrayList, sig: Uint8Array): Promise<boolean> {
2324
return crypto.hashAndVerify(this._key, sig, data)
2425
}
2526

@@ -34,7 +35,7 @@ export class RsaPublicKey {
3435
}).subarray()
3536
}
3637

37-
encrypt (bytes: Uint8Array): Uint8Array {
38+
encrypt (bytes: Uint8Array | Uint8ArrayList): Uint8Array {
3839
return crypto.encrypt(this._key, bytes)
3940
}
4041

@@ -62,7 +63,7 @@ export class RsaPrivateKey {
6263
return crypto.getRandomValues(16)
6364
}
6465

65-
async sign (message: Uint8Array): Promise<Uint8Array> {
66+
async sign (message: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
6667
return crypto.hashAndSign(this._key, message)
6768
}
6869

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

77-
decrypt (bytes: Uint8Array): Uint8Array {
78+
decrypt (bytes: Uint8Array | Uint8ArrayList): Uint8Array {
7879
return crypto.decrypt(this._key, bytes)
7980
}
8081

packages/crypto/src/keys/rsa.ts

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { CodeError } from '@libp2p/interface/errors'
44
import randomBytes from '../random-bytes.js'
55
import * as utils from './rsa-utils.js'
66
import type { JWKKeyPair } from './interface.js'
7+
import type { Uint8ArrayList } from 'uint8arraylist'
78

89
const keypair = promisify(crypto.generateKeyPair)
910

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

4344
export { randomBytes as getRandomValues }
4445

45-
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array): Promise<Uint8Array> {
46-
return crypto.createSign('RSA-SHA256')
47-
.update(msg)
48-
// @ts-expect-error node types are missing jwk as a format
49-
.sign({ format: 'jwk', key })
46+
export async function hashAndSign (key: JsonWebKey, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
47+
const hash = crypto.createSign('RSA-SHA256')
48+
49+
if (msg instanceof Uint8Array) {
50+
hash.update(msg)
51+
} else {
52+
for (const buf of msg) {
53+
hash.update(buf)
54+
}
55+
}
56+
57+
// @ts-expect-error node types are missing jwk as a format
58+
return hash.sign({ format: 'jwk', key })
5059
}
5160

52-
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array): Promise<boolean> {
53-
return crypto.createVerify('RSA-SHA256')
54-
.update(msg)
55-
// @ts-expect-error node types are missing jwk as a format
56-
.verify({ format: 'jwk', key }, sig)
61+
export async function hashAndVerify (key: JsonWebKey, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
62+
const hash = crypto.createVerify('RSA-SHA256')
63+
64+
if (msg instanceof Uint8Array) {
65+
hash.update(msg)
66+
} else {
67+
for (const buf of msg) {
68+
hash.update(buf)
69+
}
70+
}
71+
72+
// @ts-expect-error node types are missing jwk as a format
73+
return hash.verify({ format: 'jwk', key }, sig)
5774
}
5875

5976
const padding = crypto.constants.RSA_PKCS1_PADDING
6077

61-
export function encrypt (key: JsonWebKey, bytes: Uint8Array): Uint8Array {
62-
// @ts-expect-error node types are missing jwk as a format
63-
return crypto.publicEncrypt({ format: 'jwk', key, padding }, bytes)
78+
export function encrypt (key: JsonWebKey, bytes: Uint8Array | Uint8ArrayList): Uint8Array {
79+
if (bytes instanceof Uint8Array) {
80+
// @ts-expect-error node types are missing jwk as a format
81+
return crypto.publicEncrypt({ format: 'jwk', key, padding }, bytes)
82+
} else {
83+
// @ts-expect-error node types are missing jwk as a format
84+
return crypto.publicEncrypt({ format: 'jwk', key, padding }, bytes.subarray())
85+
}
6486
}
6587

66-
export function decrypt (key: JsonWebKey, bytes: Uint8Array): Uint8Array {
67-
// @ts-expect-error node types are missing jwk as a format
68-
return crypto.privateDecrypt({ format: 'jwk', key, padding }, bytes)
88+
export function decrypt (key: JsonWebKey, bytes: Uint8Array | Uint8ArrayList): Uint8Array {
89+
if (bytes instanceof Uint8Array) {
90+
// @ts-expect-error node types are missing jwk as a format
91+
return crypto.privateDecrypt({ format: 'jwk', key, padding }, bytes)
92+
} else {
93+
// @ts-expect-error node types are missing jwk as a format
94+
return crypto.privateDecrypt({ format: 'jwk', key, padding }, bytes.subarray())
95+
}
6996
}
7097

7198
export function keySize (jwk: JsonWebKey): number {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { CodeError } from '@libp2p/interface/errors'
2+
import { secp256k1 as secp } from '@noble/curves/secp256k1'
3+
import { sha256 } from 'multiformats/hashes/sha2'
4+
import type { Uint8ArrayList } from 'uint8arraylist'
5+
6+
const PRIVATE_KEY_BYTE_LENGTH = 32
7+
8+
export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength }
9+
10+
export function generateKey (): Uint8Array {
11+
return secp.utils.randomPrivateKey()
12+
}
13+
14+
/**
15+
* Hash and sign message with private key
16+
*/
17+
export async function hashAndSign (key: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<Uint8Array> {
18+
const { digest } = await sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray())
19+
try {
20+
const signature = secp.sign(digest, key)
21+
return signature.toDERRawBytes()
22+
} catch (err) {
23+
throw new CodeError(String(err), 'ERR_INVALID_INPUT')
24+
}
25+
}
26+
27+
/**
28+
* Hash message and verify signature with public key
29+
*/
30+
export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array | Uint8ArrayList): Promise<boolean> {
31+
try {
32+
const { digest } = await sha256.digest(msg instanceof Uint8Array ? msg : msg.subarray())
33+
return secp.verify(sig, digest, key)
34+
} catch (err) {
35+
throw new CodeError(String(err), 'ERR_INVALID_INPUT')
36+
}
37+
}
38+
39+
export function compressPublicKey (key: Uint8Array): Uint8Array {
40+
const point = secp.ProjectivePoint.fromHex(key).toRawBytes(true)
41+
return point
42+
}
43+
44+
export function decompressPublicKey (key: Uint8Array): Uint8Array {
45+
const point = secp.ProjectivePoint.fromHex(key).toRawBytes(false)
46+
return point
47+
}
48+
49+
export function validatePrivateKey (key: Uint8Array): void {
50+
try {
51+
secp.getPublicKey(key, true)
52+
} catch (err) {
53+
throw new CodeError(String(err), 'ERR_INVALID_PRIVATE_KEY')
54+
}
55+
}
56+
57+
export function validatePublicKey (key: Uint8Array): void {
58+
try {
59+
secp.ProjectivePoint.fromHex(key)
60+
} catch (err) {
61+
throw new CodeError(String(err), 'ERR_INVALID_PUBLIC_KEY')
62+
}
63+
}
64+
65+
export function computePublicKey (privateKey: Uint8Array): Uint8Array {
66+
try {
67+
return secp.getPublicKey(privateKey, true)
68+
} catch (err) {
69+
throw new CodeError(String(err), 'ERR_INVALID_PRIVATE_KEY')
70+
}
71+
}

0 commit comments

Comments
 (0)