Source: algs/ecies_bie1.js

import { Buffer } from 'buffer'
import { Bn, Point, PubKey } from 'bsv'
import crypto from 'isomorphic-webcrypto'
import Key from '../key'
import { fromBsvPubKey } from '../util'


/**
 * ECIES_BIE1 module. Implements Electrum-flavoured ECIES encryption and
 * decryption.
 */
const ECIES_BIE1 = {
  /**
   * Decrypts the cyphertext with the key using the specified algorithm.
   * 
   * Accepted options:
   * 
   * * `epk` - Ephemeral public key
   * * `apu` - Agreement PartyUInfo
   * * `apv` - Agreement PartyVInfo
   * * Any accepted AES_GCM options
   * 
   * @param {String} alg
   * @param {Buffer} encrypted
   * @param {Key} key
   * @param {Object} opts
   * @returns {Buffer}
   */
  async decrypt(alg, encrypted, key, opts = {}) {
    assertKey(key, alg)

    const len = encrypted.length - 69,
          prefix = encrypted.slice(0, 4),
          ephemeralPubKeyBuf = encrypted.slice(4, 4 + 33),
          cipher = encrypted.slice(4 + 33, 4 + 33 + len),
          mac = encrypted.slice(4 + 33 + len);

    if (prefix.toString() !== 'BIE1') throw 'invalid magic bytes';

    // Derive ECDH key and sha512 hash
    const ephemeralPubKey = fromBsvPubKey(PubKey.fromDer(ephemeralPubKeyBuf))
    const keyHash = await crypto.subtle.digest('SHA-512', computeSharedSecret(key, ephemeralPubKey))

    // iv and keyE used in AES, keyM used in HMAC
    const iv = keyHash.slice(0, 16),
          keyE = keyHash.slice(16, 32),
          keyM = keyHash.slice(32);

    // HMAC validation
    const macKey = await crypto.subtle.importKey('raw', keyM, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])
    const verified = await crypto.subtle.verify({ name: 'HMAC' }, macKey, mac, encrypted.slice(0, -32))
    if (!verified) throw 'mac validation failed';

    // Decrypt cyphertext
    const encKey = await crypto.subtle.importKey('raw', keyE, 'AES-CBC', false, ['decrypt'])
    const result = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: iv }, encKey, cipher)
    return Buffer.from(result)
  },

  /**
   * Encrypts the message with the key using the specified algorithm. Returns
   * an object containing the encrypted cyphertext and any headers to add to
   * the Recipient.
   * 
   * @param {String} alg
   * @param {Buffer|String} msg
   * @param {Key} key
   * @param {Object} opts
   * @returns {Object}
   */
  async encrypt(alg, msg, key, _opts = {}) {
    assertKey(key, alg)

    // Derive ECDH key and sha512 hash
    const ephemeralKey = await Key.generate('ec', 'secp256k1')
    const keyHash = await crypto.subtle.digest('SHA-512', computeSharedSecret(ephemeralKey, key))

    // iv and keyE used in AES, keyM used in HMAC
    const iv = keyHash.slice(0, 16),
          keyE = keyHash.slice(16, 32),
          keyM = keyHash.slice(32);
    
    // Create ciphertext
    const encKey = await crypto.subtle.importKey('raw', keyE, 'AES-CBC', false, ['encrypt'])
    const cipher = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, encKey, Buffer.from(msg))
    
    // Concat encrypted data with hmac
    const macKey = await crypto.subtle.importKey('raw', keyM, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])
    const macData = Buffer.concat([
      Buffer.from('BIE1'),
      pubKeyBuf(ephemeralKey),
      Buffer.from(cipher)
    ])
    const mac = await crypto.subtle.sign({ name: 'HMAC' }, macKey, macData)

    return {
      encrypted: Buffer.concat([macData, Buffer.from(mac)])
    }
  }
}

/**
 * Asserts the key is valid.
 * 
 * @param {Key} key 
 * @param {String} alg
 */
function assertKey(key, _alg) {
  if (key.type !== 'EC' || key.params.crv !== 'secp256k1') {
    throw `Invalid key for ECIES-BIE1 algorithm`
  }
}

/**
 * Computes and returns a ECDH shared secret from the given keys.
 * 
 * @param {Key} privKey
 * @param {Key} pubKey
 * @returns {Buffer}
 */
function computeSharedSecret(privKey, pubKey) {
  const x = Bn.fromBuffer(pubKey.params.x),
        y = Bn.fromBuffer(pubKey.params.y),
        d = Bn.fromBuffer(privKey.params.d),
        p = new Point(x, y),
        s = p.mul(d);

  return Buffer.concat([
    Buffer.from([s.y.isOdd() ? 0x03 : 0x02]),
    Buffer.from(s.x.toArray('big', 32))
  ])
}

/**
 * Converts a Key to a Buffer
 * 
 * @param {Key} key
 * @returns {Buffer}
 */
function pubKeyBuf(key) {
  const x = Bn.fromBuffer(key.params.x),
        y = Bn.fromBuffer(key.params.y),
        p = new Point(x, y),
        pubKey = new PubKey(p);

  return pubKey.toBuffer()
}

export default ECIES_BIE1