OverviewFilesCommitsIssuesDocsReleasesPeers

autobase-doctor / node_modules / autobase / lib / encryption.js

385 lines · javascript ref:
const HypercoreEncryption = require('hypercore/lib/default-encryption.js')
const sodium = require('sodium-universal')
const crypto = require('hypercore-crypto')
const c = require('compact-encoding')
const b4a = require('b4a')
const rrp = require('resolve-reject-promise')
const ReadyResource = require('ready-resource')

const SystemView = require('./system.js')
const { EncryptionDescriptor, ManifestData } = require('./messages.js')
const { NS_VIEW_BLOCK_KEY, NS_HASH_KEY, NS_ENCRYPTION } = require('./caps.js')

const nonce = b4a.alloc(sodium.crypto_stream_NONCEBYTES)
const hash = nonce.subarray(0, sodium.crypto_generichash_BYTES_MIN)

class AutobaseEncryption {
  static PADDING = 8

  constructor(encryption) {
    this.encryption = encryption

    this.compat = null
    this.keys = null
    this.keysById = new Map()
  }

  get id() {
    return this.keys ? this.keys.id : 0
  }

  padding() {
    return AutobaseEncryption.PADDING
  }

  isCompat() {
    return false
  }

  load(keys) {
    if (this.keys === null) this.keys = keys
  }

  async update(ctx) {
    if (this.id !== 0 && this.id === this.encryption.id) return
    const keys = await this.get(this.encryption.id, ctx)
    if (keys) this.keys = keys
  }

  async get(id, ctx) {
    if (this.keysById.has(id)) return this.keysById.get(id)

    const keys = await this.getKeys(id, ctx)
    this.keysById.set(id, keys)

    return keys
  }

  async getKeys(id, ctx) {
    const entropy = await this.encryption.get(id)
    if (!entropy) return null

    const block = this.blockKey(entropy, ctx)
    const hash = crypto.hash([NS_HASH_KEY, block])

    return {
      id,
      block,
      hash
    }
  }

  blockKey(entropy, ctx) {
    return this.encryption.blockKey(entropy, ctx)
  }

  async _ensureCompat(ctx) {
    if (!this.compat) this.compat = this.compatKeys(ctx)
  }

  compatKeys() {
    throw new Error('Compatibility method is not specified')
  }

  async encrypt(index, block, fork, ctx) {
    if (this.isCompat(ctx, index)) {
      this._ensureCompat(ctx)
      return HypercoreEncryption.encrypt(
        index,
        block,
        fork,
        this.compat.block,
        this.compat.blinding
      )
    }

    await this.update(ctx)

    encryptBlock(index, block, this.keys.id, this.keys.block, this.keys.hash)
  }

  async decrypt(index, block, ctx) {
    if (this.isCompat(ctx, index)) {
      this._ensureCompat(ctx)
      return HypercoreEncryption.decrypt(index, block, this.compat.block)
    }

    const padding = block.subarray(0, AutobaseEncryption.PADDING)
    block = block.subarray(AutobaseEncryption.PADDING)

    const type = padding[0]
    switch (type) {
      case 0:
        return block // unencrypted

      case 1:
        break

      default:
        throw new Error('Unrecognised encryption type')
    }

    const id = c.uint32.decode({ start: 4, end: 8, buffer: padding })

    const keys = await this.get(id, ctx)

    c.uint64.encode({ start: 0, end: 8, buffer: nonce }, index)
    nonce.set(padding, 8, 16)

    // Decrypt the block using the full nonce
    decrypt(block, nonce, keys.block)
  }
}

class ViewEncryption extends AutobaseEncryption {
  constructor(encryption, name) {
    super(encryption)
    this.name = name
  }

  isCompat(ctx, index) {
    if (ctx.manifest.version <= 1) return true
    if (!ctx.manifest.userData) return false
    const { legacyBlocks } = c.decode(ManifestData, ctx.manifest.userData)
    return index < legacyBlocks
  }

  compatKeys() {
    const { bootstrap, encryptionKey } = this.encryption.base
    const block = getCompatBlockKey(bootstrap, encryptionKey, this.name)
    return {
      block,
      blinding: crypto.hash(block)
    }
  }

  blockKey(entropy) {
    return getCompatBlockKey(this.encryption.base.bootstrap, entropy, this.name)
  }
}

class WriterEncryption extends AutobaseEncryption {
  isCompat(ctx) {
    return ctx.manifest.version <= 1
  }

  compatKeys(ctx) {
    return HypercoreEncryption.deriveKeys(this.encryption.base.encryptionKey, ctx.key)
  }

  blockKey(entropy, ctx) {
    if (ctx.manifest.userData) {
      const userData = c.decode(ManifestData, ctx.manifest.userData)
      if (userData.namespace !== null) {
        return this.encryption.blockKey(entropy, { key: userData.namespace })
      }
    }

    return this.encryption.blockKey(entropy, ctx)
  }
}

class EncryptionView extends ReadyResource {
  constructor(base, core) {
    super()

    this.base = base
    this.core = core || null

    this.sessions = new Map()

    this._initialising = null
  }

