UNPKG

@xmr-core/xmr-crypto-utils

Version:

Core crypto operations for Monero and implementations and interfaces of various hardware devices for creating Monero transactions securely

1,434 lines (1,258 loc) 30 kB
import Transport from "@ledgerhq/hw-transport"; import { INS } from "./commands"; import { HWDevice, DeviceMode, PublicAddress, Key, KeyDerivation, PublicKey, IAccountKeys, ISubaddressIndex, PublicSpendKey, SecretKey, EcScalar, Hash8, KeyV, CtKeyV, SecretKeys, ChachaKey, } from "../types"; import * as crypto from "../../crypto-ops"; import { KeyPair, Commit } from "../../types"; import { JSONPrettyPrint } from "@xmr-core/xmr-str-utils"; enum RCT { RCTTypeNull = 0x00, RCTTypeFull = 0x01, RCTTypeSimple = 0x02, RCTTypeFullBulletproof = 0x03, RCTTypeSimpleBulletproof = 0x04, } type ArrLike = (number | string)[] | Buffer; interface IABPKeys { Aout: Key; Bout: Key; is_subaddress: boolean; index: number; // size_t Pout: Key; // search by this property AKout: Key; } class Keymap { private map: { [Pout: string]: IABPKeys | undefined } = {}; public add(keys: IABPKeys) { if (this.map[keys.Pout]) { throw Error( `Cannot add key to map, Pout: ${keys.Pout} already exists`, ); } this.map[keys.Pout] = keys; } public find(Pout: Key) { return this.map[Pout]; } public clear() { this.map = {}; } } const ledgerLog = (fnname: string, obj: any, extra?: string) => JSONPrettyPrint(`LedgerDevice ${fnname}`, obj, extra); // tslint:disable-next-line:max-classes-per-file export class LedgerDevice<T> implements HWDevice { private readonly transport: Transport<T>; private name: string; private mode: DeviceMode; private privateViewKey: string; private has_view_key: boolean; private readonly null_skey = this.hexString(); private readonly key_map = new Keymap(); constructor(transport: Transport<T>) { this.transport = transport; this.name = ""; this.mode = DeviceMode.NONE; this.has_view_key = false; this.privateViewKey = this.hexString(); transport.decorateAppAPIMethods( this, [ "set_mode", "put_key", "get_public_address", "get_secret_keys", "derive_subaddress_public_key", "get_subaddress_spend_public_key", "get_subaddress", "get_subaddress_secret_key", "verify_keys", "scalarmultKey", "scalarmultBase", "sc_secret_add", "generate_keys", "generate_key_derivation", "derivation_to_scalar", "derive_secret_key", "derive_public_key", "secret_key_to_public_key", "generate_key_image", "open_tx", "encrypt_payment_id", "ecdhEncode", "ecdhDecode", "mlsag_prehash", "mlsag_prepare", "mlsag_hash", "mlsag_sign", "close_tx", ] as any[], "MOON", ); transport.setScrambleKey("MOON"); } /* ======================================================================= */ /* MISC */ /* ======================================================================= */ // #region MISC public reset() { return this.send(INS.RESET, 0x00, 0x00); } // #endregion MISC /* ======================================================================= */ /* SETUP/TEARDOWN */ /* ======================================================================= */ // #region SETUP/TEARDOWN public set_name(name: string) { this.name = name; return true; } public get_name() { return this.name; } public async set_mode(mode: DeviceMode) { ledgerLog("set_mode", { mode }, "args"); switch (mode) { case DeviceMode.TRANSACTION_CREATE_REAL: case DeviceMode.TRANSACTION_CREATE_FAKE: await this.send(INS.SET_SIGNATURE_MODE, 0x01, 0x00, [ 0x00, mode, ]); this.mode = mode; break; case DeviceMode.TRANSACTION_PARSE: case DeviceMode.NONE: this.mode = mode; break; default: throw Error( `device_ledger::set_mode(unsigned int mode): invalid mode ${mode}`, ); } console.log(`Switched to mode: ${mode}`); return true; } // #endregion SETUP/TEARDOWN /* ======================================================================= */ /* WALLET & ADDRESS */ /* ======================================================================= */ // #region WALLET & ADDRESS public async put_key( privViewKey: string, pubViewKey: string, privSpendKey: string, pubSpendKey: string, _b58PubKey: string, ): Promise<boolean> { ledgerLog( "put_key", { privViewKey, pubViewKey, privSpendKey, pubSpendKey, _b58PubKey, }, "args", ); await this.send(INS.PUT_KEY, 0x00, 0x00, [ 0x00, privViewKey, pubViewKey, privSpendKey, pubSpendKey, // dummy pub key just to bypass length verification // isnt used in blue-app-monero code for anything else "bc3b105abc2f939571e5b107ab58dc4f6ea22923d8189be54a47d107d187d901bc3b105abc2f939571e5b107ab58dc4f6ea22923d8189be54a47d107d187d901bc3b105abc2f939571e5b107ab58dc4f6ea22923d8189be54a47d107d187", ]); return true; } public async get_public_address(): Promise<PublicAddress> { ledgerLog("get_public_address", { command: "get_public_address" }); const [view_public_key, spend_public_key] = await this.send( INS.GET_KEY, 0x01, 0x00, [0x00], [32, 64], ); ledgerLog( "get_public_address", { view_public_key, spend_public_key, }, "ret", ); return { view_public_key, spend_public_key, }; } /** * * @description Retrives the secret view key if the user allows the export * Skips retriving the view key if already previously exported * @returns Fake view and send private key * @memberof XMR */ public async get_secret_keys(): Promise<SecretKeys> { // secret keys are represented as fake keys on the wallet side // because secret keys are always in possesion of the ledger device const vkey = this.hexString(); const skey = this.hexString(0xff); ledgerLog("get_secret_keys", { command: "get_secret_keys" }); if (this.is_fake_view_key(this.privateViewKey)) { const [viewKey] = await this.send( INS.GET_KEY, 0x02, 0x00, [0x00], [32], ); this.privateViewKey = viewKey; } this.has_view_key = !this.is_fake_view_key(this.privateViewKey); ledgerLog("get_secret_keys", { viewKey: vkey, spendKey: skey }, "ret"); return { viewKey: vkey, spendKey: skey }; } public async export_private_view_key(): Promise<string> { ledgerLog("export_private_view_key", { command: "export_private_view_key", }); if (this.is_fake_view_key(this.privateViewKey)) { await this.get_secret_keys(); } ledgerLog( "export_private_view_key", { viewKey: this.privateViewKey }, "ret", ); return this.privateViewKey; } public async generate_chacha_key(_keys: IAccountKeys): Promise<ChachaKey> { const [] = await this.send( INS.GET_CHACHA8_PREKEY, 0x00, 0x00, [0x00], [200], ); return this.notSupported(); } // #endregion WALLET & ADDRESS /* ======================================================================= */ /* SUB ADDRESS */ /* ======================================================================= */ // #region SUB ADDRESS /** * * @param {PublicKey} pub * @param {KeyDerivation} derivation * @param {number} output_index * @returns {Promise<PublicKey>} * @memberof XMR */ public async derive_subaddress_public_key( pub: PublicKey, derivation: KeyDerivation, output_index: number, ): Promise<PublicKey> { if (this.mode === DeviceMode.TRANSACTION_PARSE && this.has_view_key) { //If we are in TRANSACTION_PARSE, the given derivation has been retrieved decrypted (without the help //of the device), so continue that way. return this.notSupported(); // return this.extern.derive_subaddress_public_key( // pub, // derivation, // output_index, // ); } else { const [derived_pub] = await this.send( INS.DERIVE_SUBADDRESS_PUBLIC_KEY, 0x00, 0x00, [ 0x00, pub, derivation, output_index >> 24, output_index >> 16, output_index >> 8, output_index >> 0, ], [32], ); return derived_pub; } } public async get_subaddress_spend_public_key( keys: IAccountKeys, index: ISubaddressIndex, ): Promise<PublicKey> { if (index.isZero()) { return keys.m_account_address.spend_public_key; } // decompress index, taking 4+4 bytes const [D] = await this.send( INS.GET_SUBADDRESS_SPEND_PUBLIC_KEY, 0x00, 0x00, [0x00, index.major, index.minor], [32], ); return D; } public async get_subaddress_spend_public_keys( keys: IAccountKeys, account: number, begin: number, end: number, ): Promise<PublicSpendKey[]> { const pkeys: PublicSpendKey[] = []; for (let index = begin; index < end; index++) { pkeys.push( await this.get_subaddress_spend_public_key(keys, { major: account, minor: index, isZero: () => false, // need to impl }), ); } return pkeys; } public async get_subaddress( keys: IAccountKeys, index: ISubaddressIndex, ): Promise<PublicAddress> { if (index.isZero()) { return keys.m_account_address; } const [view_public_key, spend_public_key] = await this.send( INS.GET_SUBADDRESS, 0x00, 0x00, [0x00, index.major, index.minor], [32, 64], ); return { view_public_key, spend_public_key }; } public async get_subaddress_secret_key( sec: SecretKey, index: ISubaddressIndex, ): Promise<SecretKey> { const [sub_sec] = await this.send( INS.GET_SUBADDRESS_SECRET_KEY, 0x00, 0x00, [0x00, sec, index.major, index.minor], [32], ); return sub_sec; } // #endregion SUB ADDRESS /* ======================================================================= */ /* DERIVATION & KEY */ /* ======================================================================= */ // #region DERIVATION & KEY public async verify_keys(secret_key: SecretKey, public_key: PublicKey) { ledgerLog("verify_keys", { secret_key, public_key }, "args"); const verifyArr = await this.send( INS.VERIFY_KEY, 0x00, 0x00, [0x00, secret_key, public_key], [1, 2, 3, 4], ).then(arr => arr.map(str => parseInt(str, 16))); // TODO: support full 32 bit return value in the future // for any verification return value changes // but for now, we just need to check the last 4 bytes // to see if the last bit is 1 or not // const verified = // (verifyArr[0] << 24) | // (verifyArr[1] << 16) | // (verifyArr[2] << 8) | // (verifyArr[3] << 0); const verified = verifyArr[3]; ledgerLog("verify_keys", { verified: verified === 1 }, "ret"); return verified === 1; } public async scalarmultKey(P: Key, a: Key): Promise<Key> { ledgerLog("scalarmultKey", { P, a }, "args"); const [aP] = await this.send( INS.SECRET_SCAL_MUL_KEY, 0x00, 0x00, [0x00, P, a], [32], ); ledgerLog("scalarmultKey", { aP }, "ret"); return aP; } public async scalarmultBase(a: Key): Promise<Key> { ledgerLog("scalarmultBase", { a }, "args"); const [aG] = await this.send( INS.SECRET_SCAL_MUL_BASE, 0x00, 0x00, [0x00, a], [32], ); ledgerLog("scalarmultBase", { aG }, "args"); return aG; } public async sc_secret_add(a: SecretKey, b: SecretKey) { ledgerLog("sc_secret_add", { a, b }, "args"); const [r] = await this.send( INS.SECRET_KEY_ADD, 0x00, 0x00, [0x00, a, b], [32], ); ledgerLog("sc_secret_add", { r }, "args"); return r; } public async generate_keys(recovery_key?: SecretKey): Promise<KeyPair> { ledgerLog("generate_keys", { command: "generate_keys" }); if (recovery_key) { throw Error( "Ledger device method generate_keys does not support recover", ); } const [pub, sec] = await this.send( INS.GENERATE_KEYPAIR, 0x00, 0x00, [0x00], [32, 64], ); ledgerLog("generate_keys", { pub, sec }, "ret"); return { pub, sec }; } public async generate_key_derivation( pub: PublicKey, sec: SecretKey, ): Promise<KeyDerivation> { ledgerLog("generate_key_derivation", { pub, sec }, "args"); if (this.mode === DeviceMode.TRANSACTION_PARSE && this.has_view_key) { // When a derivation is requested in PARSE mode and the view key is available, // Perform the derivation via extern library and return the derivation unencrypted console.log( "generate_key_derivation : PARSE mode with known viewkey", ); //Note derivation in PARSE mode can only happen with viewkey, so assert it! (?) if (this.is_fake_view_key(this.privateViewKey)) { throw Error( "Derivation in PARSE mode can only happen with viewkey", ); } const derivation = crypto.derivation.generate_key_derivation( pub, this.privateViewKey, ); return derivation; } else { const [derivation] = await this.send( INS.GEN_KEY_DERIVATION, 0x00, 0x00, [0x00, pub, sec], [32], ); ledgerLog("generate_key_derivation", { derivation }, "ret"); return derivation; } } public async conceal_derivation( derivation: KeyDerivation, tx_pub_key: PublicKey, additional_tx_pub_keys: PublicKey[], main_derivation: KeyDerivation, additional_derivations: KeyDerivation[], ) { ledgerLog( "conceal_derivation", { derivation, tx_pub_key, additional_tx_pub_keys, main_derivation, additional_derivations, }, "args", ); let pubKey: string | undefined; if (derivation === main_derivation) { pubKey = tx_pub_key; console.log("conceal derivation with main tx pub key"); } else { console.warn( "conceal_derivation NOTE: if size of additional_derivations > Number.MAX_INTEGER, then we have a problem", ); const derivationIdx = additional_derivations.indexOf(derivation); if (derivationIdx !== -1) { pubKey = additional_tx_pub_keys[derivationIdx]; } console.log("conceal derivation with additional tx pub key"); } if (pubKey === undefined) { throw Error("Mismatched derivation on scan info"); } ledgerLog( "conceal_derivation", { derivation: this.generate_key_derivation( pubKey, this.null_skey, ), }, "ret", ); return this.generate_key_derivation(pubKey, this.null_skey); } public async derivation_to_scalar( derivation: KeyDerivation, output_index: number, ): Promise<EcScalar> { ledgerLog( "derivation_to_scalar", { derivation, output_index, }, "args", ); const [scalar] = await this.send( INS.DERIVATION_TO_SCALAR, 0x00, 0x00, [ 0x00, derivation, output_index >> 24, output_index >> 16, output_index >> 8, output_index >> 0, ], [32], ); ledgerLog( "derivation_to_scalar", { scalar, }, "ret", ); return scalar; } public async derive_secret_key( derivation: KeyDerivation, output_index: number, sec: SecretKey, ): Promise<SecretKey> { ledgerLog( "derive_secret_key", { derivation, output_index, sec, }, "args", ); const [derivedSec] = await this.send( INS.DERIVE_SECRET_KEY, 0x00, 0x00, [ 0x00, derivation, output_index >> 24, output_index >> 16, output_index >> 8, output_index >> 0, sec, ], [32], ); ledgerLog( "derive_secret_key", { derivedSec, }, "args", ); return derivedSec; } public async derive_public_key( derivation: PublicKey, output_index: number, pub: PublicKey, ): Promise<PublicKey> { ledgerLog( "derive_public_key", { derivation, output_index, pub, }, "args", ); const [derived_pub] = await this.send( INS.DERIVE_PUBLIC_KEY, 0x00, 0x00, [ 0x00, derivation, output_index >> 24, output_index >> 16, output_index >> 8, output_index >> 0, pub, ], [32], ); ledgerLog( "derive_public_key", { derived_pub, }, "ret", ); return derived_pub; } public async secret_key_to_public_key(sec: SecretKey): Promise<PublicKey> { ledgerLog( "secret_key_to_public_key", { sec, }, "args", ); const [pub] = await this.send( INS.SECRET_KEY_TO_PUBLIC_KEY, 0x00, 0x00, [0x00, sec], [32], ); ledgerLog( "secret_key_to_public_key", { pub, }, "ret", ); return pub; } public async generate_key_image( pub: PublicKey, sec: SecretKey, ): Promise<PublicKey> { const [ki] = await this.send( INS.GEN_KEY_IMAGE, 0x00, 0x00, [0x00, pub, sec], [32], ); return ki; } // #endregion DERIVATION & KEY /* ======================================================================= */ /* TRANSACTION */ /* ======================================================================= */ // #region TRANSACTION public async open_tx(): Promise<SecretKey> { ledgerLog("open_tx", { command: "open_tx", }); const options = 0x00; const account = [0x00, 0x00, 0x00, 0x00]; // skip over R and grab encrypted r instead const [, enc_r] = await this.send( INS.OPEN_TX, 0x01, 0x00, [options, ...account], [32, 64], ); const sec_tx_key = enc_r; ledgerLog( "open_tx", { sec_tx_key, }, "ret", ); return sec_tx_key; } public async encrypt_payment_id( paymentId: string, public_key: string, secret_key: string, ): Promise<Hash8> { ledgerLog( "encrypt_payment_id", { paymentId, public_key, secret_key, }, "args", ); const [enc_pid] = await this.send( INS.STEALTH, 0x00, 0x00, [0x00, public_key, secret_key, paymentId], [8], ); ledgerLog( "encrypt_payment_id", { enc_pid, }, "ret", ); return enc_pid; } public async decrypt_payment_id( paymentId: string, public_key: string, secret_key: string, ): Promise<Hash8> { ledgerLog( "decrypt_payment_id", { paymentId, public_key, secret_key, }, "args", ); return await this.encrypt_payment_id(paymentId, public_key, secret_key); } /** * @description store keys during construct_tx_with_tx_key to be later used during genRct -> mlsag_prehash * @param {PublicKey} Aout * @param {PublicKey} Bout * @param {boolean} is_subaddress * @param {number} real_output_index * @param {Key} amount_key * @param {PublicKey} out_eph_public_key * @returns {Promise<boolean>} */ public add_output_key_mapping( Aout: PublicKey, Bout: PublicKey, is_subaddress: boolean, real_output_index: number, amount_key: Key, out_eph_public_key: PublicKey, ): boolean { ledgerLog( "add_output_key_mapping", { Aout: Aout, Bout: Bout, is_subaddress, index: real_output_index, Pout: out_eph_public_key, AKout: amount_key, }, "args", ); this.key_map.add({ Aout: Aout, Bout: Bout, is_subaddress, index: real_output_index, Pout: out_eph_public_key, AKout: amount_key, }); return true; } public async ecdhEncode( unmasked: Commit, AKout: SecretKey, ): Promise<Commit> { ledgerLog( "ecdhEncode", { unmasked, AKout, }, "args", ); // AKout -> Amount key for output // AKout = encrypted private derivation data computed during the processing of output transaction keys // derivation data = generate_key_derivation(Kv (recipent view public key), r (tx_key) ) = r.Kv // scalar = hash_to_scalar(Kv.r) // AKout = rct::sk2rct(Hn(rKv)) where rct::sk2rct just typecasts type crypto::secret_key to rct::key const [blindAmount, blindMask] = await this.send( INS.BLIND, 0x00, 0x00, [0x00, AKout, unmasked.mask, unmasked.amount], [32, 64], ); ledgerLog( "ecdhEncode", { blindAmount, blindMask, }, "ret", ); return { amount: blindAmount, mask: blindMask }; } public async ecdhDecode(masked: Commit, AKout: SecretKey): Promise<Commit> { ledgerLog( "ecdhDecode", { masked, AKout, }, "args", ); const [unmaskedAmount, unmaskedMask] = await this.send( INS.UNBLIND, 0x00, 0x00, [0x00, AKout, masked.mask, masked.amount], [32, 64], ); ledgerLog( "ecdhDecode", { unmaskedAmount, unmaskedMask, }, "ret", ); return { amount: unmaskedAmount, mask: unmaskedMask }; } public async mlsag_prehash( blob: string, inputs_size: number, // 64 bits outputs_size: number, // 64 bits hashes: KeyV, outPk: CtKeyV, ): Promise<Key> { ledgerLog( "mlsag_prehash", { blob, inputs_size, outputs_size, hashes, outPk, }, "args", ); const data = Buffer.from(blob, "hex"); const options = inputs_size === 0 ? 0x00 : 0x80; const type = data[0]; const txnFee: number[] = []; let data_offset = 1; while (data[data_offset] & 0x80) { txnFee.push(data[data_offset]); data_offset += 1; } ledgerLog("mlsag_prehash", {}, "INS.VALIDATE p2 === 1"); // monero_apdu_mlsag_prehash_init p2 === 1 await this.send(INS.VALIDATE, 0x01, 0x01, [ options, type, ...txnFee, data[data_offset], ]); data_offset += 1; // monero_apdu_mlsag_prehash_init p2 > 1 // pseudoOuts if ( type === RCT.RCTTypeSimple || type === RCT.RCTTypeSimpleBulletproof ) { for (let i = 0; i < inputs_size; i++) { const p1 = 0x01; const p2 = i + 0x02; const opts = i === inputs_size - 1 ? 0x00 : 0x80; // slice 32 bytes starting from data_offset const pseudoOut = data .slice(data_offset, data_offset + 32) .toString("hex"); ledgerLog("mlsag_prehash", {}, `INS.VALIDATE p2 === ${p2}`); await this.send(INS.VALIDATE, p1, p2, [opts, pseudoOut]); data_offset += 32; } } // ====== Aout, Bout, AKout, C, v, k ====== // where k is the mask // and v is the amount // Aout, Bout is the receiver main view/spend public keys // keccak: 2nd group generator, such H = h.G and keccak is unknown // C is the commitment to v where Cv = k.G + v.H // monero_apdu_mlsag_prehash_update let kv_offset = data_offset; let C_offset = kv_offset + 32 * 2 * outputs_size; for (let i = 0; i < outputs_size; i++) { const outKeys = this.key_map.find(outPk[i].dest); if (!outKeys) { throw Error(`Pout not found: ${outPk[i].dest} `); } const p1 = 0x02; const p2 = i + 0x01; const opts = i === outputs_size - 1 ? 0x00 : 0x80; const data_buf: any[] = [ opts, outKeys.is_subaddress, outKeys.Aout, outKeys.Bout, outKeys.AKout, ]; // C data_buf.push(data.slice(C_offset, C_offset + 32).toString("hex")); C_offset += 32; // k data_buf.push( data.slice(kv_offset, kv_offset + 32).toString("hex"), ); kv_offset += 32; //v data_buf.push( data.slice(kv_offset, kv_offset + 32).toString("hex"), ); kv_offset += 32; ledgerLog( "mlsag_prehash", { outKeys, p1, p2, opts, data_buf, C_offset, kv_offset, }, `INS.VALIDATE p2 === ${p2}`, ); await this.send(INS.VALIDATE, p1, p2, data_buf); } // ====== C[], message, proof====== let _i = 0; C_offset = kv_offset; for (_i = 0; _i < outputs_size; _i++) { const p1 = 0x03; const p2 = _i + 0x01; const opts = 0x80; // C const C = data.slice(C_offset, C_offset + 32).toString("hex"); C_offset += 32; ledgerLog( "mlsag_prehash", { p1, p2, opts, C, C_offset, kv_offset, }, `INS.VALIDATE p2 === ${p2}`, ); await this.send(INS.VALIDATE, p1, p2, [opts, C]); } const [prehash] = await this.send( INS.VALIDATE, 0x03, _i + 0x01, [0x00, hashes[0], hashes[2]], [32], ); ledgerLog("mlsag_prehash", { prehash }, "ret"); return prehash; } /** * * @description Generate the matrix ring parameters * @param {Key} H * @param {Key} xx * @returns {Promise<{ a: Key, aG: Key, aHP: Key, II: Key }>} * @memberof Device */ public async mlsag_prepare( H: Key, xx: Key, ): Promise<{ a: Key; aG: Key; aHP: Key; II: Key }>; /** * * @description Generate the matrix ring parameters * @returns {Promise<{ a: Key, aG: Key }>} * @memberof Device */ public async mlsag_prepare(): Promise<{ a: Key; aG: Key }>; public async mlsag_prepare(H?: Key, xx?: Key) { if (!H || !xx) { const [a, aG] = await this.send( INS.MLSAG, 0x01, 0x00, [0x00], [32, 64], ); return { a, aG }; } else { ledgerLog("mlsag_prepare", { H, xx }, "args"); // a -> alpha -> one time secret key for tx // aG -> alpha.G -> one time public key for tx const [a, aG, aHP, II] = await this.send( INS.MLSAG, 0x01, 0x00, [0x00, H, xx], [32, 64, 96, 128], ); ledgerLog("mlsag_prepare", { a, aG, aHP, II }, "ret"); return { a, aG, aHP, II }; } } public async mlsag_hash(long_message: KeyV): Promise<Key> { ledgerLog("mlsag_hash", { long_message }, "args"); // cnt is size_t const cnt = long_message.length; let res: string = ""; for (let i = 0; i < cnt; i++) { [res] = await this.send( INS.MLSAG, 0x02, i + 0x01, [i === cnt - 1 ? 0x00 : 0x80, long_message[i]], [32], ); ledgerLog("mlsag_hash", { res }, `iteration ${i}`); } if (!res) { throw Error("Return value of last exchange is empty string"); } ledgerLog("mlsag_hash", { res }, `ret`); return res; } public async mlsag_sign( c: Key, xx: KeyV, alpha: KeyV, rows: number, dsRows: number, ss: KeyV, ): Promise<KeyV> { ledgerLog( "mlsag_sign", { c, xx, alpha, rows, dsRows, ss, }, `args`, ); if (dsRows >= rows) { throw Error("dsRows greater than rows"); } if (xx.length !== rows) { throw Error("xx size does not match rows"); } if (alpha.length !== rows) { throw Error("alpha size does not match rows"); } for (let j = 0; j < dsRows; j++) { // ss[j] const [res] = await this.send( INS.MLSAG, 0x03, j + 1, [j === dsRows - 1 ? 0x80 : 0x00, xx[j], alpha[j]], [32], ); ss[j] = res; ledgerLog( "mlsag_sign", { "ss[j]": ss[j], ss, }, `iteration ${j}`, ); } for (let j = dsRows; j < rows; j++) { // sc_mulsub(const unsigned char *a, const unsigned char *b, const unsigned char *c) -> unsigned char *s // c - a.b mod l ss[j] = crypto.primitive_ops.sc_mulsub(c, xx[j], alpha[j]); ledgerLog( "mlsag_sign", { "ss[j]": ss[j], ss }, `sc_mulsub j:${j}`, ); } ledgerLog("mlsag_sign", { ss }, "ret"); return ss; } public async close_tx(): Promise<boolean> { ledgerLog("close_tx", { command: "close_tx" }); await this.send(INS.CLOSE_TX, 0x00, 0x00, [0x00]); return true; } // #endregion TRANSACTION // #region Internal private methods private is_fake_view_key(viewKey: string) { return viewKey === this.hexString(); } /** * @description Create a hex string by filling a array with the supplied value * and then converting it to a byte buffer, then to a string * @private * @param {number} [byteValue=0x00] * @param {number} [length=32] * @returns * @memberof XMR */ private hexString(byteValue: number = 0x00, length: number = 32) { return Buffer.alloc(length, byteValue, "hex").toString("hex"); } /** * * @description Generates hex string slices from a buffer * @private * @param {Buffer} buffer to buffer to slice and convert into hex strings * @param {number[]} endingIndicesToSliceAt An array of ending indices to slice at * * Ex. If [32,64] is supplied, the following slices will be returned: * * [buffer.slice(0,32).toString("hex"), buffer.slice(32,64).toString("hex")] * @memberof XMR */ private bufferToSlicedHexString( buffer: Buffer, endingIndicesToSliceAt: number[], ) { function sliceBufToHex(buf: Buffer, start: number, end: number) { // initialize a buffer of required size // so we dont slice out of bounds if returned bytes // is less than slice size const zeroBuf = Buffer.alloc(end); // copy data into zero buffer buf.copy(zeroBuf); // slice const slice = zeroBuf.slice(start, end).toString("hex"); return slice; } const res = endingIndicesToSliceAt.reduce( (prev, currEndSliceIdx, idx, slicingIndices) => { return [ ...prev, sliceBufToHex( buffer, !idx ? 0 : slicingIndices[idx - 1], currEndSliceIdx, ), ]; }, [], ); return res; } private arrLikeToBuf(arrLike: ArrLike) { return Array.isArray(arrLike) ? arrLike.reduce( (accu, curr) => typeof curr === "string" ? Buffer.concat([accu, Buffer.from(curr, "hex")]) : Buffer.concat([accu, Buffer.from([curr])]), Buffer.alloc(0), ) : arrLike; } private async send( ins: INS, p1: number, p2: number, data?: ArrLike | undefined, ): Promise<undefined>; private async send( ins: INS, p1: number, p2: number, data: ArrLike | undefined, endingIndicesToSliceAt: number[], ): Promise<string[]>; // #endregion Internal private methods private async send( ins: INS, p1: number, p2: number, data?: ArrLike | undefined, endingIndicesToSliceAt?: number[], ) { const serializedData = data ? this.arrLikeToBuf(data) : undefined; ledgerLog( "send", { ins, p1, p2, data, serializedDataHex: serializedData ? serializedData.toString("hex") : undefined, endingIndicesToSliceAt, }, "before_send_to_ledger", ); const buf = await this.transport.send( 0x00, ins, p1, p2, serializedData, ); if (!endingIndicesToSliceAt) { return; } else { ledgerLog( "send", { bufHex: buf.toString("hex") }, "returned buffer converted to hex string", ); const res = this.bufferToSlicedHexString( buf, endingIndicesToSliceAt, ); ledgerLog("send", { res }, "ret"); return res; } } private notSupported(): any { throw Error("This device function is not supported"); } }