Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sodium-native #161

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions benchmarks/crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/* eslint-disable */
import {stablelib} from '../dist/src/crypto/stablelib.js'
import {sodiumNative} from '../dist/src/crypto/sodium-native.js'
import benchmark from 'benchmark'

const main = async function () {
const data = Buffer.from('encryptthis encryptthis encryptthis encryptthis')
const nonce = 1000
const nonceBytes = new Uint8Array(12)
new DataView(nonceBytes.buffer, nonceBytes.byteOffset, nonceBytes.byteLength).setUint32(4, nonce, true)
const key = new Uint8Array(Array.from({length: 32}, () => 1))
const encrypted = stablelib.chaCha20Poly1305Encrypt(data, nonceBytes, new Uint8Array(0), key)

for(const {id, crypto} of [
{id: 'stablelib decrypt', crypto: stablelib},
{id: 'sodium-native decrypt', crypto: sodiumNative}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tuyennhv please do benchmarks for a range of data sizes. Research what the range of paquet sizes for eth2 objects and bechmark against them, for example: [200, 500, 1000, 1e4, 1e5, 1e6]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dapplion for eth2, most of the chunk size is 0-500, per 100 chunks like that there's a big chunk size at 20000. I updated the benchmark below

]) {
const bench = new benchmark(id, {
fn: async function () {
crypto.chaCha20Poly1305Decrypt(encrypted, nonceBytes, new Uint8Array(0), key)
}
})
.on('complete', function (stats) {
console.log(String(stats.currentTarget))
})
bench.run()
}

for(const {id, crypto} of [
{id: 'stablelib encrypt', crypto: stablelib},
{id: 'sodium-native encrypt', crypto: sodiumNative}
]) {
const bench = new benchmark(id, {
fn: async function () {
crypto.chaCha20Poly1305Encrypt(data, nonceBytes, new Uint8Array(0), key)
}
})
.on('complete', function (stats) {
console.log(String(stats.currentTarget))
})
bench.run()
}
}

