OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-vdrive / test / encryption.mjs

113 lines · javascript ref:
// 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
  )
)