  async _open() {
    await this.initialised()
    await this.core.ready()
  }

  initialised() {
    if (this.core !== null) return this.core.ready()
    if (this._initialising) return this._initialising

    this._initialising = rrp()

    return this._initialising.promise
  }

  _close() {
    if (this._initialising) {
      this._initialising.reject(new Error('Encryption closed'))
      this._initialising = null
    }

    if (this.core) return this.core.close()
  }

  get bootstrapped() {
    return !!(this.core && this.core.length > 0)
  }

  get id() {
    return this.core ? this.core.length : 0
  }

  _createPayload(key) {
    return key
  }

  async reload(core) {
    if (this.core) await this.core.close()

    this.core = core
    await this.core.ready()

    if (this._initialising) {
      this._initialising.resolve()
      this._initialising = null
    }
  }

  unpack(type, payload) {
    if (type > 0) throw new Error('Unsupported version')
    return payload
  }

  async update(key) {
    const payload = await this._createPayload(key)
    const desc = { type: 0, payload }

    await this.core.append(c.encode(EncryptionDescriptor, desc))
  }

  async _preload() {
    await this.ready()
    return this.id()
  }

  getViewEncryption(name) {
    if (this.sessions.has(name)) return this.sessions.get(name)

    const encryption = new ViewEncryption(this, name)
    this.sessions.set(name, encryption)

    return encryption
  }

  getWriterEncryption() {
    return new WriterEncryption(this)
  }

  async encryptAnchor(block, namespace) {
    const entropy = await this.get(this.id)
    if (!entropy) return null

    const blockKey = this.blockKey(entropy, { key: namespace })
    const hashKey = crypto.hash([NS_HASH_KEY, block])

    encryptBlock(0, block, this.id, blockKey, hashKey)
  }

  blockKey(entropy, ctx) {
    return getBlockKey(this.base.bootstrap, this.base.encryptionKey, entropy, ctx.key)
  }

  async get(encryptionId) {
    if (encryptionId === 0) return SystemView.GENESIS_ENTROPY

    if (!this.core) await this.initialised()

    const index = encryptionId - 1
    const desc = await this.core.get(index)
    const { type, payload } = c.decode(EncryptionDescriptor, desc)

    const key = this.unpack(type, payload)
    return key
  }

  getSystemEncryption() {
    return this.getViewEncryption('_system')
  }

  static namespace(entropy) {
    return crypto.hash([NS_ENCRYPTION, entropy])
  }

  static getBlockKey(bootstrap, encryptionKey, entropy, hypercoreKey) {
    return getBlockKey(bootstrap, encryptionKey, entropy, hypercoreKey)
  }

  static getSystemEncryption(base, core) {
    const view = new EncryptionView(base, core)
    return view.getSystemEncryption()
  }

  static async setSystemEncryption(base, core, opts) {
    if (base.encryptionKey === null) return null

    await core.ready()
    if (core.manifest.version === 1) {
      const key = getCompatBlockKey(base.bootstrap, base.encryptionKey, '_system')
      return core.setEncryptionKey(key, { block: true })
    }

    if (!core.manifest.linked.length) {
      throw new Error('System manifest does not link encryption view')
    }

    const enc = base.store.get({ key: core.manifest.linked[0], active: false })
    const encryption = EncryptionView.getSystemEncryption(base, enc)

    await core.setEncryption(encryption, opts)

    return enc
  }
}

module.exports = {
  AutobaseEncryption,
  EncryptionView
}

function encrypt(block, nonce, key) {
  sodium.crypto_stream_xor(block, block, nonce, key)
}

function decrypt(block, nonce, key) {
  return encrypt(block, nonce, key) // symmetric
}

function getBlockKey(bootstrap, encryptionKey, entropy, hypercoreKey) {
  return (
    encryptionKey &&
    crypto.hash([NS_VIEW_BLOCK_KEY, bootstrap, encryptionKey, entropy, hypercoreKey])
  )
}

function getCompatBlockKey(bootstrap, encryptionKey, name) {
  if (typeof name === 'string') return getCompatBlockKey(bootstrap, encryptionKey, b4a.from(name))
  return encryptionKey && crypto.hash([NS_VIEW_BLOCK_KEY, bootstrap, encryptionKey, name])
}

function blockhash(block, padding, hashKey) {
  sodium.crypto_generichash(hash, block, hashKey)
  padding.set(hash.subarray(0, 8)) // copy first 8 bytes of hash
  hash.fill(0) // clear nonce buffer
}

function encryptBlock(index, block, id, blockKey, hashKey) {
  const padding = block.subarray(0, AutobaseEncryption.PADDING)
  block = block.subarray(AutobaseEncryption.PADDING)

  blockhash(block, padding, hashKey)
  c.uint32.encode({ start: 4, end: 8, buffer: padding }, id)

  c.uint64.encode({ start: 0, end: 8, buffer: nonce }, index)

  padding[0] = 1 // version in plaintext

  nonce.set(padding, 8, 16)

  // The combination of index, key id, fork id and block hash is very likely
  // to be unique for a given Hypercore and therefore our nonce is suitable
  encrypt(block, nonce, blockKey)
}