main()
File renamed without changes.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@
]
},
"scripts": {
"bench": "node benchmarks/benchmark.js",
"bench": "node benchmarks/*.js",
"clean": "aegir clean",
"dep-check": "aegir dep-check",
"build": "aegir build",
"prebuild": "mkdirp dist/src && cp -R src/proto dist/src",
"lint": "aegir lint",
"lint:fix": "aegir lint --fix",
"test": "aegir test",
"test:unit": "aegir test -f './dist/test/unit/*.spec.js' -t node",
"test:node": "aegir test -t node",
"test:browser": "aegir test -t browser -t webworker",
"test:electron-main": "aegir test -t electron-main",
Expand All @@ -82,12 +83,14 @@
"it-pb-stream": "^1.0.2",
"it-pipe": "^2.0.3",
"protons-runtime": "^1.0.3",
"sodium-native": "^3.3.0",
"uint8arraylist": "^1.4.0",
"uint8arrays": "^3.0.0"
},
"devDependencies": {
"@libp2p/interface-connection-encrypter-compliance-tests": "^1.0.1",
"@libp2p/peer-id-factory": "^1.0.8",
"@types/sodium-native": "2.3.5",
"aegir": "^37.3.0",
"benchmark": "^2.1.4",
"events": "^3.3.0",
Expand Down
130 changes: 130 additions & 0 deletions src/crypto/sodium-native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/* eslint-disable @typescript-eslint/naming-convention */
import type { bytes, bytes32 } from '../@types/basic.js'
import type { Hkdf } from '../@types/handshake.js'
import type { KeyPair } from '../@types/libp2p.js'
import type { ICryptoInterface } from '../crypto.js'
import sodium from 'sodium-native'

import { concat as uint8ArrayConcat } from 'uint8arrays/concat'

const {
/* @ts-expect-error */
crypto_aead_chacha20poly1305_ietf_decrypt,
/* @ts-expect-error */
crypto_aead_chacha20poly1305_ietf_encrypt,
/* @ts-expect-error */
crypto_aead_chacha20poly1305_ietf_ABYTES,
crypto_box_keypair,
crypto_box_PUBLICKEYBYTES,
crypto_box_SECRETKEYBYTES,
crypto_box_seed_keypair,
crypto_hash_sha256,
crypto_scalarmult,
crypto_scalarmult_BYTES,
sodium_malloc,
sodium_memzero
} = sodium

const hkdfBlockLen = 64
const hkdfHashLen = 32
const hkdfStep1 = new Uint8Array([0x01])
const hkdfStep2 = new Uint8Array([0x02])
const hkdfStep3 = new Uint8Array([0x03])
const hmacBuffer = sodium_malloc(hkdfBlockLen * 3)
const hmacKey = hmacBuffer.subarray(hkdfBlockLen * 0, hkdfBlockLen)
const hmacOuterKeyPad = hmacBuffer.subarray(hkdfBlockLen, hkdfBlockLen * 2)
const hmacInnerKeyPad = hmacBuffer.subarray(hkdfBlockLen * 2, hkdfBlockLen * 3)

/* c8 ignore start */
function hmac (out: Buffer, data: Uint8Array, key: bytes32): void {
if (key.byteLength > hkdfBlockLen) {
crypto_hash_sha256(hmacKey.subarray(0, hkdfHashLen), Buffer.from(key))
sodium_memzero(hmacKey.subarray(hkdfHashLen))
} else {
hmacKey.set(key)
sodium_memzero(hmacKey.subarray(key.byteLength))
}

for (let i = 0; i < hmacKey.byteLength; i++) {
hmacOuterKeyPad[i] = 0x5c ^ hmacKey[i]
hmacInnerKeyPad[i] = 0x36 ^ hmacKey[i]
}

crypto_hash_sha256(out, Buffer.from(uint8ArrayConcat([hmacInnerKeyPad, data])))
sodium_memzero(hmacInnerKeyPad)
crypto_hash_sha256(out, Buffer.from(uint8ArrayConcat([hmacOuterKeyPad, out])))
sodium_memzero(hmacOuterKeyPad)
}

export const sodiumNative: ICryptoInterface = {
hashSHA256 (data: Uint8Array): Uint8Array {
const out = sodium_malloc(32)
crypto_hash_sha256(out, Buffer.from(data))

return out
},
getHKDF (ck: bytes32, ikm: Uint8Array): Hkdf {
// Extract
const prk = sodium_malloc(32)
hmac(prk, ikm, ck)

// Derive
const out = sodium_malloc(hkdfHashLen * 3)
const out1 = out.subarray(0, hkdfHashLen)
const out2 = out.subarray(hkdfHashLen, hkdfHashLen * 2)
const out3 = out.subarray(hkdfHashLen * 2, hkdfHashLen * 3)
hmac(out1, hkdfStep1, prk)
hmac(out2, uint8ArrayConcat([out1, hkdfStep2]), prk)
hmac(out3, uint8ArrayConcat([out2, hkdfStep3]), prk)

return [
out.slice(0, hkdfHashLen),
out.slice(hkdfHashLen, hkdfHashLen * 2),
out.slice(hkdfHashLen * 2, hkdfHashLen * 3)
]
},
generateX25519KeyPair (): KeyPair {
const publicKey = sodium_malloc(crypto_box_PUBLICKEYBYTES)
const privateKey = sodium_malloc(crypto_box_SECRETKEYBYTES)

crypto_box_keypair(publicKey, privateKey)

return { publicKey, privateKey }
},
generateX25519KeyPairFromSeed (seed: Uint8Array): KeyPair {
const publicKey = sodium_malloc(crypto_box_PUBLICKEYBYTES)
const privateKey = sodium_malloc(crypto_box_SECRETKEYBYTES)

crypto_box_seed_keypair(publicKey, privateKey, Buffer.from(seed))

return { publicKey, privateKey }
},
generateX25519SharedKey (privateKey: Uint8Array, publicKey: Uint8Array): Uint8Array {
const shared = sodium_malloc(crypto_scalarmult_BYTES)
crypto_scalarmult(shared, Buffer.from(privateKey), Buffer.from(publicKey))

return shared
},
chaCha20Poly1305Encrypt (plaintext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes {
const out = sodium_malloc(plaintext.length + (crypto_aead_chacha20poly1305_ietf_ABYTES as number))

crypto_aead_chacha20poly1305_ietf_encrypt(out, plaintext, ad, null, nonce, k)

return out
},
chaCha20Poly1305Decrypt (ciphertext: Uint8Array, nonce: Uint8Array, ad: Uint8Array, k: bytes32): bytes | null {
const out = sodium_malloc(ciphertext.length - crypto_aead_chacha20poly1305_ietf_ABYTES)

try {
crypto_aead_chacha20poly1305_ietf_decrypt(out, null, ciphertext, ad, nonce, k)
} catch (error) {
if ((error as Error).message === 'could not verify data') {
return null
}

throw error
}

return out
}
}
26 changes: 26 additions & 0 deletions test/unit/crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { assert } from 'aegir/chai'
import { stablelib } from '../../src/crypto/stablelib.js'
import { sodiumNative } from '../../src/crypto/sodium-native.js'
import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
import type { ICryptoInterface } from '../../src/crypto.js'

describe('Crypto implementation', () => {
const testCases: Array<{id: string, encrypt: ICryptoInterface, decrypt: ICryptoInterface}> = [
{ id: 'sodium-native should be able to decrypt from data encrypted by stablelib', decrypt: sodiumNative, encrypt: stablelib },
{ id: 'sodium-native should be able to decrypt from data encrypted by sodium-native', decrypt: sodiumNative, encrypt: sodiumNative },
{ id: 'stablelib should be able to decrypt from data encrypted by sodium-native', decrypt: stablelib, encrypt: sodiumNative },
{ id: 'stablelib should be able to decrypt from data encrypted by stablelib', decrypt: stablelib, encrypt: stablelib }
]
for (const { id, encrypt, decrypt } of testCases) {
it(id, () => {
const data = Buffer.from('encryptthis')
const nonce = 1000
const nonceBytes = new Uint8Array(12)
new DataView(nonceBytes.buffer, nonceBytes.byteOffset, nonceBytes.byteLength).setUint32(4, nonce, true)
const key = new Uint8Array(Array.from({ length: 32 }, () => 1))
const encrypted = encrypt.chaCha20Poly1305Encrypt(data, nonceBytes, new Uint8Array(0), key)
const decrypted = decrypt.chaCha20Poly1305Decrypt(encrypted, nonceBytes, new Uint8Array(0), key)
assert(uint8ArrayEquals(decrypted as Uint8Array, Buffer.from('encryptthis')))
})
}
})