archethic
Version:
Archethic Javascript SDK
538 lines (457 loc) • 15.9 kB
JavaScript
import CryptoJS from 'crypto-js';
import sjcl from "sjcl";
import sha3 from 'js-sha3';
import blake from 'blakejs';
import * as curve25519 from 'curve25519-js'
import ed2curve from 'ed2curve'
import nacl from "tweetnacl"
import elliptic from "elliptic"
import { hexToUint8Array, concatUint8Arrays, encodeInt32, maybeHexToUint8Array, maybeStringToUint8Array, uint8ArrayToHex, wordArrayToUint8Array } from './utils.js';
const { sha3_512, sha3_256 } = sha3
const EC = elliptic.ec
const ec_P256 = new EC("p256")
const ec_secp256k1 = new EC("secp256k1")
const SOFTWARE_ID = 1
/**
* Generate a random secret key of 32 bytes
*/
export function randomSecretKey() {
return wordArrayToUint8Array(CryptoJS.lib.WordArray.random(32))
}
/**
* Get the ID of a given hash algorithm
* @params {String} hashAlgo Hash algorithm
*/
export function hashAlgoToID(hashAlgo) {
switch (hashAlgo) {
case "sha256":
return 0
case "sha512":
return 1
case "sha3-256":
return 2
case "sha3-512":
return 3
case "blake2b":
return 4
default:
throw "Hash algorithm not supported"
}
}
/**
* Get the hash algo name from the hash algorithm ID
* @param {Integer} ID Hash algorithm's ID
*/
export function IDToHashAlgo(ID) {
switch (ID) {
case 0:
return "sha256"
case 1:
return "sha512"
case 2:
return "sha3-256"
case 3:
return "sha3-512"
case 4:
return "blake2b"
default:
throw "Hash algorithm ID not supported"
}
}
/**
* Get the ID of a given Elliptic curve
* @params {String} curve Elliptic curve
*/
export function curveToID(curve) {
switch (curve) {
case "ed25519":
return 0;
case "P256":
return 1;
case "secp256k1":
return 2;
default:
throw "Curve not supported"
}
}
/**
* Get the curve name from the curve ID
* @param {Integer} ID Curve's ID
*/
export function IDToCurve(ID) {
switch (ID) {
case 0:
return "ed25519"
case 1:
return "P256"
case 2:
return "secp256k1"
default:
throw "Curve ID not supported"
}
}
/**
* Create an address from a seed, an index, an elliptic curve and an hash algorithm.
* The address is prepended by the curve identification, the hash algorithm and the digest of the address
*
* @param {String} seed Keypair derivation seed
* @param {Integer} index Number to identify the order of keys to generate
* @param {String} curve Elliptic Curves(ed25519, P256, secp256k1)
* @param {String} algo Hash algorithm ("sha256", "sha512", "sha3-256", "sha3-512", "blake2b")
*/
export function deriveAddress(seed, index, curve = "ed25519", hashAlgo = "sha256") {
const { publicKey } = deriveKeyPair(seed, index, curve)
const curveID = curveToID(curve)
const hashedPublicKey = hash(publicKey, hashAlgo)
return concatUint8Arrays(
[
Uint8Array.from([curveID]),
Uint8Array.from(hashedPublicKey)
]
)
}
/**
* Create a hash digest from the data with an hash algorithm identification prepending the digest
* @param {String | Uint8Array} content Data to hash (string or buffer)
* @param {String} algo Hash algorithm ("sha256", "sha512", "sha3-256", "sha3-512", "blake2b")
*/
export function hash(content, algo = "sha256") {
content = maybeStringToUint8Array(content)
const algoID = hashAlgoToID(algo)
const digest = getHashDigest(content, algo)
return concatUint8Arrays([
Uint8Array.from([algoID]),
Uint8Array.from(digest)
])
}
export function getHashDigest(content, algo) {
switch (algo) {
case "sha256": {
const input = CryptoJS.lib.WordArray.create(content)
const digest = CryptoJS.SHA256(input)
return wordArrayToUint8Array(digest)
}
case "sha512": {
const input = CryptoJS.lib.WordArray.create(content)
const digest = CryptoJS.SHA512(input)
return wordArrayToUint8Array(digest)
}
case "sha3-256": {
const hash = sha3_256.create();
hash.update(content)
return hash.digest()
}
case "sha3-512": {
const hash = sha3_512.create()
hash.update(content)
return hash.digest()
}
case "blake2b": {
return blake.blake2b(content)
}
default:
throw "Hash algorithm not supported"
}
}
/**
* Generate a keypair using a derivation function with a seed and an index. Each keys is prepending with a curve identification.
* @param {String} seed Keypair derivation seed
* @param {Integer} index Number to identify the order of keys to generate
* @param {String} curve Elliptic curve to use ("ed25519", "P256", "secp256k1")
*/
export function deriveKeyPair(seed, index, curve = "ed25519") {
if (typeof (seed) !== "string" && !(seed instanceof Uint8Array)) {
throw "'seed must be a string"
}
if (typeof index !== 'number' || index < 0) {
throw "'index' must be a positive number"
}
const pvBuf = derivePrivateKey(seed, index)
return generateDeterministicKeyPair(pvBuf, curve, SOFTWARE_ID)
}
/**
* Generate a new keypair deterministically with a given private key, curve and origin id
* @params {Uint8Array} privateKey Private key
* @params {String} curve Elliptic curve
* @params {Integer} originID Origin identification
*/
export function generateDeterministicKeyPair(pvKey, curve, originID) {
pvKey = maybeStringToUint8Array(pvKey)
const curveID = curveToID(curve)
const { publicKey, privateKey } = getKeypair(pvKey, curve)
return {
privateKey: concatUint8Arrays([
Uint8Array.from([curveID]),
Uint8Array.from([originID]),
privateKey
]),
publicKey: concatUint8Arrays([
Uint8Array.from([curveID]),
Uint8Array.from([originID]),
publicKey
])
}
}
function getKeypair(pvKey, curve) {
// Uniform key's seed
if (pvKey.length < 32) {
pvKey = CryptoJS.lib.WordArray.create(maybeStringToUint8Array(pvKey))
pvKey = wordArrayToUint8Array(CryptoJS.SHA256(pvKey))
}
if (pvKey.length > 32) {
pvKey = pvKey.subarray(0, 32)
}
switch (curve) {
case "ed25519": {
const { publicKey } = nacl.sign.keyPair.fromSeed(pvKey)
return {
privateKey: pvKey,
publicKey: publicKey
}
}
case "P256": {
const key = ec_P256.keyFromPrivate(pvKey)
const pubBuf = hexToUint8Array(key.getPublic().encode("hex"))
return {
privateKey: pvKey,
publicKey: pubBuf
}
}
case "secp256k1": {
const key = ec_secp256k1.keyFromPrivate(pvKey)
const pubBuf = hexToUint8Array(key.getPublic().encode("hex"))
return {
privateKey: pvKey,
publicKey: pubBuf
}
}
default:
throw "Curve not supported"
}
}
/**
* Sign the data
* @param {String | Uint8Array} data Data to sign
* @param {String | Uint8Array} privateKey Private key to use to sign the data
*/
export function sign(data, privateKey) {
privateKey = maybeHexToUint8Array(privateKey)
data = maybeStringToUint8Array(data)
const curveBuf = privateKey.slice(0, 1)
const pvBuf = privateKey.slice(2, privateKey.length)
switch (curveBuf[0]) {
case 0: {
const { secretKey: secretKey } = nacl.sign.keyPair.fromSeed(pvBuf)
return nacl.sign.detached(data, secretKey)
}
case 1: {
data = CryptoJS.lib.WordArray.create(maybeStringToUint8Array(data))
const msgHash = wordArrayToUint8Array(CryptoJS.SHA256(data))
const key = ec_P256.keyFromPrivate(pvBuf)
return Uint8Array.from(key.sign(msgHash).toDER())
}
case 2: {
data = CryptoJS.lib.WordArray.create(maybeStringToUint8Array(data))
const msgHash = wordArrayToUint8Array(CryptoJS.SHA256(data))
const key = ec_secp256k1.keyFromPrivate(pvBuf)
return Uint8Array.from(key.sign(msgHash).toDER())
}
default:
throw "Curve not supported"
}
}
export function verify(sig, data, publicKey) {
sig = maybeHexToUint8Array(sig)
publicKey = maybeHexToUint8Array(publicKey)
data = maybeStringToUint8Array(data)
const curveBuf = publicKey.slice(0, 1)
const pubBuf = publicKey.slice(2, publicKey.length)
switch (curveBuf[0]) {
case 0: {
return nacl.sign.detached.verify(data, sig, pubBuf)
}
case 1: {
data = CryptoJS.lib.WordArray.create(maybeStringToUint8Array(data))
const msgHash = wordArrayToUint8Array(CryptoJS.SHA256(data))
const key = ec_P256.keyFromPublic(pubBuf)
return key.verify(msgHash, sig)
}
case 2: {
data = CryptoJS.lib.WordArray.create(maybeStringToUint8Array(data))
const msgHash = wordArrayToUint8Array(CryptoJS.SHA256(data))
const key = ec_secp256k1.keyFromPublic(pubBuf)
return key.verify(msgHash, sig)
}
default:
throw "Curve not supported"
}
}
/**
* Encrypt a data for a given public key using ECIES algorithm
* @param {String | Uint8Array} data Data to encrypt
* @param {String | Uint8Array} publicKey Public key for the shared secret encryption
*/
export function ecEncrypt(data, publicKey) {
publicKey = maybeHexToUint8Array(publicKey)
data = maybeStringToUint8Array(data)
const curve_buf = publicKey.slice(0, 1)
const pubBuf = publicKey.slice(2, publicKey.length)
switch (curve_buf[0]) {
case 0: {
const { public: ephemeralPublicKey, private: ephemeralPrivateKey } = curve25519.generateKeyPair(randomSecretKey())
const curve25519pub = ed2curve.convertPublicKey(pubBuf)
if (!curve25519pub) {
throw "public key in not a valid Ed25519 public key"
}
const sharedKey = curve25519.sharedKey(ephemeralPrivateKey, curve25519pub)
const { aesKey, iv } = deriveSecret(sharedKey)
const { tag, encrypted } = aesAuthEncrypt(data, aesKey, iv)
return concatUint8Arrays([
Uint8Array.from(ephemeralPublicKey),
tag,
encrypted
])
}
case 1: {
const ecdh = ec_P256.genKeyPair()
const pubKey = ec_P256.keyFromPublic(pubBuf).getPublic()
const sharedKey = Uint8Array.from(ecdh.derive(pubKey).toArray())
const { aesKey, iv } = deriveSecret(sharedKey)
const { tag, encrypted } = aesAuthEncrypt(data, aesKey, iv)
return concatUint8Arrays([
hexToUint8Array(ecdh.getPublic().encode("hex")),
tag,
encrypted
])
}
case 2: {
const ecdh = ec_secp256k1.genKeyPair()
const pubKey = ec_secp256k1.keyFromPublic(pubBuf).getPublic()
const sharedKey = Uint8Array.from(ecdh.derive(pubKey).toArray())
const { aesKey, iv } = deriveSecret(sharedKey)
const { tag, encrypted } = aesAuthEncrypt(data, aesKey, iv)
return concatUint8Arrays([
hexToUint8Array(ecdh.getPublic().encode("hex")),
tag,
encrypted
])
}
default:
throw "Curve not supported"
}
}
/**
* Decrypt a ciphertext for a given private key using ECIES algorithm
* @param {String | Uint8Array} ciphertext Ciphertext to decrypt
* @param {String | Uint8Array} privateKey Private key for the shared secret encryption
*/
export function ecDecrypt(ciphertext, privateKey) {
ciphertext = maybeHexToUint8Array(ciphertext)
privateKey = maybeHexToUint8Array(privateKey)
const curve_buf = privateKey.slice(0, 1)
const pvBuf = privateKey.slice(2, privateKey.length)
switch (curve_buf[0]) {
case 0: {
const ephemeralPubKey = ciphertext.slice(0, 32)
const tag = ciphertext.slice(32, 32 + 16)
const encrypted = ciphertext.slice(32 + 16, ciphertext.length)
const curve25519pv = ed2curve.convertSecretKey(pvBuf)
const sharedKey = curve25519.sharedKey(curve25519pv, ephemeralPubKey)
const { aesKey, iv } = deriveSecret(sharedKey)
return aesAuthDecrypt(encrypted, aesKey, iv, tag)
}
case 1: {
const ephemeralPubKey = ciphertext.slice(0, 65)
const tag = ciphertext.slice(65, 65 + 16)
const encrypted = ciphertext.slice(65 + 16, ciphertext.length)
const ecdh = ec_P256.keyFromPrivate(pvBuf)
const pubKey = ec_P256.keyFromPublic(ephemeralPubKey).getPublic()
const sharedKey = Uint8Array.from(ecdh.derive(pubKey).toArray())
const { aesKey, iv } = deriveSecret(sharedKey)
return aesAuthDecrypt(encrypted, aesKey, iv, tag)
}
case 2: {
const ephemeralPubKey = ciphertext.slice(0, 65)
const tag = ciphertext.slice(65, 65 + 16)
const encrypted = ciphertext.slice(65 + 16, ciphertext.length)
const ecdh = ec_secp256k1.keyFromPrivate(pvBuf)
const pubKey = ec_secp256k1.keyFromPublic(ephemeralPubKey).getPublic()
const sharedKey = Uint8Array.from(ecdh.derive(pubKey).toArray())
const { aesKey, iv } = deriveSecret(sharedKey)
return aesAuthDecrypt(encrypted, aesKey, iv, tag)
}
default:
throw "Curve not supported"
}
}
/**
* Encrypt a data for a given public key using AES algorithm
* @param {String | Uint8Array} data Data to encrypt
* @param {String | Uint8Array} key Symmetric key
*/
export function aesEncrypt(data, key) {
key = maybeHexToUint8Array(key)
data = maybeStringToUint8Array(data)
const iv = wordArrayToUint8Array(CryptoJS.lib.WordArray.random(12))
const { tag: tag, encrypted: encrypted } = aesAuthEncrypt(data, key, iv)
const ciphertext = concatUint8Arrays([
new Uint8Array(iv),
tag,
encrypted
])
return ciphertext
}
export function aesDecrypt(cipherText, key) {
cipherText = maybeHexToUint8Array(cipherText)
key = maybeHexToUint8Array(key)
const iv = cipherText.slice(0, 12)
const tag = cipherText.slice(12, 12 + 16)
const encrypted = cipherText.slice(28, cipherText.length)
return aesAuthDecrypt(encrypted, key, iv, tag)
}
function derivePrivateKey(seed, index) {
seed = CryptoJS.lib.WordArray.create(maybeStringToUint8Array(seed))
//Derive master keys
const hash = wordArrayToUint8Array(CryptoJS.SHA512(seed))
const masterKey = hash.subarray(0, 32)
const masterEntropy = hash.subarray(32, 64)
//Derive the final seed
const indexBuf = encodeInt32(index)
const extendedSeed = concatUint8Arrays([masterKey, indexBuf])
const hmacWordArray = CryptoJS.HmacSHA512(CryptoJS.lib.WordArray.create(extendedSeed), CryptoJS.lib.WordArray.create(masterEntropy))
// The first 32 bytes become the next private key
return wordArrayToUint8Array(hmacWordArray).subarray(0, 32)
}
function deriveSecret(sharedKey) {
sharedKey = CryptoJS.lib.WordArray.create(sharedKey)
const pseudoRandomKey = CryptoJS.SHA256(sharedKey)
const iv = wordArrayToUint8Array(CryptoJS.HmacSHA256("0", pseudoRandomKey)).subarray(0, 32)
const aesKey = wordArrayToUint8Array(CryptoJS.HmacSHA256("1", CryptoJS.lib.WordArray.create(iv))).subarray(0, 32)
return {
iv,
aesKey
}
}
function aesAuthEncrypt(data, aesKey, iv) {
// Format for SJCL
const keyBits = sjcl.codec.hex.toBits(uint8ArrayToHex(aesKey))
const dataBits = sjcl.codec.hex.toBits(uint8ArrayToHex(data))
const ivBits = sjcl.codec.hex.toBits(uint8ArrayToHex(iv))
const { tag, data: encrypted } = sjcl.mode.gcm.C(true, new sjcl.cipher.aes(keyBits), dataBits, [], ivBits, 128)
return {
encrypted: hexToUint8Array(sjcl.codec.hex.fromBits(encrypted)),
tag: hexToUint8Array(sjcl.codec.hex.fromBits(tag))
}
}
function aesAuthDecrypt(encrypted, aesKey, iv, tag) {
// Format for SJCL
const encryptedBits = sjcl.codec.hex.toBits(uint8ArrayToHex(encrypted))
const aesKeyBits = sjcl.codec.hex.toBits(uint8ArrayToHex(aesKey))
const ivBits = sjcl.codec.hex.toBits(uint8ArrayToHex(iv))
const tagBits = sjcl.codec.hex.toBits(uint8ArrayToHex(tag))
const { tag: actualTag, data: decrypted } = sjcl.mode.gcm.C(false, new sjcl.cipher.aes(aesKeyBits), encryptedBits, [], ivBits, 128)
if (!sjcl.bitArray.equal(actualTag, tagBits)) {
throw "Invalid tag"
}
return hexToUint8Array(sjcl.codec.hex.fromBits(decrypted))
}