UNPKG

@planq-network/encrypted-backup

Version:

Libraries for implemented password encrypted account backups

399 lines 25.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.openBackup = exports.createBackup = exports.createPasswordEncryptedBackup = exports.createPinEncryptedBackup = void 0; var address_1 = require("@planq-network/base/lib/address"); var result_1 = require("@planq-network/base/lib/result"); var circuit_breaker_1 = require("@planq-network/identity/lib/odis/circuit-breaker"); var crypto = __importStar(require("crypto")); var debug_1 = __importDefault(require("debug")); var config_1 = require("./config"); var errors_1 = require("./errors"); var odis_1 = require("./odis"); var utils_1 = require("./utils"); var debug = (0, debug_1.default)('kit:encrypted-backup:backup'); /** * Create a data backup, encrypting it with a hardened key derived from the given PIN. * * @remarks Using a 4 or 6 digit PIN for encryption requires an extremely restrictive rate limit for * attempts to guess the PIN. This is enforced by ODIS through the SequentialDelayDomain with * settings to allow the user (or an attacker) only a fixed number of attempts to guess their PIN. * * Because PINs have very little entropy, the total number of guesses is very restricted. * * On the first day, the client has 10 attempts. 5 within 10s. 5 more over roughly 45 minutes. * * On the second day, the client has 5 attempts over roughly 2 minutes. * * On the third day, the client has 3 attempts over roughly 40 seconds. * * On the fourth day, the client has 2 attempts over roughly 10 seconds. * * Overall, the client has 25 attempts over 4 days. All further attempts will be denied. * * It is strongly recommended that the calling application implement a PIN blocklist to prevent the * user from selecting a number of the most common PIN codes (e.g. blocking the top 25k PINs by * frequency of appearance in the HIBP Passwords dataset). An example implementation can be seen in * the Valora wallet. {@link * https://github.com/valora-inc/wallet/blob/3940661c40d08e4c5db952bd0abeaabb0030fc7a/packages/mobile/src/pincode/authentication.ts#L56-L108 * | PIN blocklist implementation} * * In order to handle the event of an ODIS service compromise, this configuration additionally * includes a circuit breaker service run by Valora. In the event of an ODIS compromise, the Valora * team will take their service offline, preventing backups using the circuit breaker from being * opened. This ensures that an attacker who has compromised ODIS cannot leverage their attack to * forcibly open backups created with this function. * * @param data The secret data (e.g. BIP-39 mnemonic phrase) to be included in the encrypted backup. * @param pin PIN to use in deriving the encryption key. * @param hardening Configuration for how the password should be hardened in deriving the key. * @param metadata Arbitrary key-value data to include in the backup to identify it. */ function createPinEncryptedBackup(_a) { var data = _a.data, pin = _a.pin, environment = _a.environment, metadata = _a.metadata; return __awaiter(this, void 0, void 0, function () { var hardening; return __generator(this, function (_b) { if (environment === config_1.EnvironmentIdentifier.ALFAJORES) { hardening = config_1.PIN_HARDENING_ALFAJORES_CONFIG; } else if (environment === config_1.EnvironmentIdentifier.MAINNET || environment === undefined) { hardening = config_1.PIN_HARDENING_MAINNET_CONFIG; } if (hardening === undefined) { throw new Error('Implementation error: unhandled environment identifier'); } return [2 /*return*/, createBackup({ data: data, userSecret: pin, hardening: hardening, metadata: metadata })]; }); }); } exports.createPinEncryptedBackup = createPinEncryptedBackup; /** * Create a data backup, encrypting it with a hardened key derived from the given password. * * @remarks Because passwords have moderate entropy, the total number of guesses is restricted. * * The user initially gets 5 attempts without delay. * * Then the user gets two attempts every 5 seconds for up to 20 attempts. * * Then the user gets two attempts every 30 seconds for up to 20 attempts. * * Then the user gets two attempts every 5 minutes for up to 20 attempts. * * Then the user gets two attempts every hour for up to 20 attempts. * * Then the user gets two attempts every day for up to 20 attempts. * * Following guidelines in NIST-800-63-3 it is strongly recommended that the caller apply a password * blocklist to the users choice of password. * * In order to handle the event of an ODIS service compromise, this configuration additionally * hardens the password input with a computational hardening function. In particular, scrypt is used * with IETF recommended parameters {@link * https://tools.ietf.org/id/draft-whited-kitten-password-storage-00.html#name-scrypt | IETF * recommended scrypt parameters } * * @param data The secret data (e.g. BIP-39 mnemonic phrase) to be included in the encrypted backup. * @param password Password to use in deriving the encryption key. * @param hardening Configuration for how the password should be hardened in deriving the key. * @param metadata Arbitrary key-value data to include in the backup to identify it. */ function createPasswordEncryptedBackup(_a) { var data = _a.data, password = _a.password, environment = _a.environment, metadata = _a.metadata; return __awaiter(this, void 0, void 0, function () { var hardening; return __generator(this, function (_b) { if (environment === config_1.EnvironmentIdentifier.ALFAJORES) { hardening = config_1.PASSWORD_HARDENING_ALFAJORES_CONFIG; } else if (environment === config_1.EnvironmentIdentifier.MAINNET || environment === undefined) { hardening = config_1.PASSWORD_HARDENING_MAINNET_CONFIG; } if (hardening === undefined) { throw new Error('Implementation error: unhandled environment identifier'); } return [2 /*return*/, createBackup({ data: data, userSecret: password, hardening: hardening, metadata: metadata })]; }); }); } exports.createPasswordEncryptedBackup = createPasswordEncryptedBackup; /** * Create a data backup, encrypting it with a hardened key derived from the given password or PIN. * * @param data The secret data (e.g. BIP-39 mnemonic phrase) to be included in the encrypted backup. * @param userSecret Password, PIN, or other user secret to use in deriving the encryption key. * If a string is provided, it will be UTF-8 encoded into a Buffer before use. * @param hardening Configuration for how the password should be hardened in deriving the key. * @param metadata Arbitrary key-value data to include in the backup to identify it. * * @privateRemarks Most of this functions code is devoted to key generation starting with the input * password or PIN and ending up with a hardened encryption key. It is important that the order and * inputs to each step in the derivation be well considered and implemented correctly. One important * requirement is that no output included in the backup acts as a "commitment" to the password or PIN * value, except the final ciphertext. An example of an issue with this would be if a hash of the * password and nonce were included in the backup. If a commitment to the password or PIN is * included, an attacker can locally brute force that commitment to recover the password, then use * that knowledge to complete the derivation. */ function createBackup(_a) { var _b, _c; var data = _a.data, userSecret = _a.userSecret, hardening = _a.hardening, metadata = _a.metadata; return __awaiter(this, void 0, void 0, function () { var nonce, passwordSalt, odisAuthKeySeed, userSecretBuffer, initialKey, encryptedFuseKey, updatedKey, circuitBreakerClient, serviceStatus, fuseKey, wrap, domain, odisHardenedKey, authorizer, odisHardening, computationalHardenedKey, computationalHardening, finalKey, encryption; return __generator(this, function (_d) { switch (_d.label) { case 0: // Password and backup data are not included in any logging as they are likely sensitive. debug('creating a backup with the following information', hardening, metadata); // Safety measure to prevent users from accidentally using this API without any hardening. if (hardening.odis === undefined && hardening.computational === undefined) { return [2 /*return*/, (0, result_1.Err)(new errors_1.UsageError(new Error('createBackup cannot be used with a empty hardening config')))]; } nonce = crypto.randomBytes(32); passwordSalt = nonce.slice(0, 16); odisAuthKeySeed = nonce.slice(16, 32); userSecretBuffer = typeof userSecret === 'string' ? Buffer.from(userSecret, 'utf8') : userSecret; initialKey = (0, utils_1.deriveKey)(utils_1.KDFInfo.PASSWORD, [userSecretBuffer, passwordSalt]); if (!(hardening.circuitBreaker !== undefined)) return [3 /*break*/, 2]; debug('generating a fuse key to enabled use of the circuit breaker service'); circuitBreakerClient = new circuit_breaker_1.CircuitBreakerClient(hardening.circuitBreaker.environment); return [4 /*yield*/, circuitBreakerClient.status()]; case 1: serviceStatus = _d.sent(); if (!serviceStatus.ok) { return [2 /*return*/, (0, result_1.Err)(serviceStatus.error)]; } if (serviceStatus.result !== circuit_breaker_1.CircuitBreakerKeyStatus.ENABLED) { return [2 /*return*/, (0, result_1.Err)(new circuit_breaker_1.CircuitBreakerUnavailableError(serviceStatus.result))]; } debug('confirmed that the circuit breaker is online'); // Generate a fuse key and encrypt it against the circuit breaker public key. debug('generating and wrapping the fuse key'); fuseKey = crypto.randomBytes(16); wrap = circuitBreakerClient.wrapKey(fuseKey); if (!wrap.ok) { return [2 /*return*/, (0, result_1.Err)(wrap.error)]; } encryptedFuseKey = wrap.result; // Mix the fuse key into the ongoing key hardening. Note that mixing in the circuit breaker key // occurs before the request to ODIS. This means an attacker would need to acquire the fuse key // _before_ they can make any attempts to guess the user's secret. debug('mixing the fuse key into the keying material'); updatedKey = (0, utils_1.deriveKey)(utils_1.KDFInfo.FUSE_KEY, [initialKey, fuseKey]); return [3 /*break*/, 3]; case 2: debug('not using the circuit breaker service'); updatedKey = initialKey; _d.label = 3; case 3: if (!(hardening.odis !== undefined)) return [3 /*break*/, 5]; debug('hardening the user key with output from ODIS'); authorizer = (0, odis_1.odisQueryAuthorizer)(odisAuthKeySeed); domain = (0, odis_1.buildOdisDomain)(hardening.odis, authorizer.address); debug('sending request to ODIS to harden the backup encryption key'); return [4 /*yield*/, (0, odis_1.odisHardenKey)(updatedKey, domain, hardening.odis.environment, authorizer.wallet)]; case 4: odisHardening = _d.sent(); if (!odisHardening.ok) { return [2 /*return*/, (0, result_1.Err)(odisHardening.error)]; } odisHardenedKey = odisHardening.result; return [3 /*break*/, 6]; case 5: debug('not using ODIS for key hardening'); _d.label = 6; case 6: if (!(hardening.computational !== undefined)) return [3 /*break*/, 8]; debug('hardening user key with computational function', hardening.computational); return [4 /*yield*/, (0, utils_1.computationalHardenKey)(updatedKey, hardening.computational)]; case 7: computationalHardening = _d.sent(); if (!computationalHardening.ok) { return [2 /*return*/, (0, result_1.Err)(computationalHardening.error)]; } computationalHardenedKey = computationalHardening.result; return [3 /*break*/, 9]; case 8: debug('not using computational key hardening'); _d.label = 9; case 9: debug('finalizing encryption key'); finalKey = (0, utils_1.deriveKey)(utils_1.KDFInfo.FINALIZE, [ updatedKey, odisHardenedKey !== null && odisHardenedKey !== void 0 ? odisHardenedKey : Buffer.alloc(0), computationalHardenedKey !== null && computationalHardenedKey !== void 0 ? computationalHardenedKey : Buffer.alloc(0), ]); debug('encrypting backup data with final encryption key'); encryption = (0, utils_1.encrypt)(finalKey, data); if (!encryption.ok) { return [2 /*return*/, (0, result_1.Err)(encryption.error)]; } // Encrypted and wrap the data in a Backup structure. debug('created encrypted backup'); return [2 /*return*/, (0, result_1.Ok)({ encryptedData: encryption.result, nonce: nonce, odisDomain: domain, encryptedFuseKey: encryptedFuseKey, computationalHardening: hardening.computational, // TODO(victor): Bump this to 1.0 when the final crypto is added. version: '0.0.1', metadata: metadata, environment: { odis: (_b = hardening.odis) === null || _b === void 0 ? void 0 : _b.environment, circuitBreaker: (_c = hardening.circuitBreaker) === null || _c === void 0 ? void 0 : _c.environment, }, })]; } }); }); } exports.createBackup = createBackup; /** * Open an encrypted backup file, using the provided password or PIN to derive the decryption key. * * @param backup Backup structure including the ciphertext and key derivation information. * @param userSecret Password, PIN, or other user secret to use in deriving the encryption key. * If a string is provided, it will be UTF-8 encoded into a Buffer before use. */ function openBackup(_a) { var _b, _c; var backup = _a.backup, userSecret = _a.userSecret; return __awaiter(this, void 0, void 0, function () { var passwordSalt, odisAuthKeySeed, userSecretBuffer, initialKey, updatedKey, circuitBreakerClient, unwrap, odisHardenedKey, domain, authorizer, odisHardening, computationalHardenedKey, computationalHardening, finalKey, decryption; return __generator(this, function (_d) { switch (_d.label) { case 0: debug('opening an encrypted backup'); passwordSalt = backup.nonce.slice(0, 16); odisAuthKeySeed = backup.nonce.slice(16, 32); userSecretBuffer = typeof userSecret === 'string' ? Buffer.from(userSecret, 'utf8') : userSecret; initialKey = (0, utils_1.deriveKey)(utils_1.KDFInfo.PASSWORD, [userSecretBuffer, passwordSalt]); if (!(backup.encryptedFuseKey !== undefined)) return [3 /*break*/, 2]; if (((_b = backup.environment) === null || _b === void 0 ? void 0 : _b.circuitBreaker) === undefined) { return [2 /*return*/, (0, result_1.Err)(new errors_1.InvalidBackupError(new Error('encrypted fuse key is provided but no circuit breaker environment is provided')))]; } circuitBreakerClient = new circuit_breaker_1.CircuitBreakerClient(backup.environment.circuitBreaker); debug('requesting the circuit breaker service unwrap the encrypted circuit breaker key', backup.environment.circuitBreaker); return [4 /*yield*/, circuitBreakerClient.unwrapKey(backup.encryptedFuseKey)]; case 1: unwrap = _d.sent(); if (!unwrap.ok) { return [2 /*return*/, (0, result_1.Err)(unwrap.error)]; } // Mix the fuse key into the ongoing key hardening. Note that mixing in the circuit breaker key // occurs before the request to ODIS. This means an attacker would need to aquire the fuse key // _before_ they can make any attempts to guess the user's secret. updatedKey = (0, utils_1.deriveKey)(utils_1.KDFInfo.FUSE_KEY, [initialKey, unwrap.result]); return [3 /*break*/, 3]; case 2: debug('backup did not specify an encrypted fuse key'); updatedKey = initialKey; _d.label = 3; case 3: if (!(backup.odisDomain !== undefined)) return [3 /*break*/, 5]; domain = backup.odisDomain; authorizer = (0, odis_1.odisQueryAuthorizer)(odisAuthKeySeed); if (domain.address.defined && !(0, address_1.eqAddress)(authorizer.address, domain.address.value)) { return [2 /*return*/, (0, result_1.Err)(new errors_1.InvalidBackupError(new Error('domain query authorizer address is provided but is not derived from the backup nonce')))]; } if (((_c = backup.environment) === null || _c === void 0 ? void 0 : _c.odis) === undefined) { return [2 /*return*/, (0, result_1.Err)(new errors_1.InvalidBackupError(new Error('ODIS domain is provided by no ODIS environment information')))]; } debug('requesting a key hardening response from ODIS'); return [4 /*yield*/, (0, odis_1.odisHardenKey)(updatedKey, domain, backup.environment.odis, authorizer.wallet)]; case 4: odisHardening = _d.sent(); if (!odisHardening.ok) { return [2 /*return*/, (0, result_1.Err)(odisHardening.error)]; } odisHardenedKey = odisHardening.result; return [3 /*break*/, 6]; case 5: debug('not using ODIS for key hardening'); _d.label = 6; case 6: if (!(backup.computationalHardening !== undefined)) return [3 /*break*/, 8]; debug('hardening user key with computational function', backup.computationalHardening); return [4 /*yield*/, (0, utils_1.computationalHardenKey)(updatedKey, backup.computationalHardening)]; case 7: computationalHardening = _d.sent(); if (!computationalHardening.ok) { return [2 /*return*/, (0, result_1.Err)(computationalHardening.error)]; } computationalHardenedKey = computationalHardening.result; return [3 /*break*/, 9]; case 8: debug('not using computational key hardening'); _d.label = 9; case 9: debug('finalizing decryption key'); finalKey = (0, utils_1.deriveKey)(utils_1.KDFInfo.FINALIZE, [ updatedKey, odisHardenedKey !== null && odisHardenedKey !== void 0 ? odisHardenedKey : Buffer.alloc(0), computationalHardenedKey !== null && computationalHardenedKey !== void 0 ? computationalHardenedKey : Buffer.alloc(0), ]); debug('decrypting backup with finalized decryption key'); decryption = (0, utils_1.decrypt)(finalKey, backup.encryptedData); if (!decryption.ok) { return [2 /*return*/, (0, result_1.Err)(decryption.error)]; } debug('decrypted backup'); return [2 /*return*/, (0, result_1.Ok)(decryption.result)]; } }); }); } exports.openBackup = openBackup; //# sourceMappingURL=backup.js.map