lib/ecc/src/aes.js
// https://code.google.com/p/crypto-js
import AES from "crypto-js/aes";
import encHex from "crypto-js/enc-hex";
import encBase64 from "crypto-js/enc-base64";
import assert from "assert";
import {sha256, sha512} from "./hash";
const Buffer = require("safe-buffer").Buffer;
/** Provides symetric encrypt and decrypt via AES. */
class Aes {
/** @private */
constructor(iv, key) {
(this.iv = iv), (this.key = key);
}
/** This is an excellent way to ensure that all references to Aes can not operate anymore (example: a wallet becomes locked). An application should ensure there is only one Aes object instance for a given secret `seed`. */
clear() {
return (this.iv = this.key = undefined);
}
/** @arg {string} seed - secret seed may be used to encrypt or decrypt. */
static fromSeed(seed) {
if (seed === undefined) {
throw new Error("seed is required");
}
var _hash = sha512(seed);
_hash = _hash.toString("hex");
// DEBUG console.log('... fromSeed _hash',_hash)
return Aes.fromSha512(_hash);
}
/** @arg {string} hash - A 128 byte hex string, typically one would call {@link fromSeed} instead. */
static fromSha512(hash) {
assert.equal(
hash.length,
128,
`A Sha512 in HEX should be 128 characters long, instead got ${
hash.length
}`
);
var iv = encHex.parse(hash.substring(64, 96));
var key = encHex.parse(hash.substring(0, 64));
return new Aes(iv, key);
}
static fromBuffer(buf) {
assert(Buffer.isBuffer(buf), "Expecting Buffer");
assert.equal(
buf.length,
64,
`A Sha512 Buffer should be 64 characters long, instead got ${
buf.length
}`
);
return Aes.fromSha512(buf.toString("hex"));
}
/**
@throws {Error} - "Invalid Key, ..."
@arg {PrivateKey} private_key - required and used for decryption
@arg {PublicKey} public_key - required and used to calcualte the shared secret
@arg {string} [nonce = ""] optional but should always be provided and be unique when re-using the same private/public keys more than once. This nonce is not a secret.
@arg {string|Buffer} message - Encrypted message containing a checksum
@return {Buffer}
*/
static decrypt_with_checksum(
private_key,
public_key,
nonce,
message,
legacy = false
) {
// Warning: Do not put `nonce = ""` in the arguments, in es6 this will not convert "null" into an emtpy string
if (nonce == null)
// null or undefined
nonce = "";
if (!Buffer.isBuffer(message)) {
message = new Buffer(message, "hex");
}
var S = private_key.get_shared_secret(public_key, legacy);
// D E B U G
// console.log('decrypt_with_checksum', {
// priv_to_pub: private_key.toPublicKey().toString(),
// pub: public_key.toPublicKeyString(),
// nonce: nonce,
// message: message.length,
// S: S.toString('hex')
// })
var aes = Aes.fromSeed(
Buffer.concat([
// A null or empty string nonce will not effect the hash
Buffer.from("" + nonce),
Buffer.from(S.toString("hex"))
])
);
var planebuffer = aes.decrypt(message);
if (!(planebuffer.length >= 4)) {
throw new Error("Invalid key, could not decrypt message(1)");
}
// DEBUG console.log('... planebuffer',planebuffer)
var checksum = planebuffer.slice(0, 4);
var plaintext = planebuffer.slice(4);
// console.log('... checksum',checksum.toString('hex'))
// console.log('... plaintext',plaintext.toString())
var new_checksum = sha256(plaintext);
new_checksum = new_checksum.slice(0, 4);
new_checksum = new_checksum.toString("hex");
if (!(checksum.toString("hex") === new_checksum)) {
throw new Error("Invalid key, could not decrypt message(2)");
}
return plaintext;
}
/** Identical to {@link decrypt_with_checksum} but used to encrypt. Should not throw an error.
@return {Buffer} message - Encrypted message which includes a checksum
*/
static encrypt_with_checksum(private_key, public_key, nonce, message) {
// Warning: Do not put `nonce = ""` in the arguments, in es6 this will not convert "null" into an emtpy string
if (nonce == null)
// null or undefined
nonce = "";
if (!Buffer.isBuffer(message)) {
message = new Buffer(message, "binary");
}
var S = private_key.get_shared_secret(public_key);
// D E B U G
// console.log('encrypt_with_checksum', {
// priv_to_pub: private_key.toPublicKey().toString()
// pub: public_key.toPublicKeyString()
// nonce: nonce
// message: message.length
// S: S.toString('hex')
// })
var aes = Aes.fromSeed(
Buffer.concat([
// A null or empty string nonce will not effect the hash
Buffer.from("" + nonce),
Buffer.from(S.toString("hex"))
])
);
// DEBUG console.log('... S',S.toString('hex'))
var checksum = sha256(message).slice(0, 4);
var payload = Buffer.concat([checksum, message]);
// DEBUG console.log('... payload',payload.toString())
return aes.encrypt(payload);
}
/** @private */
_decrypt_word_array(cipher) {
// https://code.google.com/p/crypto-js/#Custom_Key_and_IV
// see wallet_records.cpp master_key::decrypt_key
return AES.decrypt({ciphertext: cipher, salt: null}, this.key, {
iv: this.iv
});
}
/** @private */
_encrypt_word_array(plaintext) {
//https://code.google.com/p/crypto-js/issues/detail?id=85
var cipher = AES.encrypt(plaintext, this.key, {iv: this.iv});
return encBase64.parse(cipher.toString());
}
/** This method does not use a checksum, the returned data must be validated some other way.
@arg {string} ciphertext
@return {Buffer} binary
*/
decrypt(ciphertext) {
if (typeof ciphertext === "string") {
ciphertext = new Buffer(ciphertext, "binary");
}
if (!Buffer.isBuffer(ciphertext)) {
throw new Error("buffer required");
}
assert(ciphertext, "Missing cipher text");
// hex is the only common format
var hex = this.decryptHex(ciphertext.toString("hex"));
return new Buffer(hex, "hex");
}
/** This method does not use a checksum, the returned data must be validated some other way.
@arg {string} plaintext
@return {Buffer} binary
*/
encrypt(plaintext) {
if (typeof plaintext === "string") {
plaintext = new Buffer(plaintext, "binary");
}
if (!Buffer.isBuffer(plaintext)) {
throw new Error("buffer required");
}
//assert plaintext, "Missing plain text"
// hex is the only common format
var hex = this.encryptHex(plaintext.toString("hex"));
return new Buffer(hex, "hex");
}
/** This method does not use a checksum, the returned data must be validated some other way.
@arg {string|Buffer} plaintext
@return {string} hex
*/
encryptToHex(plaintext) {
if (typeof plaintext === "string") {
plaintext = new Buffer(plaintext, "binary");
}
if (!Buffer.isBuffer(plaintext)) {
throw new Error("buffer required");
}
//assert plaintext, "Missing plain text"
// hex is the only common format
return this.encryptHex(plaintext.toString("hex"));
}
/** This method does not use a checksum, the returned data must be validated some other way.
@arg {string} cipher - hex
@return {string} binary (could easily be readable text)
*/
decryptHex(cipher) {
assert(cipher, "Missing cipher text");
// Convert data into word arrays (used by Crypto)
var cipher_array = encHex.parse(cipher);
var plainwords = this._decrypt_word_array(cipher_array);
return encHex.stringify(plainwords);
}
/** This method does not use a checksum, the returned data must be validated some other way.
@arg {string} cipher - hex
@return {Buffer} encoded as specified by the parameter
*/
decryptHexToBuffer(cipher) {
assert(cipher, "Missing cipher text");
// Convert data into word arrays (used by Crypto)
var cipher_array = encHex.parse(cipher);
var plainwords = this._decrypt_word_array(cipher_array);
var plainhex = encHex.stringify(plainwords);
return new Buffer(plainhex, "hex");
}
/** This method does not use a checksum, the returned data must be validated some other way.
@arg {string} cipher - hex
@arg {string} [encoding = 'binary'] - a valid Buffer encoding
@return {String} encoded as specified by the parameter
*/
decryptHexToText(cipher, encoding = "binary") {
return this.decryptHexToBuffer(cipher).toString(encoding);
}
/** This method does not use a checksum, the returned data must be validated some other way.
@arg {string} plainhex - hex format
@return {String} hex
*/
encryptHex(plainhex) {
var plain_array = encHex.parse(plainhex);
var cipher_array = this._encrypt_word_array(plain_array);
return encHex.stringify(cipher_array);
}
}
export default Aes;