Source: src/memo.js

/**
 * NanoMemoTools.memo module
 * @module NanoMemoTools/memo
 */

const version = require('./version');
const tools = require('./tools');
const node = require('./node');
const NanoCurrency = require('nanocurrency');

/**
* This function validates a given message
* @public
* @param {string} message message to validate
* @param {number} [maxlength=512] configurable maxlength of message
* @returns {boolean} true for validated message, false otherwise
*/
const validateMessage = function(message, maxlength=512) {
  try {
    message = message.toString('ascii');
    if (message.length > maxlength) return false;
    return true;
  } catch(e) {
    return false;
  }
}
module.exports.validateMessage = validateMessage;

/**
* This function validates a given signature
* @public
* @param {string} signature 128-hex string representing a signature
* @returns {boolean} true for validated signature, false otherwise
*/
const validateSignature = function(signature) {
  try {
      if (signature.length != 128) return false;
      return true;
  } catch(e) {
      return false;
  }
}
module.exports.validateSignature = validateSignature;

/**
* This function validates a given key (public or private)
* @public
* @param {string} key 64-hex string representing a key
* @returns {boolean} true for validated key, false otherwise
*/
const validateKey = function(key) {
  try {
      return NanoCurrency.checkKey(key);
  } catch(e) {
      return false;
  }
}
module.exports.validateKey = validateKey;

/**
* This function validates a given address
* @public
* @param {string} address Nano address
* @returns {boolean} true for validated address, false otherwise
*/
const validateAddress = function(address) {
  try {
      return NanoCurrency.checkAddress(address);
  } catch(e) {
      return false;
  }
}
module.exports.validateAddress = validateAddress;

/**
* This function validates a given hash
* @public
* @param {string} hash 64-hex string representing a Nano block hash
* @returns {boolean} true for validated hash, false otherwise
*/
const validateHash = function(hash) {
  try {
      return NanoCurrency.checkHash(hash);
  } catch(e) {
      return false;
  }
}
module.exports.validateHash = validateHash;

/**
 * This function validates one or more Memos against the Nano Network; No username/password required if connecting to DEFAULT_SERVER or public API
 * @param {array} memos Array of Memos to validate against the Nano Network
 * @param {string} [url=node.DEFAULT_SERVER] url of Nano Node RPC
 * @param {string} [username=undefined] username for Nano Node RPC authentication
 * @param {string} [password=password] password for Nano Node RPC authentication
 * @returns {object} { valid: [<array of hashes>], invalid: [<array of hashes>], not_found: [<array of hashes>] }; returns undefined on error
 */
 const nodeValidated = async function(memos, url=node.DEFAULT_SERVER, username=undefined, password=undefined) {

  let ret = {
    valid: [],
    invalid: [],
    not_found: []
  }

  let local_valid_memos = [];
  let local_invalid_memos = [];
  for (let memo of memos) {
    if (memo.valid_signature) local_valid_memos.push(memo);
    else local_invalid_memos.push(memo.hash);
  }

  // Return early if there are no valid memos to check
  // if (local_valid_memos.length == 0) return ret;

  // Query node
  const response = await node.blocks_info(memos.map(memo => memo.hash), url, username, password).catch(function(e) {
    console.error('In memo.nodeValidated, an error was caught running node.blocks_info');
    console.error(memos.map(memo => memo.hash));
    console.error(e);
    return undefined;
  });
  if (!response || (!response.blocks && !response.blocks_not_found)) {
    // Invalid response
    console.error('In memo.nodeValidate, no response was received from node.blocks_info');
    return undefined;
  }
  if (response.blocks == '') response.blocks = {};
  if (response.blocks_not_found == '') response.blocks_not_found = {};

  // Memo has already been validated with the signing_address
  // Don't compare addresses because nano_ or xrb_ prefixes may not match, so convert to public key
  //  and compare to be same
  for (let memo of memos) {

    if (response.blocks[memo.hash] !== undefined) {
      // Block exists exists on the Nano Network
      const block = response.blocks[memo.hash];
      const block_signing_public_key = tools.getPublicKeyFromAddress(block.block_account);
      if (memo.valid_signature && memo.signing_public_key.toUpperCase() == block_signing_public_key.toUpperCase()) {
        // Memo is valid and matches keys with the block
        ret.valid.push(memo.hash);
      } else {
        ret.invalid.push(memo.hash);
      }
    } else {
      ret.not_found.push(memo.hash);
    }
  }

  return ret;
}
module.exports.nodeValidated = nodeValidated;

/**
* This function converts a Memo into an EncryptedMemo Object
* @public
* @param {Memo} memo Memo Object to convert
* @param {string} signing_private_key 64-hex string representing a Nano Account's private key
* @param {string} decrypting_address Nano address whose will be able to decrypt the memo
* @param {number} [version_encrypt=undefined] version of encryption algorithm - Versioning not yet implemented
* @returns {EncryptedMemo} EncryptedMemo Object with the message encrypted
*/
module.exports.encrypt = function(memo, signing_private_key, decrypting_address, version_encrypt=undefined) {

  // Validate inputs
  if (!validateKey(signing_private_key)) {
    throw new TypeError('Invalid signing_private_key');
  }
  if (!validateAddress(decrypting_address)) {
    throw new TypeError('Invalid decrypting_address');
  }
  const decrypting_public_key = tools.getPublicKeyFromAddress(decrypting_address);

  // The hash is used as the nonce for encryption
  const encrypted_message = tools.encryptMessage(
    memo.message,
    memo.hash,
    decrypting_public_key,
    signing_private_key,
    version_encrypt
  );
  
  // Clear signature as message has changed
  const encrypted_memo = new EncryptedMemo(
    memo.hash,
    encrypted_message,
    memo.signing_address,
    decrypting_address,
    undefined,
    memo.version_sign,
    version_encrypt
  );
  
  return encrypted_memo;
}

