// Encryption — hypercore encrypts ABOVE storage, so the bytes reaching putBlock
// are ciphertext with 8 bytes of padding, not our chunk bytes. Expected outcome:
// still correct, but nothing virtualises. This measures how completely.
// node test/encryption.mjs <workDir>
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const fs = require('fs')
const { execFileSync } = require('child_process')
const crypto = require('hypercore-crypto')
const Corestore = require('corestore')
const Hyperdrive = require('hyperdrive')
const CorestoreStorage = require('hypercore-storage')
const DefaultEncryption = require('hypercore/lib/default-encryption')
const { ContentStore, build, stats } = require('../lib/storage.cjs')
const { chunk } = await import('../../hyper-overlay/lib/chunker.js')
const ROOT = process.argv[2]
const PRIMARY = Buffer.alloc(32, 7)
const ENCRYPTION_KEY = Buffer.alloc(32, 42)
const du = (p) => Number(execFileSync('du', ['-sb', p]).toString().split('\t')[0])
async function compactedSize (dir) {
const s = new CorestoreStorage(dir)
await s.ready()
await s.compact()
await s.close()
return du(dir)
}
const srcDir = ROOT + '/src'
fs.mkdirSync(srcDir, { recursive: true })
const payload = crypto.randomBytes(8 * 1024 * 1024)
const file = srcDir + '/payload.bin'
fs.writeFileSync(file, payload)
const chunks = chunk(payload)
const content = new ContentStore()
for (const c of chunks) content.add(c.hash, file, c.offset, c.length)
function write (ws, buf) {
return new Promise((resolve) => {
if (ws.write(buf)) return resolve()
ws.once('drain', resolve)
})
}
async function run (dir, { encrypted }) {
const before = { ...stats }
const Storage = build(content)
const store = new Corestore(new Storage(dir), { primaryKey: PRIMARY, unsafe: true })
const drive = new Hyperdrive(store, encrypted ? { encryptionKey: ENCRYPTION_KEY } : {})
await drive.ready()
const ws = drive.createWriteStream('/payload.bin', { dedup: true })
for (const c of chunks) await write(ws, c.data)
await new Promise((resolve, reject) => {
ws.once('error', reject)
ws.once('close', resolve)
ws.end()
})
await drive.getBlobs()
const got = await drive.get('/payload.bin')
const out = {
driveKey: drive.key.toString('hex'),
blobsLength: drive.blobs.core.length,
readbackOk: got !== null && got.equals(payload),
redirected: stats.putRedirected - before.putRedirected,
literal: stats.putStored - before.putStored
}
await drive.close()
await store.close()
out.storeBytes = await compactedSize(dir)
return out
}
const plain = await run(ROOT + '/plain', { encrypted: false })
const enc = await run(ROOT + '/encrypted', { encrypted: true })
// Is the cipher deterministic? If so, an adapter given the key could re-derive
// ciphertext from plaintext on read, and virtualisation would work encrypted too.
const keys = DefaultEncryption.deriveKeys(ENCRYPTION_KEY, Buffer.alloc(32, 1), {})
function encryptOnce (plaintext, index, fork) {
const block = Buffer.concat([Buffer.alloc(DefaultEncryption.PADDING), plaintext])
DefaultEncryption.encrypt(index, block, fork, keys.block, keys.blinding)
return block
}
const sample = payload.subarray(0, 4096)
const a = encryptOnce(sample, 7, 0)
const b = encryptOnce(sample, 7, 0)
const c = encryptOnce(sample, 8, 0)
content.close()
console.log(
JSON.stringify(
{
plain,
encrypted: enc,
sameDriveKey: plain.driveKey === enc.driveKey,
virtualisationLostWhenEncrypted: enc.redirected === 0,
cipher: {
deterministicSameIndex: a.equals(b),
differsAcrossIndex: !a.equals(c),
paddingBytes: DefaultEncryption.PADDING
}
},
null,
2
)
)