Skip to content

Commit a1cc6b0

Browse files
authored
Merge pull request #133 from web3-storage/dynamic-backend
feat: Make crypto implementation pluggable.
2 parents b879089 + c988bcd commit a1cc6b0

26 files changed

+322
-238
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ This module exposes a crypto interface, as defined in the repository [js-interfa
5050

5151
[» API Docs](https://github.com/libp2p/js-libp2p-interfaces/tree/master/packages/libp2p-interfaces/src/crypto#api)
5252

53+
## Bring your own crypto
54+
55+
You can provide a custom crypto implementation (instead of the default, based on [stablelib](https://www.stablelib.com/)) by passing a third argument to the `Noise` constructor.
56+
57+
The implementation must conform to the `ICryptoInterface`, defined in https://github.com/ChainSafe/js-libp2p-noise/blob/master/src/crypto.ts
5358

5459
## Contribute
5560

src/@types/handshake-interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import type { PeerId } from '@libp2p/interfaces/peer-id'
12
import type { bytes } from './basic.js'
23
import type { NoiseSession } from './handshake.js'
3-
import type { PeerId } from '@libp2p/interfaces/peer-id'
44

55
export interface IHandshake {
66
session: NoiseSession

src/@types/libp2p.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { bytes, bytes32 } from './basic.js'
21
import type { ConnectionEncrypter } from '@libp2p/interfaces/connection-encrypter'
2+
import type { bytes, bytes32 } from './basic.js'
33

44
export interface KeyPair {
55
publicKey: bytes32

src/crypto.ts

Lines changed: 11 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,16 @@
1-
import type { Transform } from 'it-stream-types'
2-
import type { IHandshake } from './@types/handshake-interface.js'
3-
import { NOISE_MSG_MAX_LENGTH_BYTES, NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG } from './constants.js'
1+
import type { bytes32, bytes } from './@types/basic.js'
2+
import type { Hkdf } from './@types/handshake.js'
3+
import type { KeyPair } from './@types/libp2p.js'
44

5-
// Returns generator that encrypts payload from the user
6-
export function encryptStream (handshake: IHandshake): Transform<Uint8Array> {
7-
return async function * (source) {
8-
for await (const chunk of source) {
9-
for (let i = 0; i < chunk.length; i += NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG) {
10-
let end = i + NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG
11-
if (end > chunk.length) {
12-
end = chunk.length
13-
}
5+
export interface ICryptoInterface {
6+
hashSHA256: (data: Uint8Array) => Uint8Array
147

15-
const data = handshake.encrypt(chunk.slice(i, end), handshake.session)
16-
yield data
17-
}
18-
}
19-
}
20-
}
8+
getHKDF: (ck: bytes32, ikm: Uint8Array) => Hkdf
219

22-
// Decrypt received payload to the user
23-
export function decryptStream (handshake: IHandshake): Transform<Uint8Array> {
24-
return async function * (source) {
25-
for await (const chunk of source) {
26-
for (let i = 0; i < chunk.length; i += NOISE_MSG_MAX_LENGTH_BYTES) {
27-
let end = i + NOISE_MSG_MAX_LENGTH_BYTES
28-
if (end > chunk.length) {
29-
end = chunk.length
30-
}
10+
generateX25519KeyPair: () => KeyPair
11+
generateX25519KeyPairFromSeed: (seed: Uint8Array) => KeyPair
12+
generateX25519SharedKey: (privateKey: Uint8Array, publicKey: Uint8Array) => Uint8Array
3113

32-
const { plaintext: decrypted, valid } = await handshake.decrypt(chunk.slice(i, end), handshake.session)
33-
if (!valid) {
34-
throw new Error('Failed to validate decrypted chunk')
35-
}
36-
yield decrypted
37-
}
38-
}
39-
}
14+
chaCha20Poly1305Encrypt: (plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32) => bytes
15+
chaCha20Poly1305Decrypt: (ciphertext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32) => bytes | null
4016
}

src/crypto/stablelib.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { HKDF } from '@stablelib/hkdf'
2+
import * as x25519 from '@stablelib/x25519'
3+
import { SHA256, hash } from '@stablelib/sha256'
4+
import { ChaCha20Poly1305 } from '@stablelib/chacha20poly1305'
5+
import type { bytes32, bytes } from '../@types/basic.js'
6+
import type { Hkdf } from '../@types/handshake.js'
7+
import type { KeyPair } from '../@types/libp2p.js'
8+
import type { ICryptoInterface } from '../crypto.js'
9+
10+
export const stablelib: ICryptoInterface = {
11+
hashSHA256 (data: Uint8Array): Uint8Array {
12+
return hash(data)
13+
},
14+
15+
getHKDF (ck: bytes32, ikm: Uint8Array): Hkdf {
16+
const hkdf = new HKDF(SHA256, ikm, ck)
17+
const okmU8Array = hkdf.expand(96)
18+
const okm = okmU8Array
19+
20+
const k1 = okm.slice(0, 32)
21+
const k2 = okm.slice(32, 64)
22+
const k3 = okm.slice(64, 96)
23+
24+
return [k1, k2, k3]
25+
},
26+
27+
generateX25519KeyPair (): KeyPair {
28+
const keypair = x25519.generateKeyPair()
29+
30+
return {
31+
publicKey: keypair.publicKey,
32+
privateKey: keypair.secretKey
33+
}
34+
},
35+
36+
generateX25519KeyPairFromSeed (seed: Uint8Array): KeyPair {
37+
const keypair = x25519.generateKeyPairFromSeed(seed)
38+
39+
return {
40+
publicKey: keypair.publicKey,
41+
privateKey: keypair.secretKey
42+
}
43+
},
44+
45+
generateX25519SharedKey (privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
46+
return x25519.sharedKey(privateKey, publicKey)
47+
},
48+
49+
chaCha20Poly1305Encrypt (plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes {
50+
const ctx = new ChaCha20Poly1305(k)
51+
52+
return ctx.seal(nonce, plaintext, ad)
53+
},
54+
55+
chaCha20Poly1305Decrypt (ciphertext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes | null {
56+
const ctx = new ChaCha20Poly1305(k)
57+
58+
return ctx.open(nonce, ciphertext, ad)
59+
}
60+
}

src/crypto/streaming.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Transform } from 'it-stream-types'
2+
import type { IHandshake } from '../@types/handshake-interface.js'
3+
import { NOISE_MSG_MAX_LENGTH_BYTES, NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG } from '../constants.js'
4+
5+
// Returns generator that encrypts payload from the user
6+
export function encryptStream (handshake: IHandshake): Transform<Uint8Array> {
7+
return async function * (source) {
8+
for await (const chunk of source) {
9+
for (let i = 0; i < chunk.length; i += NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG) {
10+
let end = i + NOISE_MSG_MAX_LENGTH_BYTES_WITHOUT_TAG
11+
if (end > chunk.length) {
12+
end = chunk.length
13+
}
14+
15+
const data = handshake.encrypt(chunk.slice(i, end), handshake.session)
16+
yield data
17+
}
18+
}
19+
}
20+
}
21+
22+
// Decrypt received payload to the user
23+
export function decryptStream (handshake: IHandshake): Transform<Uint8Array> {
24+
return async function * (source) {
25+
for await (const chunk of source) {
26+
for (let i = 0; i < chunk.length; i += NOISE_MSG_MAX_LENGTH_BYTES) {
27+
let end = i + NOISE_MSG_MAX_LENGTH_BYTES
28+
if (end > chunk.length) {
29+
end = chunk.length
30+
}
31+
32+
const { plaintext: decrypted, valid } = await handshake.decrypt(chunk.slice(i, end), handshake.session)
33+
if (!valid) {
34+
throw new Error('Failed to validate decrypted chunk')
35+
}
36+
yield decrypted
37+
}
38+
}
39+
}
40+
}

src/encoder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
2+
import type { Uint8ArrayList } from 'uint8arraylist'
13
import type { bytes } from './@types/basic.js'
24
import type { MessageBuffer } from './@types/handshake.js'
3-
import type { Uint8ArrayList } from 'uint8arraylist'
4-
import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
55

66
const allocUnsafe = (len: number): Uint8Array => {
77
if (globalThis.Buffer) {

src/handshake-ik.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import type { PeerId } from '@libp2p/interfaces/peer-id'
12
import type { ProtobufStream } from 'it-pb-stream'
2-
import { IK } from './handshakes/ik.js'
33
import type { CipherState, NoiseSession } from './@types/handshake.js'
44
import type { bytes, bytes32 } from './@types/basic.js'
55
import type { KeyPair } from './@types/libp2p.js'
66
import type { IHandshake } from './@types/handshake-interface.js'
7+
import type { ICryptoInterface } from './crypto.js'
8+
import { IK } from './handshakes/ik.js'
79
import { decode0, decode1, encode0, encode1 } from './encoder.js'
8-
import { decodePayload, getPeerIdFromPayload, verifySignedPayload } from './utils.js'
910
import { FailedIKError } from './errors.js'
1011
import {
1112
logger,
@@ -15,7 +16,7 @@ import {
1516
logRemoteEphemeralKey,
1617
logCipherState
1718
} from './logger.js'
18-
import type { PeerId } from '@libp2p/interfaces/peer-id'
19+
import { decodePayload, getPeerIdFromPayload, verifySignedPayload } from './utils.js'
1920

2021
export class IKHandshake implements IHandshake {
2122
public isInitiator: boolean
@@ -33,6 +34,7 @@ export class IKHandshake implements IHandshake {
3334
isInitiator: boolean,
3435
payload: bytes,
3536
prologue: bytes32,
37+
crypto: ICryptoInterface,
3638
staticKeypair: KeyPair,
3739
connection: ProtobufStream,
3840
remoteStaticKey: bytes,
@@ -47,7 +49,7 @@ export class IKHandshake implements IHandshake {
4749
if (remotePeer) {
4850
this.remotePeer = remotePeer
4951
}
50-
this.ik = handshake ?? new IK()
52+
this.ik = handshake ?? new IK(crypto)
5153
this.session = this.ik.initSession(this.isInitiator, this.prologue, this.staticKeypair, remoteStaticKey)
5254
this.remoteEarlyData = new Uint8Array()
5355
}

src/handshake-xx-fallback.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import type { PeerId } from '@libp2p/interfaces/peer-id'
2+
import type { ProtobufStream } from 'it-pb-stream'
3+
import type { bytes, bytes32 } from './@types/basic.js'
4+
import type { KeyPair } from './@types/libp2p.js'
15
import { XXHandshake } from './handshake-xx.js'
26
import type { XX } from './handshakes/xx.js'
3-
import type { KeyPair } from './@types/libp2p.js'
4-
import type { bytes, bytes32 } from './@types/basic.js'
5-
import { decodePayload, getPeerIdFromPayload, verifySignedPayload } from './utils.js'
6-
import { logger, logLocalEphemeralKeys, logRemoteEphemeralKey, logRemoteStaticKey } from './logger.js'
7-
import type { ProtobufStream } from 'it-pb-stream'
7+
import type { ICryptoInterface } from './crypto.js'
88
import { decode0, decode1 } from './encoder.js'
9-
import type { PeerId } from '@libp2p/interfaces/peer-id'
9+
import { logger, logLocalEphemeralKeys, logRemoteEphemeralKey, logRemoteStaticKey } from './logger.js'
10+
import { decodePayload, getPeerIdFromPayload, verifySignedPayload } from './utils.js'
1011

1112
export class XXFallbackHandshake extends XXHandshake {
1213
private readonly ephemeralKeys?: KeyPair
@@ -16,14 +17,15 @@ export class XXFallbackHandshake extends XXHandshake {
1617
isInitiator: boolean,
1718
payload: bytes,
1819
prologue: bytes32,
20+
crypto: ICryptoInterface,
1921
staticKeypair: KeyPair,
2022
connection: ProtobufStream,
2123
initialMsg: bytes,
2224
remotePeer?: PeerId,
2325
ephemeralKeys?: KeyPair,
2426
handshake?: XX
2527
) {
26-
super(isInitiator, payload, prologue, staticKeypair, connection, remotePeer, handshake)
28+
super(isInitiator, payload, prologue, crypto, staticKeypair, connection, remotePeer, handshake)
2729
if (ephemeralKeys) {
2830
this.ephemeralKeys = ephemeralKeys
2931
}

src/handshake-xx.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { XX } from './handshakes/xx.js'
2-
import type { KeyPair } from './@types/libp2p.js'
1+
import type { PeerId } from '@libp2p/interfaces/peer-id'
2+
import type { ProtobufStream } from 'it-pb-stream'
33
import type { bytes, bytes32 } from './@types/basic.js'
44
import type { CipherState, NoiseSession } from './@types/handshake.js'
5+
import type { KeyPair } from './@types/libp2p.js'
56
import type { IHandshake } from './@types/handshake-interface.js'
6-
import {
7-
decodePayload,
8-
getPeerIdFromPayload,
9-
verifySignedPayload
10-
} from './utils.js'
7+
import type { ICryptoInterface } from './crypto.js'
8+
import { decode0, decode1, decode2, encode0, encode1, encode2 } from './encoder.js'
9+
import { XX } from './handshakes/xx.js'
1110
import {
1211
logger,
1312
logLocalStaticKeys,
@@ -16,9 +15,11 @@ import {
1615
logRemoteStaticKey,
1716
logCipherState
1817
} from './logger.js'
19-
import { decode0, decode1, decode2, encode0, encode1, encode2 } from './encoder.js'
20-
import type { ProtobufStream } from 'it-pb-stream'
21-
import type { PeerId } from '@libp2p/interfaces/peer-id'
18+
import {
19+
decodePayload,
20+
getPeerIdFromPayload,
21+
verifySignedPayload
22+
} from './utils.js'
2223

2324
export class XXHandshake implements IHandshake {
2425
public isInitiator: boolean
@@ -37,6 +38,7 @@ export class XXHandshake implements IHandshake {
3738
isInitiator: boolean,
3839
payload: bytes,
3940
prologue: bytes32,
41+
crypto: ICryptoInterface,
4042
staticKeypair: KeyPair,
4143
connection: ProtobufStream,
4244
remotePeer?: PeerId,
@@ -50,7 +52,7 @@ export class XXHandshake implements IHandshake {
5052
if (remotePeer) {
5153
this.remotePeer = remotePeer
5254
}
53-
this.xx = handshake ?? new XX()
55+
this.xx = handshake ?? new XX(crypto)
5456
this.session = this.xx.initSession(this.isInitiator, this.prologue, this.staticKeypair)
5557
this.remoteEarlyData = new Uint8Array(0)
5658
}

0 commit comments

Comments
 (0)