UNPKG

edge-core-js

Version:

Edge account & wallet management library

1,558 lines (1,466 loc) 437 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var yaob = require('yaob'); var disklet = require('disklet'); var scryptJs = require('scrypt-js'); var rfc4648 = require('rfc4648'); var serverlet = require('serverlet'); var cleaners = require('cleaners'); var baseX = require('base-x'); var aesjs = require('aes-js'); var hashjs = require('hash.js'); var edgeSyncClient = require('edge-sync-client'); var redux = require('redux'); var reduxPixies = require('redux-pixies'); var biggystring = require('biggystring'); var currencyCodes = require('currency-codes'); var elliptic = require('elliptic'); var yavent = require('yavent'); var reduxKeto = require('redux-keto'); var crypto = require('crypto'); var fetch$1 = require('node-fetch'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var scryptJs__default = /*#__PURE__*/_interopDefaultLegacy(scryptJs); var baseX__default = /*#__PURE__*/_interopDefaultLegacy(baseX); var aesjs__default = /*#__PURE__*/_interopDefaultLegacy(aesjs); var hashjs__default = /*#__PURE__*/_interopDefaultLegacy(hashjs); var elliptic__default = /*#__PURE__*/_interopDefaultLegacy(elliptic); var crypto__default = /*#__PURE__*/_interopDefaultLegacy(crypto); var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch$1); function scrypt$2(data, salt, n, r, p, dklen) { return new Promise((resolve, reject) => { // The scrypt library will crash if it gets a Uint8Array > 64 bytes: const copy = []; for (let i = 0; i < data.length; ++i) copy[i] = data[i]; scryptJs__default["default"](copy, salt, n, r, p, dklen, (error, progress, key) => { if (error != null) return reject(error); if (key != null) return resolve(Uint8Array.from(key)); }); }); } /** * Generates deterministic "random" data for unit-testing. */ function makeFakeRandom() { let seed = 0; return bytes => { const out = new Uint8Array(bytes); for (let i = 0; i < bytes; ++i) { // Simplest numbers that give a full-period generator with // a good mix of high & low values within the first few bytes: seed = 5 * seed + 3 & 0xff; out[i] = seed; } return out; }; } const fakeFetch = () => { return Promise.reject(new Error('Fake network error')); }; /** * Creates a simulated io context object. */ function makeFakeIo() { const out = { // Crypto: random: makeFakeRandom(), scrypt: scrypt$2, // Local io: disklet: disklet.makeMemoryDisklet(), // Networking: fetch: fakeFetch, fetchCors: fakeFetch }; return out; } /** * Client-side EdgeAccount methods. */ class AccountSync { getFirstWalletInfo(type) { return this.allKeys.find(info => info.type === type); } getWalletInfo(id) { return this.allKeys.find(info => info.id === id); } listWalletIds() { return this.allKeys.map(info => info.id); } } yaob.shareData(AccountSync.prototype, 'AccountSync'); /** * Verifies that a password meets our suggested rules. */ function checkPasswordRules(password) { const tooShort = password.length < 10; const noNumber = !/[0-9]/.test(password); const noLowerCase = !/[a-z]/.test(password); const noUpperCase = !/[A-Z]/.test(password); // Quick & dirty password strength estimation: const charset = (/[0-9]/.test(password) ? 10 : 0) + (/[A-Z]/.test(password) ? 26 : 0) + (/[a-z]/.test(password) ? 26 : 0) + (/[^0-9A-Za-z]/.test(password) ? 30 : 0); const secondsToCrack = Math.pow(charset, password.length) / 1e6; return { secondsToCrack, tooShort, noNumber, noLowerCase, noUpperCase, passed: password.length >= 16 || !(tooShort || noNumber || noUpperCase || noLowerCase) }; } yaob.shareData({ checkPasswordRules }); /** * Normalizes a username, and checks for invalid characters. * TODO: Support a wider character range via Unicode normalization. */ function fixUsername(username) { const out = username.toLowerCase().replace(/[ \f\r\n\t\v]+/g, ' ').replace(/ $/, '').replace(/^ /, ''); for (let i = 0; i < out.length; ++i) { const c = out.charCodeAt(i); if (c < 0x20 || c > 0x7e) { throw new Error('Bad characters in username'); } } return out; } yaob.shareData({ fixUsername }); /** * Synchronously constructs a transaction stream. * This method creates a secret internal stream, * which differs slightly from the AsyncIterableIterator protocol * because of YAOB limitations. * It then wraps the internal stream object with the correct API. */ function streamTransactions(opts) { let stream; let streamClosed = false; const out = { next: async () => { if (stream == null) stream = await this.$internalStreamTransactions(opts); if (!streamClosed) { const out = await stream.next(); if (!out.done) return out; yaob.close(stream); streamClosed = true; } return { done: true, value: undefined }; }, /** * Closes the iterator early if the client doesn't want all the results. * This is necessary to prevent memory leaks over the bridge. */ return: async () => { if (stream != null && !streamClosed) { yaob.close(stream); streamClosed = true; } return { done: true, value: undefined }; }, [Symbol.asyncIterator]: () => out }; return out; } yaob.shareData({ streamTransactions }, 'CurrencyWalletSync'); /** * A string of hex-encoded binary data. */ const asBase16 = cleaners.asCodec(raw => rfc4648.base16.parse(cleaners.asString(raw)), clean => rfc4648.base16.stringify(clean).toLowerCase()); /** * A string of base32-encoded binary data. */ const asBase32 = cleaners.asCodec(raw => rfc4648.base32.parse(cleaners.asString(raw), { loose: true }), clean => rfc4648.base32.stringify(clean, { pad: false })); /** * A string of base64-encoded binary data. */ const asBase64 = cleaners.asCodec(raw => rfc4648.base64.parse(cleaners.asString(raw)), clean => rfc4648.base64.stringify(clean)); // --------------------------------------------------------------------- // public Edge types // --------------------------------------------------------------------- const asEdgePendingVoucher = cleaners.asObject({ voucherId: cleaners.asString, activates: cleaners.asDate, created: cleaners.asDate, ip: cleaners.asString, ipDescription: cleaners.asString, deviceDescription: cleaners.asOptional(cleaners.asString) }); // --------------------------------------------------------------------- // internal Edge types // --------------------------------------------------------------------- const asEdgeBox = cleaners.asObject({ encryptionType: cleaners.asNumber, data_base64: asBase64, iv_hex: asBase16 }); const asEdgeKeyBox = cleaners.asObject({ created: cleaners.asOptional(cleaners.asDate), data_base64: asBase64, encryptionType: cleaners.asNumber, iv_hex: asBase16 }); const asEdgeSnrp = cleaners.asObject({ salt_hex: asBase16, n: cleaners.asNumber, r: cleaners.asNumber, p: cleaners.asNumber }); const asEdgeLobbyRequest = cleaners.asObject({ loginRequest: cleaners.asOptional(cleaners.asObject({ appId: cleaners.asString }).withRest), publicKey: asBase64, timeout: cleaners.asOptional(cleaners.asNumber) }).withRest; const asEdgeLobbyReply = cleaners.asObject({ publicKey: asBase64, box: asEdgeBox }); /** * An array of base64-encoded hashed recovery answers. */ const asRecovery2Auth = cleaners.asArray(asBase64); // --------------------------------------------------------------------- // top-level request & response bodies // --------------------------------------------------------------------- const asLoginRequestBody = cleaners.asObject({ // The request payload: data: cleaners.asUnknown, // Common fields for all login methods: challengeId: cleaners.asOptional(cleaners.asString), deviceDescription: cleaners.asOptional(cleaners.asString), otp: cleaners.asOptional(cleaners.asString), syncToken: cleaners.asOptional(cleaners.asString), voucherId: cleaners.asOptional(cleaners.asString), voucherAuth: cleaners.asOptional(asBase64), // Secret-key login: loginId: cleaners.asOptional(asBase64), loginAuth: cleaners.asOptional(asBase64), // Password login: userId: cleaners.asOptional(asBase64), passwordAuth: cleaners.asOptional(asBase64), // PIN login: pin2Id: cleaners.asOptional(asBase64), pin2Auth: cleaners.asOptional(asBase64), // Recovery login: recovery2Id: cleaners.asOptional(asBase64), recovery2Auth: cleaners.asOptional(asRecovery2Auth), // Messages: loginIds: cleaners.asOptional(cleaners.asArray(asBase64)), // OTP reset: otpResetAuth: cleaners.asOptional(cleaners.asString), // Legacy: did: cleaners.asOptional(cleaners.asString), l1: cleaners.asOptional(asBase64), lp1: cleaners.asOptional(asBase64), lpin1: cleaners.asOptional(asBase64), lra1: cleaners.asOptional(asBase64), recoveryAuth: cleaners.asOptional(asBase64) // lra1 }); const asLoginResponseBody = cleaners.asObject({ // The response payload: results: cleaners.asOptional(cleaners.asUnknown), // What type of response is this (success or failure)?: status_code: cleaners.asNumber, message: cleaners.asString }); // --------------------------------------------------------------------- // request payloads // --------------------------------------------------------------------- const asChangeOtpPayload = cleaners.asObject({ otpTimeout: cleaners.asOptional(cleaners.asNumber, 7 * 24 * 60 * 60), // seconds otpKey: asBase32 }); const asChangePasswordPayload = cleaners.asObject({ passwordAuth: asBase64, passwordAuthBox: asEdgeBox, passwordAuthSnrp: asEdgeSnrp, passwordBox: asEdgeBox, passwordKeySnrp: asEdgeSnrp }); const asChangePin2IdPayload = cleaners.asObject({ pin2Id: asBase64 }); const asChangePin2Payload = cleaners.asObject({ pin2Id: cleaners.asOptional(asBase64), pin2Auth: cleaners.asOptional(asBase64), pin2Box: cleaners.asOptional(asEdgeBox), pin2KeyBox: cleaners.asOptional(asEdgeBox), pin2TextBox: asEdgeBox }); const asChangeRecovery2IdPayload = cleaners.asObject({ recovery2Id: asBase64 }); const asChangeRecovery2Payload = cleaners.asObject({ recovery2Id: asBase64, recovery2Auth: asRecovery2Auth, recovery2Box: asEdgeBox, recovery2KeyBox: asEdgeBox, question2Box: asEdgeBox }); const asChangeSecretPayload = cleaners.asObject({ loginAuthBox: asEdgeBox, loginAuth: asBase64 }); const asChangeUsernamePayload = cleaners.asObject({ userId: asBase64, userTextBox: asEdgeBox }); const asChangeVouchersPayload = cleaners.asObject({ approvedVouchers: cleaners.asOptional(cleaners.asArray(cleaners.asString)), rejectedVouchers: cleaners.asOptional(cleaners.asArray(cleaners.asString)) }); const asCreateKeysPayload = cleaners.asObject({ keyBoxes: cleaners.asArray(asEdgeBox), newSyncKeys: cleaners.asOptional(cleaners.asArray(cleaners.asString), () => []) }); const asCreateLoginPayload = cleaners.asObject({ appId: cleaners.asString, loginId: asBase64, parentBox: cleaners.asOptional(asEdgeBox) }); // --------------------------------------------------------------------- // response payloads // --------------------------------------------------------------------- const asChallengeErrorPayload = cleaners.asObject({ challengeId: cleaners.asString, challengeUri: cleaners.asString }); const asCreateChallengePayload = cleaners.asObject({ challengeId: cleaners.asString, challengeUri: cleaners.asOptional(cleaners.asString) }); const asLobbyPayload = cleaners.asObject({ request: asEdgeLobbyRequest, replies: cleaners.asArray(asEdgeLobbyReply) }); const asTrue = cleaners.asValue(true); const asLoginPayload = cleaners.asObject({ // Identity: appId: cleaners.asString, created: cleaners.asDate, loginId: asBase64, syncToken: cleaners.asOptional(cleaners.asString), // Nested logins: children: cleaners.asOptional(cleaners.asArray(raw => asLoginPayload(raw))), parentBox: cleaners.asOptional(asEdgeBox), // 2-factor login: otpKey: cleaners.asOptional(cleaners.asEither(asTrue, asBase32)), otpResetDate: cleaners.asOptional(cleaners.asDate), otpTimeout: cleaners.asOptional(cleaners.asNumber), // Password login: passwordAuthBox: cleaners.asOptional(asEdgeBox), passwordAuthSnrp: cleaners.asOptional(asEdgeSnrp), passwordBox: cleaners.asOptional(cleaners.asEither(asTrue, asEdgeBox)), passwordKeySnrp: cleaners.asOptional(asEdgeSnrp), // PIN v2 login: pin2Box: cleaners.asOptional(cleaners.asEither(asTrue, asEdgeBox)), pin2KeyBox: cleaners.asOptional(asEdgeBox), pin2TextBox: cleaners.asOptional(asEdgeBox), // Recovery v2 login: question2Box: cleaners.asOptional(asEdgeBox), recovery2Box: cleaners.asOptional(cleaners.asEither(asTrue, asEdgeBox)), recovery2KeyBox: cleaners.asOptional(asEdgeBox), // Secret-key login: loginAuthBox: cleaners.asOptional(asEdgeBox), // Username: userId: cleaners.asOptional(asBase64), userTextBox: cleaners.asOptional(asEdgeBox), // Voucher login: pendingVouchers: cleaners.asOptional(cleaners.asArray(asEdgePendingVoucher), () => []), // Resources: keyBoxes: cleaners.asOptional(cleaners.asArray(asEdgeKeyBox)), mnemonicBox: cleaners.asOptional(asEdgeBox), rootKeyBox: cleaners.asOptional(asEdgeBox), syncKeyBox: cleaners.asOptional(asEdgeBox) }); const asMessagesPayload = cleaners.asArray(cleaners.asObject({ loginId: asBase64, otpResetPending: cleaners.asOptional(cleaners.asBoolean, false), pendingVouchers: cleaners.asOptional(cleaners.asArray(asEdgePendingVoucher), () => []), recovery2Corrupt: cleaners.asOptional(cleaners.asBoolean, false) })); const asOtpErrorPayload = cleaners.asObject({ login_id: cleaners.asOptional(asBase64), otp_reset_auth: cleaners.asOptional(cleaners.asString), otp_timeout_date: cleaners.asOptional(cleaners.asDate), reason: cleaners.asOptional(cleaners.asValue('ip', 'otp'), 'otp'), voucher_activates: cleaners.asOptional(cleaners.asDate), voucher_auth: cleaners.asOptional(asBase64), voucher_id: cleaners.asOptional(cleaners.asString) }); const asOtpResetPayload = cleaners.asObject({ otpResetDate: cleaners.asDate }); const asPasswordErrorPayload = cleaners.asObject({ wait_seconds: cleaners.asOptional(cleaners.asNumber) }); const asRecovery2InfoPayload = cleaners.asObject({ question2Box: asEdgeBox }); const asUsernameInfoPayload = cleaners.asObject({ loginId: asBase64, // Password login: passwordAuthSnrp: cleaners.asOptional(asEdgeSnrp), // Recovery v1 login: questionBox: cleaners.asOptional(asEdgeBox), questionKeySnrp: cleaners.asOptional(asEdgeSnrp), recoveryAuthSnrp: cleaners.asOptional(asEdgeSnrp) }); // --------------------------------------------------------------------- // uncleaners // --------------------------------------------------------------------- // Common types: const wasEdgeBox = cleaners.uncleaner(asEdgeBox); const wasEdgeLobbyReply = cleaners.uncleaner(asEdgeLobbyReply); const wasEdgeLobbyRequest = cleaners.uncleaner(asEdgeLobbyRequest); // Top-level request / response bodies: const wasLoginRequestBody = cleaners.uncleaner(asLoginRequestBody); const wasLoginResponseBody = cleaners.uncleaner(asLoginResponseBody); // Request payloads: const wasChangeOtpPayload = cleaners.uncleaner(asChangeOtpPayload); const wasChangePasswordPayload = cleaners.uncleaner(asChangePasswordPayload); const wasChangePin2IdPayload = cleaners.uncleaner(asChangePin2IdPayload); const wasChangePin2Payload = cleaners.uncleaner(asChangePin2Payload); const wasChangeRecovery2IdPayload = cleaners.uncleaner(asChangeRecovery2IdPayload); const wasChangeRecovery2Payload = cleaners.uncleaner(asChangeRecovery2Payload); const wasChangeSecretPayload = cleaners.uncleaner(asChangeSecretPayload); const wasChangeUsernamePayload = cleaners.uncleaner(asChangeUsernamePayload); const wasChangeVouchersPayload = cleaners.uncleaner(asChangeVouchersPayload); const wasCreateKeysPayload = cleaners.uncleaner(asCreateKeysPayload); const wasCreateLoginPayload = cleaners.uncleaner(asCreateLoginPayload); // Response payloads: const wasChallengeErrorPayload = cleaners.uncleaner(asChallengeErrorPayload); const wasCreateChallengePayload = cleaners.uncleaner(asCreateChallengePayload); const wasLobbyPayload = cleaners.uncleaner(asLobbyPayload); const wasLoginPayload = cleaners.uncleaner(asLoginPayload); const wasMessagesPayload = cleaners.uncleaner(asMessagesPayload); const wasOtpErrorPayload = cleaners.uncleaner(asOtpErrorPayload); const wasOtpResetPayload = cleaners.uncleaner(asOtpResetPayload); const wasPasswordErrorPayload = cleaners.uncleaner(asPasswordErrorPayload); const wasRecovery2InfoPayload = cleaners.uncleaner(asRecovery2InfoPayload); const wasUsernameInfoPayload = cleaners.uncleaner(asUsernameInfoPayload); const asEdgeRepoDump = cleaners.asObject(asEdgeBox); const asEdgeVoucherDump = cleaners.asObject({ // Identity: loginId: asBase64, voucherAuth: asBase64, voucherId: cleaners.asString, // Login capability: created: cleaners.asDate, activates: cleaners.asDate, // Automatically becomes approved on this date status: cleaners.asValue('pending', 'approved', 'rejected'), // Information about the login: ip: cleaners.asString, ipDescription: cleaners.asString, deviceDescription: cleaners.asOptional(cleaners.asString) }); const asEdgeLoginDump = cleaners.asObject({ // Identity: appId: cleaners.asString, created: cleaners.asOptional(cleaners.asDate, () => new Date()), loginId: asBase64, // Nested logins: children: cleaners.asOptional(cleaners.asArray(raw => asEdgeLoginDump(raw)), () => []), parentBox: cleaners.asOptional(asEdgeBox), parentId: () => undefined, // 2-factor login: otpKey: cleaners.asOptional(asBase32), otpResetAuth: cleaners.asOptional(cleaners.asString), otpResetDate: cleaners.asOptional(cleaners.asDate), otpTimeout: cleaners.asOptional(cleaners.asNumber), // Password login: passwordAuth: cleaners.asOptional(asBase64), passwordAuthBox: cleaners.asOptional(asEdgeBox), passwordAuthSnrp: cleaners.asOptional(asEdgeSnrp), passwordBox: cleaners.asOptional(asEdgeBox), passwordKeySnrp: cleaners.asOptional(asEdgeSnrp), // PIN v2 login: pin2Id: cleaners.asOptional(asBase64), pin2Auth: cleaners.asOptional(asBase64), pin2Box: cleaners.asOptional(asEdgeBox), pin2KeyBox: cleaners.asOptional(asEdgeBox), pin2TextBox: cleaners.asOptional(asEdgeBox), // Recovery v2 login: recovery2Id: cleaners.asOptional(asBase64), recovery2Auth: cleaners.asOptional(asRecovery2Auth), question2Box: cleaners.asOptional(asEdgeBox), recovery2Box: cleaners.asOptional(asEdgeBox), recovery2KeyBox: cleaners.asOptional(asEdgeBox), // Secret-key login: loginAuth: cleaners.asOptional(asBase64), loginAuthBox: cleaners.asOptional(asEdgeBox), // Username: userId: cleaners.asOptional(asBase64), userTextBox: cleaners.asOptional(asEdgeBox), // Keys and assorted goodies: keyBoxes: cleaners.asOptional(cleaners.asArray(asEdgeKeyBox), () => []), mnemonicBox: cleaners.asOptional(asEdgeBox), rootKeyBox: cleaners.asOptional(asEdgeBox), syncKeyBox: cleaners.asOptional(asEdgeBox), vouchers: cleaners.asOptional(cleaners.asArray(asEdgeVoucherDump), () => []), // Obsolete: pinBox: cleaners.asOptional(asEdgeBox), pinId: cleaners.asOptional(cleaners.asString), pinKeyBox: cleaners.asOptional(asEdgeBox) }); const wasEdgeLoginDump = cleaners.uncleaner(asEdgeLoginDump); const wasEdgeRepoDump = cleaners.uncleaner(asEdgeRepoDump); const base58Codec = baseX__default["default"]('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'); const base58 = { parse(text) { return base58Codec.decode(text); }, stringify(data) { return base58Codec.encode(data); } }; const utf8 = { parse(text) { const byteString = encodeURI(text); const out = new Uint8Array(byteString.length); // Treat each character as a byte, except for %XX escape sequences: let di = 0; // Destination index for (let i = 0; i < byteString.length; ++i) { const c = byteString.charCodeAt(i); if (c === 0x25) { out[di++] = parseInt(byteString.slice(i + 1, i + 3), 16); i += 2; } else { out[di++] = c; } } // Trim any over-allocated space (zero-copy): return out.subarray(0, di); }, stringify(data) { // Create a %XX escape sequence for each input byte: let byteString = ''; for (let i = 0; i < data.length; ++i) { const byte = data[i]; byteString += '%' + (byte >> 4).toString(16) + (byte & 0xf).toString(16); } return decodeURIComponent(byteString); } }; /** * We only accept *.edge.app or localhost as valid domain names. */ function validateServer(server) { const url = new URL(server); if (url.protocol === 'http:' || url.protocol === 'ws:') { if (url.hostname === 'localhost') return; } if (url.protocol === 'https:' || url.protocol === 'wss:') { if (url.hostname === 'localhost') return; if (/^([A-Za-z0-9_-]+\.)*edge(test)?\.app$/.test(url.hostname)) return; } throw new Error(`Only *.edge.app or localhost are valid login domain names, not ${url.hostname}`); } /* * These are errors the core knows about. * * The GUI should handle these errors in an "intelligent" way, such as by * displaying a localized error message or asking the user for more info. * All these errors have a `name` field, which the GUI can use to select * the appropriate response. * * Other errors are possible, of course, since the Javascript language * itself can generate exceptions. Those errors won't have a `type` field, * and the GUI should just show them with a stack trace & generic message, * since the program has basically crashed at that point. */ /** * Thrown when the login server requires a CAPTCHA. * * After showing the WebView with the challengeUri, * pass the challengeId to the login method * (such as loginWithPassword) to complete the login. * * The challengeUri web page will signal that it is done by navigating * to a new location that ends with either /success or /failure, * such as https://login.edge.app/challenge/success * The login UI can use this as a signal to close the WebView. */ function ChallengeError(resultsJson, message = 'Login requires a CAPTCHA') { if (!(this instanceof ChallengeError)) throw new TypeError("Class constructor ChallengeError cannot be invoked without 'new'"); var _this; function _super(message) { _this = new Error(message); Object.defineProperty(_this, "constructor", { value: ChallengeError, configurable: true, writable: true }); return _this; } _super(message); _this.name = 'ChallengeError'; _this.challengeId = resultsJson.challengeId; _this.challengeUri = resultsJson.challengeUri; return _this; } /** * Trying to spend an uneconomically small amount of money. */ function DustSpendError(message = 'Please send a larger amount') { if (!(this instanceof DustSpendError)) throw new TypeError("Class constructor DustSpendError cannot be invoked without 'new'"); var _this2; function _super2(message) { _this2 = new Error(message); Object.defineProperty(_this2, "constructor", { value: DustSpendError, configurable: true, writable: true }); return _this2; } _super2(message); _this2.name = 'DustSpendError'; return _this2; } /** * Trying to spend more money than the wallet contains. */ function InsufficientFundsError(opts) { if (!(this instanceof InsufficientFundsError)) throw new TypeError("Class constructor InsufficientFundsError cannot be invoked without 'new'"); var _this3; function _super3(message) { _this3 = new Error(message); Object.defineProperty(_this3, "constructor", { value: InsufficientFundsError, configurable: true, writable: true }); return _this3; } const { tokenId = null, networkFee } = opts ?? {}; _super3(`Insufficient ${tokenId ?? 'funds'}`); _this3.tokenId = tokenId; _this3.networkFee = networkFee; _this3.name = 'InsufficientFundsError'; return _this3; } /** * Could not reach the server at all. */ function NetworkError(message = 'Cannot reach the network') { if (!(this instanceof NetworkError)) throw new TypeError("Class constructor NetworkError cannot be invoked without 'new'"); var _this4; function _super4(message) { _this4 = new Error(message); Object.defineProperty(_this4, "constructor", { value: NetworkError, configurable: true, writable: true }); return _this4; } _super4(message); _this4.name = 'NetworkError'; return _this4; } /** * Attempting to create a MakeSpend without specifying an amount of currency to send */ function NoAmountSpecifiedError(message = 'Unable to create zero-amount transaction.') { if (!(this instanceof NoAmountSpecifiedError)) throw new TypeError("Class constructor NoAmountSpecifiedError cannot be invoked without 'new'"); var _this5; function _super5(message) { _this5 = new Error(message); Object.defineProperty(_this5, "constructor", { value: NoAmountSpecifiedError, configurable: true, writable: true }); return _this5; } _super5(message); _this5.name = 'NoAmountSpecifiedError'; return _this5; } /** * The endpoint on the server is obsolete, and the app needs to be upgraded. */ function ObsoleteApiError(message = 'The application is too old. Please upgrade.') { if (!(this instanceof ObsoleteApiError)) throw new TypeError("Class constructor ObsoleteApiError cannot be invoked without 'new'"); var _this6; function _super6(message) { _this6 = new Error(message); Object.defineProperty(_this6, "constructor", { value: ObsoleteApiError, configurable: true, writable: true }); return _this6; } _super6(message); _this6.name = 'ObsoleteApiError'; return _this6; } /** * The OTP token was missing / incorrect. * * The error object should include a `resetToken` member, * which can be used to reset OTP protection on the account. * * The error object may include a `resetDate` member, * which indicates that an OTP reset is already pending, * and when it will complete. */ function OtpError(resultsJson, message = 'Invalid OTP token') { if (!(this instanceof OtpError)) throw new TypeError("Class constructor OtpError cannot be invoked without 'new'"); var _this7; function _super7(message) { _this7 = new Error(message); Object.defineProperty(_this7, "constructor", { value: OtpError, configurable: true, writable: true }); return _this7; } _super7(message); _this7.name = 'OtpError'; _this7.reason = 'otp'; const clean = cleaners.asMaybe(asOtpErrorPayload)(resultsJson); if (clean == null) return; if (clean.login_id != null) { _this7.loginId = rfc4648.base64.stringify(clean.login_id); } _this7.resetToken = clean.otp_reset_auth; _this7.reason = clean.reason; _this7.resetDate = clean.otp_timeout_date; _this7.voucherActivates = clean.voucher_activates; if (clean.voucher_auth != null) { _this7.voucherAuth = rfc4648.base64.stringify(clean.voucher_auth); } _this7.voucherId = clean.voucher_id; return _this7; } /** * The provided authentication is incorrect. * * Reasons could include: * - Password login: wrong password * - PIN login: wrong PIN * - Recovery login: wrong answers * * The error object may include a `wait` member, * which is the number of seconds the user must wait before trying again. */ function PasswordError(resultsJson, message = 'Invalid password') { if (!(this instanceof PasswordError)) throw new TypeError("Class constructor PasswordError cannot be invoked without 'new'"); var _this8; function _super8(message) { _this8 = new Error(message); Object.defineProperty(_this8, "constructor", { value: PasswordError, configurable: true, writable: true }); return _this8; } _super8(message); _this8.name = 'PasswordError'; const clean = cleaners.asMaybe(asPasswordErrorPayload)(resultsJson); if (clean == null) return; _this8.wait = clean.wait_seconds; return _this8; } /** * PIN login is not enabled for this account on this device. */ function PinDisabledError(message) { if (!(this instanceof PinDisabledError)) throw new TypeError("Class constructor PinDisabledError cannot be invoked without 'new'"); var _this9; function _super9(message) { _this9 = new Error(message); Object.defineProperty(_this9, "constructor", { value: PinDisabledError, configurable: true, writable: true }); return _this9; } _super9(message); _this9.name = 'PinDisabledError'; return _this9; } /** * Trying to spend funds that are not yet confirmed. */ function PendingFundsError(message = 'Not enough confirmed funds') { if (!(this instanceof PendingFundsError)) throw new TypeError("Class constructor PendingFundsError cannot be invoked without 'new'"); var _this10; function _super10(message) { _this10 = new Error(message); Object.defineProperty(_this10, "constructor", { value: PendingFundsError, configurable: true, writable: true }); return _this10; } _super10(message); _this10.name = 'PendingFundsError'; return _this10; } /** * Attempting to shape shift between two wallets of same currency. */ function SameCurrencyError(message = 'Wallets can not be the same currency') { if (!(this instanceof SameCurrencyError)) throw new TypeError("Class constructor SameCurrencyError cannot be invoked without 'new'"); var _this11; function _super11(message) { _this11 = new Error(message); Object.defineProperty(_this11, "constructor", { value: SameCurrencyError, configurable: true, writable: true }); return _this11; } _super11(message); _this11.name = 'SameCurrencyError'; return _this11; } /** * Trying to spend to an address of the source wallet */ function SpendToSelfError(message = 'Spending to self') { if (!(this instanceof SpendToSelfError)) throw new TypeError("Class constructor SpendToSelfError cannot be invoked without 'new'"); var _this12; function _super12(message) { _this12 = new Error(message); Object.defineProperty(_this12, "constructor", { value: SpendToSelfError, configurable: true, writable: true }); return _this12; } _super12(message); _this12.name = 'SpendToSelfError'; return _this12; } /** * Trying to swap an amount that is either too low or too high. * @param nativeMax the maximum supported amount, in the currency specified * by the direction (defaults to "from" currency) */ function SwapAboveLimitError(swapInfo, nativeMax, direction = 'from') { if (!(this instanceof SwapAboveLimitError)) throw new TypeError("Class constructor SwapAboveLimitError cannot be invoked without 'new'"); var _this13; function _super13(message) { _this13 = new Error(message); Object.defineProperty(_this13, "constructor", { value: SwapAboveLimitError, configurable: true, writable: true }); return _this13; } _super13('Amount is too high'); _this13.name = 'SwapAboveLimitError'; _this13.pluginId = swapInfo.pluginId; _this13.swapPluginId = swapInfo.pluginId; _this13.nativeMax = nativeMax ?? ''; _this13.direction = direction; return _this13; } /** * Trying to swap an amount that is either too low or too high. * @param nativeMin the minimum supported amount, in the currency specified * by the direction (defaults to "from" currency) */ function SwapBelowLimitError(swapInfo, nativeMin, direction = 'from') { if (!(this instanceof SwapBelowLimitError)) throw new TypeError("Class constructor SwapBelowLimitError cannot be invoked without 'new'"); var _this14; function _super14(message) { _this14 = new Error(message); Object.defineProperty(_this14, "constructor", { value: SwapBelowLimitError, configurable: true, writable: true }); return _this14; } _super14('Amount is too low'); _this14.name = 'SwapBelowLimitError'; _this14.pluginId = swapInfo.pluginId; _this14.swapPluginId = swapInfo.pluginId; _this14.nativeMin = nativeMin ?? ''; _this14.direction = direction; return _this14; } /** * The swap plugin does not support this currency pair. */ function SwapCurrencyError(swapInfo, request) { if (!(this instanceof SwapCurrencyError)) throw new TypeError("Class constructor SwapCurrencyError cannot be invoked without 'new'"); var _this15; function _super15(message) { _this15 = new Error(message); Object.defineProperty(_this15, "constructor", { value: SwapCurrencyError, configurable: true, writable: true }); return _this15; } const { fromWallet, toWallet, fromTokenId, toTokenId } = request; const fromPluginId = fromWallet.currencyConfig.currencyInfo.pluginId; const toPluginId = toWallet.currencyConfig.currencyInfo.pluginId; const fromString = `${fromPluginId}:${String(fromTokenId)}`; const toString = `${toPluginId}:${String(toTokenId)}`; _super15(`${swapInfo.displayName} does not support ${fromString} to ${toString}`); _this15.name = 'SwapCurrencyError'; _this15.pluginId = swapInfo.pluginId; _this15.fromTokenId = fromTokenId ?? null; _this15.toTokenId = toTokenId ?? null; return _this15; } /** * The user is not allowed to swap these coins for some reason * (no KYC, restricted IP address, etc...). * @param reason A string giving the reason for the denial. * - 'geoRestriction': The IP address is in a restricted region * - 'noVerification': The user needs to provide KYC credentials * - 'needsActivation': The user needs to log into the service. */ function SwapPermissionError(swapInfo, reason) { if (!(this instanceof SwapPermissionError)) throw new TypeError("Class constructor SwapPermissionError cannot be invoked without 'new'"); var _this16; function _super16(message) { _this16 = new Error(message); Object.defineProperty(_this16, "constructor", { value: SwapPermissionError, configurable: true, writable: true }); return _this16; } if (reason != null) _super16(reason);else _super16('You are not allowed to make this trade'); _this16.name = 'SwapPermissionError'; _this16.pluginId = swapInfo.pluginId; _this16.reason = reason; return _this16; } // Address requirements for certain swap flows (extend as needed): function SwapAddressError(swapInfo, opts) { if (!(this instanceof SwapAddressError)) throw new TypeError("Class constructor SwapAddressError cannot be invoked without 'new'"); var _this17; function _super17(message) { _this17 = new Error(message); Object.defineProperty(_this17, "constructor", { value: SwapAddressError, configurable: true, writable: true }); return _this17; } const { reason } = opts; switch (reason) { case 'mustMatch': _super17('This swap requires from and to wallets to have the same address'); break; case 'mustBeActivated': _super17('The destination wallet must be activated to receive this swap.'); break; default: _super17('Invalid swap address'); } _this17.name = 'SwapAddressError'; _this17.swapPluginId = swapInfo.pluginId; _this17.reason = reason; return _this17; } /** * Cannot find a login with that id. * * Reasons could include: * - Password login: wrong username * - PIN login: wrong PIN key * - Recovery login: wrong username, or wrong recovery key */ function UsernameError(message = 'Invalid username') { if (!(this instanceof UsernameError)) throw new TypeError("Class constructor UsernameError cannot be invoked without 'new'"); var _this18; function _super18(message) { _this18 = new Error(message); Object.defineProperty(_this18, "constructor", { value: UsernameError, configurable: true, writable: true }); return _this18; } _super18(message); _this18.name = 'UsernameError'; return _this18; } function asMaybeError(name) { return function asError(raw) { if (raw instanceof Error && raw.name === name) { const typeHack = raw; return typeHack; } }; } const asMaybeChallengeError = asMaybeError('ChallengeError'); const asMaybeDustSpendError = asMaybeError('DustSpendError'); const asMaybeInsufficientFundsError = asMaybeError('InsufficientFundsError'); const asMaybeNetworkError = asMaybeError('NetworkError'); const asMaybeNoAmountSpecifiedError = asMaybeError('NoAmountSpecifiedError'); const asMaybeObsoleteApiError = asMaybeError('ObsoleteApiError'); const asMaybeOtpError = asMaybeError('OtpError'); const asMaybePasswordError = asMaybeError('PasswordError'); const asMaybePinDisabledError = asMaybeError('PinDisabledError'); const asMaybePendingFundsError = asMaybeError('PendingFundsError'); const asMaybeSameCurrencyError = asMaybeError('SameCurrencyError'); const asMaybeSpendToSelfError = asMaybeError('SpendToSelfError'); const asMaybeSwapAboveLimitError = asMaybeError('SwapAboveLimitError'); const asMaybeSwapBelowLimitError = asMaybeError('SwapBelowLimitError'); const asMaybeSwapCurrencyError = asMaybeError('SwapCurrencyError'); const asMaybeSwapPermissionError = asMaybeError('SwapPermissionError'); const asMaybeSwapAddressError = asMaybeError('SwapAddressError'); const asMaybeUsernameError = asMaybeError('UsernameError'); function hmacSha1(data, key) { // @ts-expect-error const hmac = hashjs__default["default"].hmac(hashjs__default["default"].sha1, key); return Uint8Array.from(hmac.update(data).digest()); } function hmacSha256(data, key) { // @ts-expect-error const hmac = hashjs__default["default"].hmac(hashjs__default["default"].sha256, key); return Uint8Array.from(hmac.update(data).digest()); } function sha256(data) { const hash = hashjs__default["default"].sha256(); return Uint8Array.from(hash.update(data).digest()); } /** * Compares two byte arrays without data-dependent branches. * Returns true if they match. */ function verifyData(a, b) { const length = a.length; if (length !== b.length) return false; let out = 0; for (let i = 0; i < length; ++i) out |= a[i] ^ b[i]; return out === 0; } const AesCbc = aesjs__default["default"].ModeOfOperation.cbc; /** * Some of our data contains terminating null bytes due to an old bug, * so this function handles text decryption as a special case. */ function decryptText(box, key) { const data = decrypt(box, key); if (data[data.length - 1] === 0) { return utf8.stringify(data.subarray(0, -1)); } return utf8.stringify(data); } /** * @param box an Airbitz JSON encryption box * @param key a key, as an ArrayBuffer */ function decrypt(box, key) { // Check JSON: if (box.encryptionType !== 0) { throw new Error('Unknown encryption type'); } const iv = box.iv_hex; const ciphertext = box.data_base64; // Decrypt: const cipher = new AesCbc(key, iv); const raw = cipher.decrypt(ciphertext); // Calculate data locations: const headerStart = 1; const headerSize = raw[0]; const dataStart = headerStart + headerSize + 4; const dataSize = raw[dataStart - 4] << 24 | raw[dataStart - 3] << 16 | raw[dataStart - 2] << 8 | raw[dataStart - 1]; const footerStart = dataStart + dataSize + 1; const footerSize = raw[footerStart - 1]; const hashStart = footerStart + footerSize; const paddingStart = hashStart + 32; // Verify SHA-256 checksum: const hash = sha256(raw.subarray(0, hashStart)); if (!verifyData(hash, raw.subarray(hashStart, paddingStart))) { throw new Error('Invalid checksum'); } // Verify pkcs7 padding: const padding = pkcs7(paddingStart); if (!verifyData(padding, raw.subarray(paddingStart))) { throw new Error('Invalid PKCS7 padding'); } // Return the payload: return raw.subarray(dataStart, dataStart + dataSize); } /** * @param payload an ArrayBuffer of data * @param key a key, as an ArrayBuffer */ function encrypt(io, data, key) { // Calculate data locations: const headerStart = 1; const headerSize = io.random(1)[0] & 0x1f; const dataStart = headerStart + headerSize + 4; const dataSize = data.length; const footerStart = dataStart + dataSize + 1; const footerSize = io.random(1)[0] & 0x1f; const hashStart = footerStart + footerSize; const paddingStart = hashStart + 32; // Initialize the buffer with padding: const padding = pkcs7(paddingStart); const raw = new Uint8Array(paddingStart + padding.length); raw.set(padding, paddingStart); // Add header: raw[0] = headerSize; raw.set(io.random(headerSize), headerStart); // Add payload: raw[dataStart - 4] = dataSize >> 24 & 0xff; raw[dataStart - 3] = dataSize >> 16 & 0xff; raw[dataStart - 2] = dataSize >> 8 & 0xff; raw[dataStart - 1] = dataSize & 0xff; raw.set(data, dataStart); // Add footer: raw[footerStart - 1] = footerSize; raw.set(io.random(footerSize), footerStart); // Add SHA-256 checksum: raw.set(sha256(raw.subarray(0, hashStart)), hashStart); // Encrypt to JSON: const iv = io.random(16); const cipher = new AesCbc(key, iv); const ciphertext = cipher.encrypt(raw); return { encryptionType: 0, iv_hex: iv, data_base64: ciphertext }; } /** * Generates the pkcs7 padding data that should be appended to * data of a particular length. */ function pkcs7(length) { const out = new Uint8Array(16 - (length & 0xf)); for (let i = 0; i < out.length; ++i) out[i] = out.length; return out; } function numberToBe64(number) { const high = Math.floor(number / 0x100000000); return new Uint8Array([high >> 24 & 0xff, high >> 16 & 0xff, high >> 8 & 0xff, high & 0xff, number >> 24 & 0xff, number >> 16 & 0xff, number >> 8 & 0xff, number & 0xff]); } /** * Implements the rfc4226 HOTP specification. * @param {*} secret The secret value, K, from rfc4226 * @param {*} counter The counter, C, from rfc4226 * @param {*} digits The number of digits to generate */ function hotp(secret, counter, digits) { const hmac = hmacSha1(numberToBe64(counter), secret); const offset = hmac[19] & 0xf; const p = (hmac[offset] & 0x7f) << 24 | hmac[offset + 1] << 16 | hmac[offset + 2] << 8 | hmac[offset + 3]; const text = p.toString(); const padding = Array(digits).join('0'); return (padding + text).slice(-digits); } /** * Generates an HOTP code based on the current time. */ function totp(secret, now = Date.now() / 1000) { return hotp(secret, now / 30, 6); } /** * Validates a TOTP code based on the current time, * within an adjustable range. */ function checkTotp(secret, otp, opts = {}) { const { now = Date.now() / 1000, spread = 1 } = opts; const index = now / 30; // Try the middle: if (otp === hotp(secret, index, 6)) return true; // Spiral outwards: for (let i = 1; i <= spread; ++i) { if (otp === hotp(secret, index - i, 6)) return true; if (otp === hotp(secret, index + i, 6)) return true; } return false; } /** * Safely concatenate a bunch of arrays, which may or may not exist. * Purrs quietly when pet. */ function softCat(...lists) { const out = []; return out.concat(...lists.filter(list => list != null)); } /** * Like `Object.assign`, but makes the properties non-enumerable. */ function addHiddenProperties(object, properties) { for (const name of Object.keys(properties)) { Object.defineProperty(object, name, { writable: true, configurable: true, // @ts-expect-error value: properties[name] }); } return object; } /** * Waits for the first successful promise. * If no promise succeeds, returns the last failure. */ /** * If the promise doesn't resolve in the given time, * reject it with the provided error, or a generic error if none is provided. */ function timeout(promise, ms, error = new Error(`Timeout of ${ms}ms exceeded`)) { return new Promise((resolve, reject) => { const timer = setTimeout(() => reject(error), ms); promise.then(ok => { clearTimeout(timer); resolve(ok); }, error => { clearTimeout(timer); reject(error); }); }); } /** * Waits for a collection of promises. * Returns all the promises that manage to resolve within the timeout. * If no promises mange to resolve within the timeout, * returns the first promise that resolves. * If all promises reject, rejects an array of errors. */ function fuzzyTimeout(promises, timeoutMs) { return new Promise((resolve, reject) => { let done = false; const results = []; const errors = []; // Set up the timer: let timer = setTimeout(() => { timer = undefined; if (results.length > 0) { done = true; resolve({ results, errors }); } }, timeoutMs); function checkEnd() { const allDone = results.length + errors.length === promises.length; if (allDone && timer != null) { clearTimeout(timer); } if (allDone || timer == null) { done = true; if (results.length > 0) resolve({ results, errors });else reject(errors); } } checkEnd(); // Handle empty lists // Attach to the promises: for (const promise of promises) { promise.then(result => { if (done) return; results.push(result); checkEnd(); }, failure => { if (done) return; errors.push(failure); checkEnd(); }); } }); } function parseReply(json) { const clean = asLoginResponseBody(json); switch (clean.status_code) { case 0: // Success return clean.results; case 2: // Account exists throw new UsernameError('Account already exists on server'); case 3: // Account does not exist throw new UsernameError('Account does not exist on server'); case 4: // Invalid password case 5: // Invalid answers throw new PasswordError(clean.results); case 6: // Invalid API key throw new Error('Invalid API key'); case 8: // Invalid OTP throw new OtpError(clean.results); case 1000: // Endpoint obsolete throw new ObsoleteApiError(); case 1: // Error case 7: // Pin expired case 9: // Invalid voucher case 10: // Conflicting change case 11: // Rate limiting default: { const results = cleaners.asMaybe(asChallengeErrorPayload)(clean.results); if (results != null) { throw new ChallengeError(results); } throw new Error(`Server error: ${clean.message}`); } } } /** * Picks a random login server and makes a request. * * We don't use the normal async waterfall, * since we never want these requests to happen in parallel. * The first server needs to fully fail before we can try again, * or we risk having document update conflicts, duplicated keys, * redundant login notifications, or other corruption. */ async function loginFetch(ai, method, path, body) { const { loginServers } = ai.props.state.login; // This will be out of range, but the modulo brings it back: const startIndex = Math.floor(Math.random() * 255); let response; let lastError = new Error('No login servers available'); for (let i = 0; i < loginServers.length; ++i) { try { const index = (startIndex + i) % loginServers.length; response = await loginFetchInner(ai, loginServers[index], method, path, body); break; } catch (error) { lastError = error; } } if (response == null) throw lastError; const { status } = response; const json = await response.json().catch(() => { throw new Error(`Invalid reply JSON, HTTP status ${status}`); }); return parseReply(json); } function loginFetchInner(ai, serverUri, method, path, body) { const { state, io, log } = ai.props; const { apiKey, apiSecret } = state.login; const bodyText = method === 'GET' || body == null ? undefined : JSON.stringify(wasLoginRequestBody(body)); // API key: let authorization = `Token ${apiKey}`; if (apiSecret != null) { const requestText = `${method}\n/api${path}\n${bodyText ?? ''}`; const hash = hmacSha256(utf8.parse(requestText), apiSecret); authorization = `HMAC ${apiKey} ${rfc4648.base64.stringify(hash)}`; } const opts = { body: bodyText, method, headers: { 'content-type': 'application/json', accept: 'application/json', authorization }, corsBypass: 'never' }; const start = Date.now(); const fullUri = `${serverUri}/api${path}`; return timeout(io.fetch(fullUri, opts), 30000).then(response => { // Log the results: const time = Date.now() - start; log(`${method} ${fullUri} returned ${response.status} in ${time}ms`); if (response.status === 409) { log.crash(`Login API conflict error`, { path }); } return response; }, networkError => { const time = Date.now() - start; log.error(`${method} ${fullUri} failed in ${time}ms, ${String(networkError)}`); throw new NetworkError(`Could not reach the auth server: ${path}`); }); } function makeSecretKit(ai, login) { const { io } = ai.props; const { loginId, loginKey } = login; const loginAuth = io.random(32); const loginAuthBox = encrypt(io, loginAuth, loginKey); return { loginId, server: wasChangeSecretPayload({ loginAuth, loginAuthBox }), serverPath: '/v2/login/secret', stash: { loginAuthBox } }; } /** * Computes an SNRP value. */ function makeSnrp(ai, targetMs = 2000) { return ai.props.output.scrypt.makeSnrp(targetMs); } /** * Performs an