dark-crystal-web3 0.1.2

Simple CLI for backing up and recovering secrets using Dark Crystal Web3
#!/usr/bin/env node
const argv = require('minimist')(process.argv.slice(2))
const sodium = require('sodium-javascript')
const assert = require('assert')

// Decrypt shares: give one or more secret keys (--sk),
// a public key --pk
// and shares (as positional arguments)
if (argv.sk && argv.pk) {
  console.log('Attempting to decrypt shares', argv._)
  if (!Array.isArray(argv.sk)) argv.sk = [argv.sk]
  let shareSet = ''
  for (const sk of argv.sk) {
    const plaintext = attemptDecryptShareSet(argv._, argv.pk, sk)
    console.log(plaintext
      ? 'Decrypted share!'
      : 'Decryption failed'
    )
    shareSet += `${plaintext.toString('hex')} `
  }
  console.log(shareSet)
} else {
  // Otherwise generate some keypairs
  const numKeypairs = argv.keypairs || 3
  console.log(`Generating ${numKeypairs} keypairs`)
  let pkString = ''
  let skString = ''
  for (let i = 0; i < numKeypairs; i++) {
    const { publicKey, secretKey } = generateKeypair()
    console.log(` Public key: ${publicKey.toString('hex')}`)
    console.log(` Secret key: ${secretKey.toString('hex')}\n`)
    pkString += `--pk ${publicKey.toString('hex')} `
    skString += `--sk ${secretKey.toString('hex')} `
  }
  console.log(pkString, '\n')
  console.log(skString)
}

/**
 * Generate a Curve25519 encryption keypair
 */
function generateKeypair (seed) {
  seed = seed || generateSeed()

  const publicKey = Buffer.alloc(sodium.crypto_box_PUBLICKEYBYTES)
  const secretKey = Buffer.alloc(sodium.crypto_box_SECRETKEYBYTES)

  sodium.crypto_box_seed_keypair(publicKey, secretKey, seed)
  return { publicKey, secretKey }

  function generateSeed () {
    const seed = Buffer.alloc(sodium.crypto_box_SEEDBYTES)
    sodium.randombytes_buf(seed)
    return seed
  }
}

/**
 * Attempt to decrypt a given ciphertext
 */
function decrypt (ciphertextWithNonce, publicKey, secretKey) {
  assert(ciphertextWithNonce.length > sodium.crypto_box_MACBYTES + sodium.crypto_box_NONCEBYTES, 'cipherText too short')
  const nonce = ciphertextWithNonce.slice(0, sodium.crypto_box_NONCEBYTES)
  const ciphertext = ciphertextWithNonce.slice(sodium.crypto_box_NONCEBYTES)
  const plain = Buffer.alloc(ciphertext.length - sodium.crypto_box_MACBYTES)
  const decrypted = sodium.crypto_box_open_easy(plain, ciphertext, nonce, publicKey, secretKey)
  if (!decrypted) throw new Error('Decryption failed')
  return plain
}

/**
 * Given an array of encrypted shares, return either a successful
 * decryption or undefined if none could be decrypted
 */
function attemptDecryptShareSet (shareSet, publicKey, secretKey) {
  for (const share of shareSet) {
    try {
      return decrypt(
        Buffer.from(share, 'hex'),
        Buffer.from(publicKey, 'hex'),
        Buffer.from(secretKey, 'hex')
      )
    } catch (e) {
      continue
    }
  }
}