/**
* This function converts an EncryptedMemo into a Memo Object
* @public
* @param {EncryptedMemo} encrypted_memo EncryptedMemo Object to convert
* @param {string} decrypting_private_key 64-hex string representing a Nano Account's private key
* @returns {Memo} Memo Object with the message as plaintext
*/
module.exports.decrypt = function(encrypted_memo, decrypting_private_key) {

  // The hash is used as the nonce for encryption
  const decrypted_message = tools.decryptMessage(
    encrypted_memo.message,
    encrypted_memo.hash,
    encrypted_memo.signing_public_key,
    decrypting_private_key,
    encrypted_memo.version_encrypt
  );

  // Clear signature as message has changed
  const decrypted_memo = new Memo(
    encrypted_memo.hash,
    decrypted_message,
    encrypted_memo.signing_address,
    undefined,
    encrypted_memo.version_sign
  );
  
  return decrypted_memo;
}

/** Class representing a Memo (with plaintext message) */
class Memo {

  /**
   * Creates a Memo
   * @param {string} hash 64-hex string representing a Nano block hash
   * @param {string} message message of memo
   * @param {string} signing_address Nano address that owns block with hash
   * @param {string} [signature=undefined] 128-hex string signature of memo
   * @param {number} [version_sign=version.sign] version of signing algorithm - Versioning not yet implemented
   */
  constructor (hash, message, signing_address, signature=undefined, version_sign=version.sign) {
    this.message = undefined;
    this.hash = undefined;
    this.signing_address = undefined;
    this.signature = undefined;
    this.version_sign = undefined;

    // Validate inputs
    if (validateHash(hash)) {
      this.hash = hash;
    } else {
      throw new TypeError('Invalid hash parameter');
    }

    if (validateMessage(message)) {
      this.message = message;
    } else {
      throw new TypeError('Invalid message parameter');
    }

    if (validateAddress(signing_address)) {
      this.signing_address = signing_address;
    } else {
      throw new TypeError('Invalid signing_address parameter');
    }

    if (signature) {  // Optional argument
      if (validateSignature(signature)) {
        this.signature = signature;
      } else {
        throw new TypeError('Invalid signature parameter');
      }
    }

    this.version_sign = version_sign;
  }

  /**
   * Getter for signing_public_key
   * @returns {string} value of signing public_key, derived from signing_address
   */
  get signing_public_key() {
    return tools.getPublicKeyFromAddress(this.signing_address);
  }

  /**
   * Getter for valid_signature
   * @returns {boolean} True if signature is valid, false otherwise
   */
  get valid_signature() {
    if (!this.signature) return false;

    // Signed buffer is concatenation of the message and the hash
    const buffer = this.message + this.hash;
    return tools.verify(buffer, this.signing_public_key, this.signature);
  }

  /**
   * Getter for is_encrypted
   * @returns {boolean} True if memo is encrypted, false otherwise
   */
  get is_encrypted() {
    return false;
  }

  /**
   * Calculates and signs the memo
   * @param {string} signing_private_key 64-hex private key of Nano Account that owns the memo
   * @param {number} [version_sign=undefined] version of signing algorithm - Versioning not yet implemented
   * @returns {string} 128-hex signature
   */
  sign(signing_private_key, version_sign=undefined) {

    // Update sign version
    if (version_sign !== undefined) this.version_sign = version_sign;

    // Validate inputs
    if (!validateKey(signing_private_key)) {
      throw new TypeError('Invalid signing_private_key parameter');
    }

    // Signed buffer is concatenation of the message and the hash
    const buffer = this.message + this.hash;
    this.signature = tools.sign(buffer, signing_private_key);
    return this.signature;
  }

}
module.exports.Memo = Memo;

/** Class representing an EncryptedMemo (with ciphertext message)
 * @extends Memo
*/
class EncryptedMemo extends Memo {

  /**
   * Creates an EncryptedMemo
   * @param {string} hash 64-hex string representing a Nano block hash
   * @param {string} encrypted_message encrypted message of memo
   * @param {string} signing_address Nano address that owns block with hash
   * @param {string} decrypting_address Nano address that will be able to decrypt and read the message
   * @param {string} [signature=undefined] 128-hex string signature of memo
   * @param {number} [version_sign=version.sign] version of signing algorithm - Versioning not yet implemented
   * @param {number} [version_encrypt=undefined] version of encryption algorithm - Versioning not yet implemented
   */
  constructor(hash, encrypted_message, signing_address, decrypting_address, signature=undefined, version_sign=version.sign, version_encrypt=undefined) {
    super(hash, encrypted_message, signing_address, signature, version_sign);
    this.decrypting_address = undefined;
    this.version_encrypt = undefined;

    if (validateAddress(decrypting_address)) {
      this.decrypting_address = decrypting_address;
    } else {
      throw new TypeError('Invalid decrypting_address parameter');
    }

    this.version_encrypt = version_encrypt;
  }

  /**
   * Getter for is_encrypted
   * @returns {boolean} True if memo is encrypted, false otherwise
   */
  get is_encrypted() {
    return true;
  }

}
module.exports.EncryptedMemo = EncryptedMemo;