UNPKG

@planq-network/encrypted-backup

Version:

Libraries for implemented password encrypted account backups

286 lines 16.8 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 }; } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.requestOdisDomainQuotaStatus = exports.odisQueryAuthorizer = exports.odisHardenKey = exports.buildOdisDomain = void 0; var result_1 = require("@planq-network/base/lib/result"); var query_1 = require("@planq-network/identity/lib/odis/query"); var phone_number_privacy_common_1 = require("@planq-network/phone-number-privacy-common"); var sign_typed_data_utils_1 = require("@planq-network/utils/lib/sign-typed-data-utils"); var wallet_local_1 = require("@planq-network/wallet-local"); var crypto = __importStar(require("crypto")); var errors_1 = require("./errors"); var utils_1 = require("./utils"); /** * Builds an ODIS SequentialDelayDomain with the given hardening configuration. * * @param authorizer Address of the key that should authorize requests to ODIS. * @returns A SequentialDelayDomain with the provided rate limiting configuration. */ function buildOdisDomain(config, authorizer, salt) { return { name: phone_number_privacy_common_1.DomainIdentifiers.SequentialDelay, version: '1', stages: config.rateLimit, address: (0, sign_typed_data_utils_1.defined)(authorizer), salt: salt ? (0, sign_typed_data_utils_1.defined)(salt) : sign_typed_data_utils_1.noString, }; } exports.buildOdisDomain = buildOdisDomain; /** * Returns a hardened key derived from the input key material and a POPRF evaluation on that keying * material under the given rate limiting domain. * * @param key Input key material which will be the blinded input to the ODIS POPRF. * @param domain Rate limiting configuration and domain input to the ODIS POPRF. * @param environment Information for the targeted ODIS environment. * @param wallet Wallet with access to the authorizer signing key specified in the domain input. * Should be provided if the input domain is authenticated. */ function odisHardenKey(key, domain, environment, wallet) { var _a; return __awaiter(this, void 0, void 0, function () { var sessionID, quotaResp, quotaState, quotaResult, blindingSeed, poprfClient, signatureResp, odisOutput, error_1; return __generator(this, function (_b) { switch (_b.label) { case 0: sessionID = (0, phone_number_privacy_common_1.genSessionID)(); return [4 /*yield*/, requestOdisDomainQuotaStatus(domain, environment, sessionID, wallet)]; case 1: quotaResp = _b.sent(); if (!quotaResp.ok) { return [2 /*return*/, quotaResp]; } quotaState = quotaResp.result.status; quotaResult = (0, phone_number_privacy_common_1.checkSequentialDelayRateLimit)(domain, // Use the local clock as a fallback. Divide by 1000 to get seconds from ms. (_a = quotaState.now) !== null && _a !== void 0 ? _a : Date.now() / 1000, quotaState); if (!quotaResult.accepted) { return [2 /*return*/, (0, result_1.Err)(new errors_1.OdisRateLimitingError(quotaResult.notBefore, new Error('client does not currently have quota based on status response.')))]; } blindingSeed = crypto.randomBytes(16); poprfClient = new phone_number_privacy_common_1.PoprfClient(Buffer.from(environment.odisPubKey, 'base64'), (0, phone_number_privacy_common_1.domainHash)(domain), key, blindingSeed); return [4 /*yield*/, requestOdisDomainSignature(poprfClient.blindedMessage.toString('base64'), quotaState.counter, domain, environment, sessionID, wallet)]; case 2: signatureResp = _b.sent(); if (!signatureResp.ok) { return [2 /*return*/, signatureResp]; } _b.label = 3; case 3: _b.trys.push([3, 5, , 6]); return [4 /*yield*/, poprfClient.unblindResponse(Buffer.from(signatureResp.result.signature, 'base64'))]; case 4: odisOutput = _b.sent(); return [3 /*break*/, 6]; case 5: error_1 = _b.sent(); return [2 /*return*/, (0, result_1.Err)(new errors_1.OdisVerificationError(error_1))]; case 6: // Mix the key with the output from ODIS to get the hardened key. return [2 /*return*/, (0, result_1.Ok)((0, utils_1.deriveKey)(utils_1.KDFInfo.ODIS_KEY_HARDENING, [key, odisOutput]))]; } }); }); } exports.odisHardenKey = odisHardenKey; /** * Derive from the nonce a private key and use it to instantiate a wallet for request signing * * @remarks It is important that the auth key does not mix in entropy from the password value. If * it did, then the derived address and signatures would act as a commitment to the underlying * password value and would allow offline brute force attacks when combined with the other values * mixed into the key value. */ function odisQueryAuthorizer(nonce) { // Derive the domain's request authorization key from the backup nonce. var authKey = (0, utils_1.deriveKey)(utils_1.KDFInfo.ODIS_AUTH_KEY, [nonce]); var wallet = new wallet_local_1.LocalWallet(); wallet.addAccount(authKey.toString('hex')); var address = wallet.getAccounts()[0]; if (address === undefined) { // Throw the error instead if returning it as this is more akin to a panic. throw new Error('Implementation error: LocalWallet with an added account returned no accounts'); } return { address: address, wallet: wallet }; } exports.odisQueryAuthorizer = odisQueryAuthorizer; /** * Returns a hardened key derived from the input key material and a POPRF evaluation on that keying * material under the given rate limiting domain. * * @param domain Rate limiting configuration and domain input to the ODIS POPRF. * @param environment Information for the targeted ODIS environment. * @param sessionID client-defined session ID for tracking requests across services * @param wallet Wallet with access to the authorizer signing key specified in the domain input. * Should be provided if the input domain is authenticated. */ function requestOdisDomainQuotaStatus(domain, environment, sessionID, wallet) { var _a; return __awaiter(this, void 0, void 0, function () { var quotaStatusReq, authorizer, _b, _c, quotaResp, error_2; return __generator(this, function (_d) { switch (_d.label) { case 0: quotaStatusReq = { type: phone_number_privacy_common_1.DomainRequestTypeTag.QUOTA, domain: domain, options: { signature: sign_typed_data_utils_1.noString, nonce: sign_typed_data_utils_1.noNumber, }, sessionID: (0, sign_typed_data_utils_1.defined)(sessionID), }; authorizer = domain.address.defined ? domain.address.value : undefined; if (!(authorizer !== undefined)) return [3 /*break*/, 2]; if (wallet === undefined || !wallet.hasAccount(authorizer)) { return [2 /*return*/, (0, result_1.Err)(new errors_1.AuthorizationError(new Error('key for signing ODIS quota status request is unavailable')))]; } _b = quotaStatusReq.options; _c = sign_typed_data_utils_1.defined; return [4 /*yield*/, wallet.signTypedData(authorizer, (0, phone_number_privacy_common_1.domainQuotaStatusRequestEIP712)(quotaStatusReq))]; case 1: _b.signature = _c.apply(void 0, [_d.sent()]); return [3 /*break*/, 3]; case 2: if (wallet !== undefined) { return [2 /*return*/, (0, result_1.Err)(new errors_1.UsageError(new Error('wallet provided but the domain is unauthenticated')))]; } _d.label = 3; case 3: _d.trys.push([3, 5, , 6]); return [4 /*yield*/, (0, query_1.sendOdisDomainRequest)(quotaStatusReq, environment, phone_number_privacy_common_1.DomainEndpoint.DOMAIN_QUOTA_STATUS, (0, phone_number_privacy_common_1.domainQuotaStatusResponseSchema)(phone_number_privacy_common_1.SequentialDelayDomainStateSchema))]; case 4: quotaResp = _d.sent(); return [3 /*break*/, 6]; case 5: error_2 = _d.sent(); if ((_a = error_2.message) === null || _a === void 0 ? void 0 : _a.includes(query_1.ErrorMessages.ODIS_FETCH_ERROR)) { return [2 /*return*/, (0, result_1.Err)(new errors_1.FetchError(error_2))]; } return [2 /*return*/, (0, result_1.Err)(new errors_1.OdisServiceError(error_2))]; case 6: if (!quotaResp.success) { return [2 /*return*/, (0, result_1.Err)(new errors_1.OdisServiceError(new Error(quotaResp.error), quotaResp.version))]; } return [2 /*return*/, (0, result_1.Ok)(quotaResp)]; } }); }); } exports.requestOdisDomainQuotaStatus = requestOdisDomainQuotaStatus; function requestOdisDomainSignature(blindedMessage, counter, domain, environment, sessionID, wallet) { var _a, _b; return __awaiter(this, void 0, void 0, function () { var signatureReq, authorizer, _c, _d, signatureResp, error_3; return __generator(this, function (_e) { switch (_e.label) { case 0: signatureReq = { type: phone_number_privacy_common_1.DomainRequestTypeTag.SIGN, domain: domain, options: { signature: sign_typed_data_utils_1.noString, nonce: (0, sign_typed_data_utils_1.defined)(counter), }, blindedMessage: blindedMessage, sessionID: (0, sign_typed_data_utils_1.defined)(sessionID), }; authorizer = domain.address.defined ? domain.address.value : undefined; if (!(authorizer !== undefined)) return [3 /*break*/, 2]; if (wallet === undefined || !wallet.hasAccount(authorizer)) { return [2 /*return*/, (0, result_1.Err)(new errors_1.AuthorizationError(new Error('key for signing ODIS domain signature request is unavailable')))]; } _c = signatureReq.options; _d = sign_typed_data_utils_1.defined; return [4 /*yield*/, wallet.signTypedData(authorizer, (0, phone_number_privacy_common_1.domainRestrictedSignatureRequestEIP712)(signatureReq))]; case 1: _c.signature = _d.apply(void 0, [_e.sent()]); return [3 /*break*/, 3]; case 2: if (wallet !== undefined) { return [2 /*return*/, (0, result_1.Err)(new errors_1.UsageError(new Error('wallet provided but the domain is unauthenticated')))]; } _e.label = 3; case 3: _e.trys.push([3, 5, , 6]); return [4 /*yield*/, (0, query_1.sendOdisDomainRequest)(signatureReq, environment, phone_number_privacy_common_1.DomainEndpoint.DOMAIN_SIGN, (0, phone_number_privacy_common_1.domainRestrictedSignatureResponseSchema)(phone_number_privacy_common_1.SequentialDelayDomainStateSchema))]; case 4: signatureResp = _e.sent(); return [3 /*break*/, 6]; case 5: error_3 = _e.sent(); if ((_a = error_3.message) === null || _a === void 0 ? void 0 : _a.includes(query_1.ErrorMessages.ODIS_FETCH_ERROR)) { return [2 /*return*/, (0, result_1.Err)(new errors_1.FetchError(error_3))]; } if ((_b = error_3.message) === null || _b === void 0 ? void 0 : _b.includes(query_1.ErrorMessages.ODIS_RATE_LIMIT_ERROR)) { return [2 /*return*/, (0, result_1.Err)(new errors_1.OdisRateLimitingError(undefined, error_3))]; } return [2 /*return*/, (0, result_1.Err)(new errors_1.OdisServiceError(error_3))]; case 6: if (!signatureResp.success) { return [2 /*return*/, (0, result_1.Err)(new errors_1.OdisServiceError(new Error(signatureResp.error), signatureResp.version))]; } return [2 /*return*/, (0, result_1.Ok)(signatureResp)]; } }); }); } //# sourceMappingURL=odis.js.map