@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
661 lines (588 loc) • 19 kB
JavaScript
/*
* ISC License (ISC)
* Copyright 2018 aeternity developers
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*/
/**
* Crypto module
* @module @aeternity/aepp-sdk/es/utils/crypto
* @example import * as Crypto from '@aeternity/aepp-sdk/es/utils/crypto'
*/
import bs58check from 'bs58check'
import * as RLP from 'rlp'
import { blake2b } from 'blakejs'
import nacl from 'tweetnacl'
import aesjs from 'aes-js'
import { leftPad, rightPad, toBytes } from './bytes'
import shajs from 'sha.js'
import { decode as decodeNode } from '../tx/builder/helpers'
const Ecb = aesjs.ModeOfOperation.ecb
/**
* Check whether a string is valid base-64.
* @param {string} str String to validate.
* @return {boolean} True if the string is valid base-64, false otherwise.
*/
export function isBase64 (str) {
let index
// eslint-disable-next-line no-useless-escape
if (str.length % 4 > 0 || str.match(/[^0-9a-z+\/=]/i)) return false
index = str.indexOf('=')
return !!(index === -1 || str.slice(index).match(/={1,2}/))
}
export const ADDRESS_FORMAT = {
sophia: 1,
api: 2,
raw: 3
}
/**
* Format account address
* @rtype (format: String, address: String) => tx: Promise[String]
* @param {String} format - Format type
* @param {String} address - Base58check account address
* @return {String} Formatted address
*/
export function formatAddress (format = ADDRESS_FORMAT.api, address) {
switch (format) {
case ADDRESS_FORMAT.api:
return address
case ADDRESS_FORMAT.sophia:
return `0x${decodeNode(address, 'ak').toString('hex')}`
}
}
/**
* Check if address is valid
* @rtype (input: String) => valid: Boolean
* @param {String} address - Address
* @return {Boolean} valid
*/
export function isAddressValid (address) {
let isValid
try {
isValid = decodeBase58Check(assertedType(address, 'ak')).length === 32
} catch (e) {
isValid = false
}
return isValid
}
/**
* Convert base58Check address to hex string
* @rtype (base58CheckAddress: String) => hexAddress: String
* @param {String} base58CheckAddress - Address
* @return {String} Hex string
*/
export function addressToHex (base58CheckAddress) {
return `0x${decodeBase58Check(assertedType(base58CheckAddress, 'ak')).toString('hex')}`
}
/**
* Parse decimal address and return base58Check encoded address with prefix 'ak'
* @rtype (input: String) => address: String
* @param {String} decimalAddress - Address
* @return {String} address
*/
export function addressFromDecimal (decimalAddress) {
return aeEncodeKey(toBytes(decimalAddress, true))
}
/**
* Calculate 256bits Blake2b hash of `input`
* @rtype (input: String) => hash: String
* @param {String} input - Data to hash
* @return {Buffer} Hash
*/
export function hash (input) {
return Buffer.from(blake2b(input, null, 32)) // 256 bits
}
/**
* Calculate 256bits Blake2b nameId of `input`
* as defined in https://github.com/aeternity/protocol/blob/master/AENS.md#hashing
* @rtype (input: String) => hash: String
* @param {String} input - Data to hash
* @return {Buffer} Hash
*/
export function nameId (input) {
let buf = Buffer.allocUnsafe(32).fill(0)
if (!input) {
return buf
} else {
const labels = input.split('.')
for (let i = 0; i < labels.length; i++) {
buf = hash(Buffer.concat([buf, hash(labels[i])]))
}
return buf
}
}
/**
* Calculate SHA256 hash of `input`
* @rtype (input: String) => hash: String
* @param {String} input - Data to hash
* @return {String} Hash
*/
export function sha256hash (input) {
return shajs('sha256').update(input).digest()
}
/**
* Generate a random salt (positive integer)
* @rtype () => salt: Number
* @return {Number} random salt
*/
export function salt () {
return Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER))
}
/**
* Base64check encode given `input`
* @rtype (input: String|buffer) => Buffer
* @param {String} input - Data to encode
* @return {Buffer} Base64check encoded data
*/
export function encodeBase64Check (input) {
const buffer = Buffer.from(input)
const checksum = checkSumFn(input)
const payloadWithChecksum = Buffer.concat([buffer, checksum], buffer.length + 4)
return payloadWithChecksum.toString('base64')
}
export function checkSumFn (payload) {
return sha256hash(sha256hash(payload)).slice(0, 4)
}
function decodeRaw (buffer) {
const payload = buffer.slice(0, -4)
const checksum = buffer.slice(-4)
const newChecksum = checkSumFn(payload)
if (!checksum.equals(newChecksum)) return
return payload
}
/**
* Base64check decode given `str`
* @rtype (str: String) => Buffer
* @param {String} str - Data to decode
* @return {Buffer} Base64check decoded data
*/
export function decodeBase64Check (str) {
const buffer = Buffer.from(str, 'base64')
const payload = decodeRaw(buffer)
if (!payload) throw new Error('Invalid checksum')
return Buffer.from(payload)
}
/**
* Base58 encode given `input`
* @rtype (input: String) => String
* @param {String} input - Data to encode
* @return {String} Base58 encoded data
*/
export function encodeBase58Check (input) {
return bs58check.encode(Buffer.from(input))
}
/**
* Base58 decode given `str`
* @rtype (str: String) => Buffer
* @param {String} str - Data to decode
* @return {Buffer} Base58 decoded data
*/
export function decodeBase58Check (str) {
return bs58check.decode(str)
}
/**
* Conver hex string to Uint8Array
* @rtype (str: String) => Uint8Array
* @param {String} str - Data to conver
* @return {Uint8Array} - converted data
*/
export function hexStringToByte (str) {
if (!str) {
return new Uint8Array()
}
var a = []
for (var i = 0, len = str.length; i < len; i += 2) {
a.push(parseInt(str.substr(i, 2), 16))
}
return new Uint8Array(a)
}
/**
* Converts a positive integer to the smallest possible
* representation in a binary digit representation
* @rtype (value: Number) => Buffer
* @param {Number} value - Value to encode
* @return {Buffer} - Encoded data
*/
export function encodeUnsigned (value) {
const binary = Buffer.allocUnsafe(4)
binary.writeUInt32BE(value)
return binary.slice(binary.findIndex(i => i !== 0))
}
/**
* Compute contract address
* @rtype (owner: String, nonce: Number) => String
* @param {String} owner - Address of contract owner
* @param {Number} nonce - Round when contract was created
* @return {String} - Contract address
*/
export function encodeContractAddress (owner, nonce) {
const publicKey = decodeBase58Check(assertedType(owner, 'ak'))
const binary = Buffer.concat([publicKey, encodeUnsigned(nonce)])
return `ct_${encodeBase58Check(hash(binary))}`
}
// KEY-PAIR HELPERS
/**
* Generate keyPair from secret key
* @rtype (secret: Uint8Array) => KeyPair
* @param {Uint8Array} secret - secret key
* @return {Object} - Object with Private(privateKey) and Public(publicKey) keys
*/
export function generateKeyPairFromSecret (secret) {
return nacl.sign.keyPair.fromSecretKey(secret)
}
/**
* Generate a random ED25519 keypair
* @rtype (raw: Boolean) => {publicKey: String, secretKey: String} | {publicKey: Buffer, secretKey: Buffer}
* @param {Boolean} raw - Whether to return raw (binary) keys
* @return {Object} Key pair
*/
export function generateKeyPair (raw = false) {
// <node>/apps/aens/test/aens_test_utils.erl
const keyPair = nacl.sign.keyPair()
const publicBuffer = Buffer.from(keyPair.publicKey)
const secretBuffer = Buffer.from(keyPair.secretKey)
if (raw) {
return {
publicKey: publicBuffer,
secretKey: secretBuffer
}
} else {
return {
publicKey: `ak_${encodeBase58Check(publicBuffer)}`,
secretKey: secretBuffer.toString('hex')
}
}
}
/**
* Encrypt given public key using `password`
* @rtype (password: String, binaryKey: Buffer) => Uint8Array
* @param {String} password - Password to encrypt with
* @param {Buffer} binaryKey - Key to encrypt
* @return {Uint8Array} Encrypted key
*/
export function encryptPublicKey (password, binaryKey) {
return encryptKey(password, rightPad(32, binaryKey))
}
/**
* Encrypt given private key using `password`
* @rtype (password: String, binaryKey: Buffer) => Uint8Array
* @param {String} password - Password to encrypt with
* @param {Buffer} binaryKey - Key to encrypt
* @return {Uint8Array} Encrypted key
*/
export function encryptPrivateKey (password, binaryKey) {
return encryptKey(password, leftPad(64, binaryKey))
}
/**
* Encrypt given data using `password`
* @rtype (password: String, binaryData: Buffer) => Uint8Array
* @param {String} password - Password to encrypt with
* @param {Buffer} binaryData - Data to encrypt
* @return {Uint8Array} Encrypted data
*/
export function encryptKey (password, binaryData) {
let hashedPasswordBytes = sha256hash(password)
let aesEcb = new Ecb(hashedPasswordBytes)
return aesEcb.encrypt(binaryData)
}
/**
* Decrypt given data using `password`
* @rtype (password: String, encrypted: String) => Uint8Array
* @param {String} password - Password to decrypt with
* @param {String} encrypted - Data to decrypt
* @return {Buffer} Decrypted data
*/
export function decryptKey (password, encrypted) {
const encryptedBytes = Buffer.from(encrypted)
let hashedPasswordBytes = sha256hash(password)
let aesEcb = new Ecb(hashedPasswordBytes)
return Buffer.from(aesEcb.decrypt(encryptedBytes))
}
// SIGNATURES
/**
* Generate signature
* @rtype (data: String|Buffer, privateKey: Buffer) => Buffer
* @param {String|Buffer} data - Data to sign
* @param {String|Buffer} privateKey - Key to sign with
* @return {Buffer} Signature
*/
export function sign (data, privateKey) {
return nacl.sign.detached(Buffer.from(data), Buffer.from(privateKey))
}
/**
* Verify that signature was signed by public key
* @rtype (str: String, signature: Buffer, publicKey: Buffer) => Boolean
* @param {String} str - Data to verify
* @param {Buffer} signature - Signature to verify
* @param {Buffer} publicKey - Key to verify against
* @return {Boolean} Valid?
*/
export function verify (str, signature, publicKey) {
return nacl.sign.detached.verify(new Uint8Array(str), signature, publicKey)
}
/**
* @typedef {Array} Transaction
* @rtype Transaction: [tag: Buffer, version: Buffer, [signature: Buffer], data: Buffer]
*/
/**
* Prepare a transaction for posting to the blockchain
* @rtype (signature: Buffer | String, data: Buffer) => Transaction
* @param {Buffer} signature - Signature of `data`
* @param {Buffer} data - Transaction data
* @return {Transaction} Transaction
*/
export function prepareTx (signature, data) {
// the signed tx deserializer expects a 4-tuple:
// <tag, version, signatures_array, binary_tx>
return [Buffer.from([11]), Buffer.from([1]), [Buffer.from(signature)], data]
}
export function personalMessageToBinary (message) {
const p = Buffer.from('æternity Signed Message:\n', 'utf8')
const msg = Buffer.from(message, 'utf8')
if (msg.length >= 0xFD) throw new Error('message too long')
return Buffer.concat([Buffer.from([p.length]), p, Buffer.from([msg.length]), msg])
}
export function signPersonalMessage (message, privateKey) {
return sign(personalMessageToBinary(message), privateKey)
}
export function verifyPersonalMessage (str, signature, publicKey) {
return verify(personalMessageToBinary(str), signature, publicKey)
}
/**
* æternity readable public keys are the base58-encoded public key, prepended
* with 'ak_'
* @rtype (binaryKey: Buffer) => String
* @param {Buffer} binaryKey - Key to encode
* @return {String} Encoded key
*/
export function aeEncodeKey (binaryKey) {
const publicKeyBuffer = Buffer.from(binaryKey, 'hex')
const pubKeyAddress = encodeBase58Check(publicKeyBuffer)
return `ak_${pubKeyAddress}`
}
/**
* Generate a new key pair using {@link generateKeyPair} and encrypt it using `password`
* @rtype (password: String) => {publicKey: Uint8Array, secretKey: Uint8Array}
* @param {String} password - Password to encrypt with
* @return {Object} Encrypted key pair
*/
export function generateSaveWallet (password) {
let keys = generateKeyPair(true)
return {
publicKey: encryptPublicKey(password, keys.publicKey),
secretKey: encryptPrivateKey(password, keys.secretKey)
}
}
/**
* Decrypt an encrypted private key
* @rtype (password: String, encrypted: Buffer) => Buffer
* @param {String} password - Password to decrypt with
* @return {Buffer} Decrypted key
*/
export function decryptPrivateKey (password, encrypted) {
return decryptKey(password, encrypted)
}
/**
* Decrypt an encrypted public key
* @rtype (password: String, encrypted: Buffer) => Buffer
* @param {String} password - Password to decrypt with
* @return {Buffer} Decrypted key
*/
export function decryptPubKey (password, encrypted) {
return decryptKey(password, encrypted).slice(0, 65)
}
/**
* Assert base58 encoded type and return its payload
* @rtype (data: String, type: String) => String, throws: Error
* @param {String} data - ae data
* @param {String} type - Prefix
* @return {String} Payload
*/
export function assertedType (data, type) {
if (RegExp(`^${type}_.+$`).test(data)) {
return data.split('_')[1]
} else {
throw Error(`Data doesn't match expected type ${type}`)
}
}
/**
* Decode a transaction
* @rtype (txHash: String) => Buffer
* @param {String} password - Password to decrypt with
* @return {Array} Decoded transaction
*/
export function decodeTx (txHash) {
return RLP.decode(Buffer.from(decodeBase64Check(assertedType(txHash, 'tx'))))
}
/**
* Encode a transaction
* @rtype (txData: Transaction) => String
* @param {Transaction} txData - Transaction to encode
* @return {String} Encoded transaction
*/
export function encodeTx (txData) {
const encodedTxData = RLP.encode(txData)
const encodedTx = encodeBase64Check(encodedTxData)
return `tx_${encodedTx}`
}
/**
* Check key pair for validity
*
* Sign a message, and then verifying that signature
* @rtype (privateKey: Buffer, publicKey: Buffer) => Boolean
* @param {Buffer} privateKey - Private key to verify
* @param {Buffer} publicKey - Public key to verify
* @return {Boolean} Valid?
*/
export function isValidKeypair (privateKey, publicKey) {
const message = 'TheMessage'
const signature = sign(message, privateKey)
return verify(message, signature, publicKey)
}
/**
* Obtain key pair from `env`
*
* Designed to be used with `env` from nodejs. Assumes enviroment variables
* `WALLET_PRIV` and `WALLET_PUB`.
* @rtype (env: Object) => {publicKey: String, secretKey: String}, throws: Error
* @param {Object} env - Environment
* @return {Object} Key pair
*/
export function envKeypair (env) {
const keypair = {
secretKey: env['WALLET_PRIV'],
publicKey: env['WALLET_PUB']
}
if (keypair.publicKey && keypair.secretKey) {
return keypair
} else {
throw Error('Environment variables WALLET_PRIV and WALLET_PUB need to be set')
}
}
/**
* RLP decode
* @rtype (data: Any) => Buffer[]
* @param {Buffer|String|Integer|Array} data - Data to decode
* @return {Array} Array of Buffers containing the original message
*/
export const decode = RLP.decode
export const encode = RLP.encode
export const rlp = RLP
const OBJECT_TAGS = {
SIGNED_TX: 11,
CHANNEL_CREATE_TX: 50,
CHANNEL_CLOSE_MUTUAL_TX: 53,
CHANNEL_OFFCHAIN_TX: 57,
CHANNEL_OFFCHAIN_UPDATE_TRANSFER: 570
}
function objectTag (tag, pretty) {
if (pretty) {
const entry = Object.entries(OBJECT_TAGS).find(([key, value]) => tag === value)
return entry ? entry[0] : tag
}
return tag
}
function readInt (buf) {
return buf.readIntBE(0, buf.length)
}
function readId (buf) {
const type = buf.readUIntBE(0, 1)
const prefix = {
1: 'ak',
2: 'nm',
3: 'cm',
4: 'ok',
5: 'ct',
6: 'ch'
}[type]
const hash = encodeBase58Check(buf.slice(1, buf.length))
return `${prefix}_${hash}`
}
function readSignatures (buf) {
const signatures = []
for (let i = 0; i < buf.length; i++) {
signatures.push(encodeBase58Check(buf[i]))
}
return signatures
}
function deserializeOffChainUpdate (binary, opts) {
const tag = readInt(binary[0])
const obj = {
tag: objectTag(tag, opts.prettyTags),
version: readInt(binary[1])
}
switch (tag) {
case OBJECT_TAGS.CHANNEL_OFFCHAIN_UPDATE_TRANSFER:
return Object.assign(obj, {
from: readId(binary[2]),
to: readId(binary[3]),
amount: readInt(binary[4])
})
}
return obj
}
function readOffChainTXUpdates (buf, opts) {
const updates = []
for (let i = 0; i < buf.length; i++) {
updates.push(deserializeOffChainUpdate(decode(buf[i]), opts))
}
return updates
}
/**
* Deserialize `binary` state channel transaction
* @rtype (binary: String) => Object
* @param {String} binary - Data to deserialize
* @param {Object} opts - Options
* @return {Object} Channel data
*/
export function deserialize (binary, opts = { prettyTags: false }) {
const tag = readInt(binary[0])
const obj = {
tag: objectTag(tag, opts.prettyTags),
version: readInt(binary[1])
}
switch (tag) {
case OBJECT_TAGS.SIGNED_TX:
return Object.assign(obj, {
signatures: readSignatures(binary[2]),
tx: deserialize(decode(binary[3]), opts)
})
case OBJECT_TAGS.CHANNEL_CREATE_TX:
return Object.assign(obj, {
initiator: readId(binary[2]),
initiatorAmount: readInt(binary[3]),
responder: readId(binary[4]),
responderAmount: readInt(binary[5]),
channelReserve: readInt(binary[6]),
lockPeriod: readInt(binary[7]),
ttl: readInt(binary[8]),
fee: readInt(binary[9])
})
case OBJECT_TAGS.CHANNEL_CLOSE_MUTUAL_TX:
return Object.assign(obj, {
channelId: readId(binary[2]),
initiatorAmount: readInt(binary[3]),
responderAmount: readInt(binary[4]),
ttl: readInt(binary[5]),
fee: readInt(binary[6]),
nonce: readInt(binary[7])
})
case OBJECT_TAGS.CHANNEL_OFFCHAIN_TX:
return Object.assign(obj, {
channelId: readId(binary[2]),
round: readInt(binary[3]),
updates: readOffChainTXUpdates(binary[4], opts),
state: encodeBase58Check(binary[5])
})
}
}