summaryrefslogtreecommitdiff
path: root/web/js/browser/lib
diff options
context:
space:
mode:
Diffstat (limited to 'web/js/browser/lib')
-rw-r--r--web/js/browser/lib/client.js792
-rw-r--r--web/js/browser/lib/crypto.js163
-rw-r--r--web/js/browser/lib/hawk.d.ts24
-rw-r--r--web/js/browser/lib/hawk.js145
-rw-r--r--web/js/browser/lib/recoveryKey.js38
-rw-r--r--web/js/browser/lib/utils.js26
6 files changed, 1188 insertions, 0 deletions
diff --git a/web/js/browser/lib/client.js b/web/js/browser/lib/client.js
new file mode 100644
index 0000000..fadbdd9
--- /dev/null
+++ b/web/js/browser/lib/client.js
@@ -0,0 +1,792 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+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 __rest = (this && this.__rest) || function (s, e) {
+ var t = {};
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
+ t[p] = s[p];
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
+ t[p[i]] = s[p[i]];
+ }
+ return t;
+};
+import * as crypto from './crypto';
+import * as hawk from './hawk';
+var ERRORS;
+(function (ERRORS) {
+ ERRORS[ERRORS["INVALID_TIMESTAMP"] = 111] = "INVALID_TIMESTAMP";
+ ERRORS[ERRORS["INCORRECT_EMAIL_CASE"] = 120] = "INCORRECT_EMAIL_CASE";
+})(ERRORS || (ERRORS = {}));
+var tokenType;
+(function (tokenType) {
+ tokenType["sessionToken"] = "sessionToken";
+ tokenType["passwordForgotToken"] = "passwordForgotToken";
+ tokenType["keyFetchToken"] = "keyFetchToken";
+ tokenType["accountResetToken"] = "accountResetToken";
+ tokenType["passwordChangeToken"] = "passwordChangeToken";
+})(tokenType || (tokenType = {}));
+var AUTH_PROVIDER;
+(function (AUTH_PROVIDER) {
+ AUTH_PROVIDER["GOOGLE"] = "google";
+ AUTH_PROVIDER["APPLE"] = "apple";
+})(AUTH_PROVIDER || (AUTH_PROVIDER = {}));
+function langHeader(lang) {
+ return new Headers(lang
+ ? {
+ 'Accept-Language': lang,
+ }
+ : {});
+}
+function pathWithKeys(path, keys) {
+ return `${path}${keys ? '?keys=true' : ''}`;
+}
+function fetchOrTimeout(input, init = {}, timeout) {
+ return __awaiter(this, void 0, void 0, function* () {
+ let id = 0;
+ if (typeof AbortController !== 'undefined') {
+ const aborter = new AbortController();
+ init.signal = aborter.signal;
+ id = setTimeout((() => aborter.abort()), timeout);
+ }
+ try {
+ return yield fetch(input, init);
+ }
+ finally {
+ if (id) {
+ clearTimeout(id);
+ }
+ }
+ });
+}
+function cleanStringify(value) {
+ // remove keys with null values
+ return JSON.stringify(value, (_, v) => (v == null ? undefined : v));
+}
+export default class AuthClient {
+ constructor(authServerUri, options = {}) {
+ if (new RegExp(`/${AuthClient.VERSION}$`).test(authServerUri)) {
+ this.uri = authServerUri;
+ }
+ else {
+ this.uri = `${authServerUri}/${AuthClient.VERSION}`;
+ }
+ this.localtimeOffsetMsec = 0;
+ this.timeout = options.timeout || 30000;
+ }
+ static create(authServerUri) {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (typeof TextEncoder === 'undefined') {
+ yield import(
+ // @ts-ignore
+ /* webpackChunkName: "fast-text-encoding" */ 'fast-text-encoding');
+ }
+ yield crypto.checkWebCrypto();
+ return new AuthClient(authServerUri);
+ });
+ }
+ url(path) {
+ return `${this.uri}${path}`;
+ }
+ request(method, path, payload, headers = new Headers()) {
+ return __awaiter(this, void 0, void 0, function* () {
+ headers.set('Content-Type', 'application/json');
+ const response = yield fetchOrTimeout(this.url(path), {
+ method,
+ headers,
+ body: cleanStringify(payload),
+ }, this.timeout);
+ let result = yield response.text();
+ try {
+ result = JSON.parse(result);
+ }
+ catch (e) { }
+ if (result.errno) {
+ throw result;
+ }
+ if (!response.ok) {
+ throw {
+ error: 'Unknown error',
+ message: result,
+ errno: 999,
+ code: response.status,
+ };
+ }
+ return result;
+ });
+ }
+ hawkRequest(method, path, token, kind, payload, extraHeaders = new Headers()) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const makeHeaders = () => __awaiter(this, void 0, void 0, function* () {
+ const headers = yield hawk.header(method, this.url(path), token, kind, {
+ payload: cleanStringify(payload),
+ contentType: 'application/json',
+ localtimeOffsetMsec: this.localtimeOffsetMsec,
+ });
+ for (const [name, value] of extraHeaders) {
+ headers.set(name, value);
+ }
+ return headers;
+ });
+ try {
+ return yield this.request(method, path, payload, yield makeHeaders());
+ }
+ catch (e) {
+ if (e.errno === ERRORS.INVALID_TIMESTAMP) {
+ const serverTime = e.serverTime * 1000 || Date.now();
+ this.localtimeOffsetMsec = serverTime - Date.now();
+ return this.request(method, path, payload, yield makeHeaders());
+ }
+ throw e;
+ }
+ });
+ }
+ sessionGet(path, sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.hawkRequest('GET', path, sessionToken, tokenType.sessionToken);
+ });
+ }
+ sessionPost(path, sessionToken, payload, headers) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.hawkRequest('POST', path, sessionToken, tokenType.sessionToken, payload, headers);
+ });
+ }
+ sessionPut(path, sessionToken, payload, headers) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.hawkRequest('PUT', path, sessionToken, tokenType.sessionToken, payload, headers);
+ });
+ }
+ signUp(email, password, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentials = yield crypto.getCredentials(email, password);
+ const payloadOptions = (_a) => {
+ var { keys, lang } = _a, rest = __rest(_a, ["keys", "lang"]);
+ return rest;
+ };
+ const payload = Object.assign({ email, authPW: credentials.authPW }, payloadOptions(options));
+ const accountData = yield this.request('POST', pathWithKeys('/account/create', options.keys), payload, langHeader(options.lang));
+ if (options.keys) {
+ accountData.unwrapBKey = credentials.unwrapBKey;
+ }
+ return accountData;
+ });
+ }
+ signIn(email, password, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentials = yield crypto.getCredentials(email, password);
+ const payloadOptions = (_a) => {
+ var { keys } = _a, rest = __rest(_a, ["keys"]);
+ return rest;
+ };
+ const payload = Object.assign({ email, authPW: credentials.authPW }, payloadOptions(options));
+ try {
+ const accountData = yield this.request('POST', pathWithKeys('/account/login', options.keys), payload);
+ if (options.keys) {
+ accountData.unwrapBKey = credentials.unwrapBKey;
+ }
+ return accountData;
+ }
+ catch (error) {
+ if (error &&
+ error.email &&
+ error.errno === ERRORS.INCORRECT_EMAIL_CASE &&
+ !options.skipCaseError) {
+ options.skipCaseError = true;
+ options.originalLoginEmail = email;
+ return this.signIn(error.email, password, options);
+ }
+ else {
+ throw error;
+ }
+ }
+ });
+ }
+ verifyCode(uid, code, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.request('POST', '/recovery_email/verify_code', Object.assign({ uid,
+ code }, options));
+ });
+ }
+ recoveryEmailStatus(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/recovery_email/status', sessionToken);
+ });
+ }
+ recoveryEmailResendCode(sessionToken, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const payloadOptions = (_a) => {
+ var { lang } = _a, rest = __rest(_a, ["lang"]);
+ return rest;
+ };
+ return this.sessionPost('/recovery_email/resend_code', sessionToken, payloadOptions(options), langHeader(options.lang));
+ });
+ }
+ passwordForgotSendCode(email, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const payloadOptions = (_a) => {
+ var { lang } = _a, rest = __rest(_a, ["lang"]);
+ return rest;
+ };
+ const payload = Object.assign({ email }, payloadOptions(options));
+ return this.request('POST', '/password/forgot/send_code', payload, langHeader(options.lang));
+ });
+ }
+ passwordForgotResendCode(email, passwordForgotToken, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const payloadOptions = (_a) => {
+ var { lang } = _a, rest = __rest(_a, ["lang"]);
+ return rest;
+ };
+ const payload = Object.assign({ email }, payloadOptions(options));
+ return this.hawkRequest('POST', '/password/forgot/resend_code', passwordForgotToken, tokenType.passwordForgotToken, payload, langHeader(options.lang));
+ });
+ }
+ passwordForgotVerifyCode(code, passwordForgotToken, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const payload = Object.assign({ code }, options);
+ return this.hawkRequest('POST', '/password/forgot/verify_code', passwordForgotToken, tokenType.passwordForgotToken, payload);
+ });
+ }
+ passwordForgotStatus(passwordForgotToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.hawkRequest('GET', '/password/forgot/status', passwordForgotToken, tokenType.passwordForgotToken);
+ });
+ }
+ accountReset(email, newPassword, accountResetToken, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentials = yield crypto.getCredentials(email, newPassword);
+ const payloadOptions = (_a) => {
+ var { keys } = _a, rest = __rest(_a, ["keys"]);
+ return rest;
+ };
+ const payload = Object.assign({ authPW: credentials.authPW }, payloadOptions(options));
+ const accountData = yield this.hawkRequest('POST', pathWithKeys('/account/reset', options.keys), accountResetToken, tokenType.accountResetToken, payload);
+ if (options.keys && accountData.keyFetchToken) {
+ accountData.unwrapBKey = credentials.unwrapBKey;
+ }
+ return accountData;
+ });
+ }
+ finishSetup(token, email, newPassword) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentials = yield crypto.getCredentials(email, newPassword);
+ const payload = {
+ token,
+ authPW: credentials.authPW,
+ };
+ return yield this.request('POST', '/account/finish_setup', payload);
+ });
+ }
+ verifyAccountThirdParty(code, provider = AUTH_PROVIDER.GOOGLE, metricsContext = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const payload = {
+ code,
+ provider,
+ metricsContext,
+ };
+ return yield this.request('POST', '/linked_account/login', payload);
+ });
+ }
+ unlinkThirdParty(sessionToken, providerId) {
+ return __awaiter(this, void 0, void 0, function* () {
+ let provider;
+ switch (providerId) {
+ case 2: {
+ provider = AUTH_PROVIDER.APPLE;
+ break;
+ }
+ default: {
+ provider = AUTH_PROVIDER.GOOGLE;
+ }
+ }
+ return yield this.sessionPost('/linked_account/unlink', sessionToken, {
+ provider,
+ });
+ });
+ }
+ accountKeys(keyFetchToken, unwrapBKey) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentials = yield hawk.deriveHawkCredentials(keyFetchToken, 'keyFetchToken');
+ const keyData = yield this.hawkRequest('GET', '/account/keys', keyFetchToken, tokenType.keyFetchToken);
+ const keys = yield crypto.unbundleKeyFetchResponse(credentials.bundleKey, keyData.bundle);
+ return {
+ kA: keys.kA,
+ kB: crypto.unwrapKB(keys.wrapKB, unwrapBKey),
+ };
+ });
+ }
+ accountDestroy(email, password, options = {}, sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentials = yield crypto.getCredentials(email, password);
+ const payload = {
+ email,
+ authPW: credentials.authPW,
+ };
+ try {
+ if (sessionToken) {
+ return yield this.sessionPost('/account/destroy', sessionToken, payload);
+ }
+ else {
+ return yield this.request('POST', '/account/destroy', payload);
+ }
+ }
+ catch (error) {
+ if (error &&
+ error.email &&
+ error.errno === ERRORS.INCORRECT_EMAIL_CASE &&
+ !options.skipCaseError) {
+ options.skipCaseError = true;
+ return this.accountDestroy(error.email, password, options, sessionToken);
+ }
+ else {
+ throw error;
+ }
+ }
+ });
+ }
+ accountStatus(uid) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.request('GET', `/account/status?uid=${uid}`);
+ });
+ }
+ accountStatusByEmail(email) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.request('POST', '/account/status', { email });
+ });
+ }
+ accountProfile(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/account/profile', sessionToken);
+ });
+ }
+ account(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/account', sessionToken);
+ });
+ }
+ sessionDestroy(sessionToken, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/session/destroy', sessionToken, options);
+ });
+ }
+ sessionStatus(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/session/status', sessionToken);
+ });
+ }
+ sessionVerifyCode(sessionToken, code, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/session/verify_code', sessionToken, Object.assign({ code }, options));
+ });
+ }
+ sessionResendVerifyCode(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/session/resend_code', sessionToken, {});
+ });
+ }
+ sessionReauth(sessionToken, email, password, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentials = yield crypto.getCredentials(email, password);
+ const payloadOptions = (_a) => {
+ var { keys } = _a, rest = __rest(_a, ["keys"]);
+ return rest;
+ };
+ const payload = Object.assign({ email, authPW: credentials.authPW }, payloadOptions(options));
+ try {
+ const accountData = yield this.sessionPost(pathWithKeys('/session/reauth', options.keys), sessionToken, payload);
+ if (options.keys) {
+ accountData.unwrapBKey = credentials.unwrapBKey;
+ }
+ return accountData;
+ }
+ catch (error) {
+ if (error &&
+ error.email &&
+ error.errno === ERRORS.INCORRECT_EMAIL_CASE &&
+ !options.skipCaseError) {
+ options.skipCaseError = true;
+ options.originalLoginEmail = email;
+ return this.sessionReauth(sessionToken, error.email, password, options);
+ }
+ else {
+ throw error;
+ }
+ }
+ });
+ }
+ certificateSign(sessionToken, publicKey, duration, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const payload = {
+ publicKey,
+ duration,
+ };
+ return this.sessionPost(`/certificate/sign${options.service
+ ? `?service=${encodeURIComponent(options.service)}`
+ : ''}`, sessionToken, payload);
+ });
+ }
+ passwordChange(email, oldPassword, newPassword, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const oldCredentials = yield this.passwordChangeStart(email, oldPassword);
+ const keys = yield this.accountKeys(oldCredentials.keyFetchToken, oldCredentials.unwrapBKey);
+ const newCredentials = yield crypto.getCredentials(oldCredentials.email, newPassword);
+ const wrapKb = crypto.unwrapKB(keys.kB, newCredentials.unwrapBKey);
+ const sessionToken = options.sessionToken
+ ? (yield hawk.deriveHawkCredentials(options.sessionToken, 'sessionToken'))
+ .id
+ : undefined;
+ const payload = {
+ wrapKb,
+ authPW: newCredentials.authPW,
+ sessionToken,
+ };
+ const accountData = yield this.hawkRequest('POST', pathWithKeys('/password/change/finish', options.keys), oldCredentials.passwordChangeToken, tokenType.passwordChangeToken, payload);
+ if (options.keys && accountData.keyFetchToken) {
+ accountData.unwrapBKey = newCredentials.unwrapBKey;
+ }
+ return accountData;
+ });
+ }
+ passwordChangeStart(email, oldPassword, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const oldCredentials = yield crypto.getCredentials(email, oldPassword);
+ try {
+ const passwordData = yield this.request('POST', '/password/change/start', {
+ email,
+ oldAuthPW: oldCredentials.authPW,
+ });
+ return {
+ authPW: oldCredentials.authPW,
+ unwrapBKey: oldCredentials.unwrapBKey,
+ email: email,
+ keyFetchToken: passwordData.keyFetchToken,
+ passwordChangeToken: passwordData.passwordChangeToken,
+ };
+ }
+ catch (error) {
+ if (error &&
+ error.email &&
+ error.errno === ERRORS.INCORRECT_EMAIL_CASE &&
+ !options.skipCaseError) {
+ options.skipCaseError = true;
+ return this.passwordChangeStart(error.email, oldPassword, options);
+ }
+ else {
+ throw error;
+ }
+ }
+ });
+ }
+ createPassword(sessionToken, email, newPassword) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const newCredentials = yield crypto.getCredentials(email, newPassword);
+ const payload = {
+ authPW: newCredentials.authPW,
+ };
+ return this.sessionPost('/password/create', sessionToken, payload);
+ });
+ }
+ getRandomBytes() {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.request('POST', '/get_random_bytes');
+ });
+ }
+ deviceRegister(sessionToken, name, type, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const payload = Object.assign({ name,
+ type }, options);
+ return this.sessionPost('/account/device', sessionToken, payload);
+ });
+ }
+ deviceUpdate(sessionToken, id, name, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const payload = Object.assign({ id,
+ name }, options);
+ return this.sessionPost('/account/device', sessionToken, payload);
+ });
+ }
+ deviceDestroy(sessionToken, id) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/account/device/destroy', sessionToken, { id });
+ });
+ }
+ deviceList(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/account/devices', sessionToken);
+ });
+ }
+ sessions(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/account/sessions', sessionToken);
+ });
+ }
+ securityEvents(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/securityEvents', sessionToken);
+ });
+ }
+ deleteSecurityEvents(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.hawkRequest('DELETE', '/securityEvents', sessionToken, tokenType.sessionToken, {});
+ });
+ }
+ attachedClients(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/account/attached_clients', sessionToken);
+ });
+ }
+ attachedClientDestroy(sessionToken, clientInfo) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/account/attached_client/destroy', sessionToken, {
+ clientId: clientInfo.clientId,
+ deviceId: clientInfo.deviceId,
+ refreshTokenId: clientInfo.refreshTokenId,
+ sessionTokenId: clientInfo.sessionTokenId,
+ });
+ });
+ }
+ sendUnblockCode(email, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.request('POST', '/account/login/send_unblock_code', Object.assign({ email }, options));
+ });
+ }
+ rejectUnblockCode(uid, unblockCode) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.request('POST', '/account/login/reject_unblock_code', {
+ uid,
+ unblockCode,
+ });
+ });
+ }
+ consumeSigninCode(code, flowId, flowBeginTime, deviceId) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.request('POST', '/signinCodes/consume', {
+ code,
+ metricsContext: {
+ deviceId,
+ flowId,
+ flowBeginTime,
+ },
+ });
+ });
+ }
+ createSigninCode(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/signinCodes', sessionToken, {});
+ });
+ }
+ createCadReminder(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/emails/reminders/cad', sessionToken, {});
+ });
+ }
+ recoveryEmails(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/recovery_emails', sessionToken);
+ });
+ }
+ recoveryEmailCreate(sessionToken, email, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/recovery_email', sessionToken, Object.assign({ email }, options));
+ });
+ }
+ recoveryEmailDestroy(sessionToken, email) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/recovery_email/destroy', sessionToken, { email });
+ });
+ }
+ recoveryEmailSetPrimaryEmail(sessionToken, email) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/recovery_email/set_primary', sessionToken, {
+ email,
+ });
+ });
+ }
+ recoveryEmailSecondaryVerifyCode(sessionToken, email, code) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/recovery_email/secondary/verify_code', sessionToken, { email, code });
+ });
+ }
+ recoveryEmailSecondaryResendCode(sessionToken, email) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/recovery_email/secondary/resend_code', sessionToken, { email });
+ });
+ }
+ createTotpToken(sessionToken, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/totp/create', sessionToken, options);
+ });
+ }
+ deleteTotpToken(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/totp/destroy', sessionToken, {});
+ });
+ }
+ checkTotpTokenExists(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/totp/exists', sessionToken);
+ });
+ }
+ verifyTotpCode(sessionToken, code, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/session/verify/totp', sessionToken, Object.assign({ code }, options));
+ });
+ }
+ replaceRecoveryCodes(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionGet('/recoveryCodes', sessionToken);
+ });
+ }
+ updateRecoveryCodes(sessionToken, recoveryCodes) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPut('/recoveryCodes', sessionToken, { recoveryCodes });
+ });
+ }
+ consumeRecoveryCode(sessionToken, code) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/session/verify/recoveryCode', sessionToken, {
+ code,
+ });
+ });
+ }
+ createRecoveryKey(sessionToken, recoveryKeyId, recoveryData, enabled) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/recoveryKey', sessionToken, {
+ recoveryKeyId,
+ recoveryData,
+ enabled,
+ });
+ });
+ }
+ getRecoveryKey(accountResetToken, recoveryKeyId) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.hawkRequest('GET', `/recoveryKey/${recoveryKeyId}`, accountResetToken, tokenType.accountResetToken);
+ });
+ }
+ resetPasswordWithRecoveryKey(accountResetToken, email, newPassword, recoveryKeyId, keys, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentials = yield crypto.getCredentials(email, newPassword);
+ const newWrapKb = crypto.unwrapKB(keys.kB, credentials.unwrapBKey);
+ const payload = {
+ wrapKb: newWrapKb,
+ authPW: credentials.authPW,
+ sessionToken: options.sessionToken,
+ recoveryKeyId,
+ };
+ const accountData = yield this.hawkRequest('POST', pathWithKeys('/account/reset', options.keys), accountResetToken, tokenType.accountResetToken, payload);
+ if (options.keys && accountData.keyFetchToken) {
+ accountData.unwrapBKey = credentials.unwrapBKey;
+ }
+ return accountData;
+ });
+ }
+ deleteRecoveryKey(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.hawkRequest('DELETE', '/recoveryKey', sessionToken, tokenType.sessionToken, {});
+ });
+ }
+ recoveryKeyExists(sessionToken, email) {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (sessionToken) {
+ return this.sessionPost('/recoveryKey/exists', sessionToken, { email });
+ }
+ return this.request('POST', '/recoveryKey/exists', { email });
+ });
+ }
+ verifyRecoveryKey(sessionToken, recoveryKeyId) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/recoveryKey/verify', sessionToken, {
+ recoveryKeyId,
+ });
+ });
+ }
+ createOAuthCode(sessionToken, clientId, state, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/oauth/authorization', sessionToken, {
+ access_type: options.access_type,
+ acr_values: options.acr_values,
+ client_id: clientId,
+ code_challenge: options.code_challenge,
+ code_challenge_method: options.code_challenge_method,
+ keys_jwe: options.keys_jwe,
+ redirect_uri: options.redirect_uri,
+ response_type: options.response_type,
+ scope: options.scope,
+ state,
+ });
+ });
+ }
+ createOAuthToken(sessionToken, clientId, options = {}) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/oauth/token', sessionToken, {
+ grant_type: 'fxa-credentials',
+ access_type: options.access_type,
+ client_id: clientId,
+ scope: options.scope,
+ ttl: options.ttl,
+ });
+ });
+ }
+ getOAuthScopedKeyData(sessionToken, clientId, scope) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/account/scoped-key-data', sessionToken, {
+ client_id: clientId,
+ scope,
+ });
+ });
+ }
+ getSubscriptionPlans() {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.request('GET', '/oauth/subscriptions/plans');
+ });
+ }
+ getActiveSubscriptions(accessToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.request('GET', '/oauth/subscriptions/active', null, new Headers({
+ authorization: `Bearer ${accessToken}`,
+ }));
+ });
+ }
+ createSupportTicket(accessToken, supportTicket) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.request('POST', '/support/ticket', supportTicket, new Headers({
+ authorization: `Bearer ${accessToken}`,
+ }));
+ });
+ }
+ updateNewsletters(sessionToken, newsletters) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/newsletters', sessionToken, {
+ newsletters,
+ });
+ });
+ }
+ verifyIdToken(idToken, clientId, expiryGracePeriod) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const payload = {
+ id_token: idToken,
+ client_id: clientId,
+ };
+ if (expiryGracePeriod) {
+ payload.expiry_grace_period = expiryGracePeriod;
+ }
+ return this.request('POST', '/oauth/id-token-verify', payload);
+ });
+ }
+ sendPushLoginRequest(sessionToken) {
+ return __awaiter(this, void 0, void 0, function* () {
+ return this.sessionPost('/session/verify/send_push', sessionToken, {});
+ });
+ }
+}
+AuthClient.VERSION = 'v1';
diff --git a/web/js/browser/lib/crypto.js b/web/js/browser/lib/crypto.js
new file mode 100644
index 0000000..6fa6107
--- /dev/null
+++ b/web/js/browser/lib/crypto.js
@@ -0,0 +1,163 @@
+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());
+ });
+};
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+import { hexToUint8, uint8ToBase64Url, uint8ToHex, xor } from './utils';
+const encoder = () => new TextEncoder();
+const NAMESPACE = 'identity.mozilla.com/picl/v1/';
+// These functions implement the onepw protocol
+// https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol
+export function getCredentials(email, password) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const passkey = yield crypto.subtle.importKey('raw', encoder().encode(password), 'PBKDF2', false, ['deriveBits']);
+ const quickStretchedRaw = yield crypto.subtle.deriveBits({
+ name: 'PBKDF2',
+ salt: encoder().encode(`${NAMESPACE}quickStretch:${email}`),
+ iterations: 1000,
+ hash: 'SHA-256',
+ }, passkey, 256);
+ const quickStretchedKey = yield crypto.subtle.importKey('raw', quickStretchedRaw, 'HKDF', false, ['deriveBits']);
+ const authPW = yield crypto.subtle.deriveBits({
+ name: 'HKDF',
+ salt: new Uint8Array(0),
+ // The builtin ts type definition for HKDF was wrong
+ // at the time this was written, hence the ignore
+ // @ts-ignore
+ info: encoder().encode(`${NAMESPACE}authPW`),
+ hash: 'SHA-256',
+ }, quickStretchedKey, 256);
+ const unwrapBKey = yield crypto.subtle.deriveBits({
+ name: 'HKDF',
+ salt: new Uint8Array(0),
+ // @ts-ignore
+ info: encoder().encode(`${NAMESPACE}unwrapBkey`),
+ hash: 'SHA-256',
+ }, quickStretchedKey, 256);
+ return {
+ authPW: uint8ToHex(new Uint8Array(authPW)),
+ unwrapBKey: uint8ToHex(new Uint8Array(unwrapBKey)),
+ };
+ });
+}
+export function deriveBundleKeys(key, keyInfo, payloadBytes = 64) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const baseKey = yield crypto.subtle.importKey('raw', hexToUint8(key), 'HKDF', false, ['deriveBits']);
+ const keyMaterial = yield crypto.subtle.deriveBits({
+ name: 'HKDF',
+ salt: new Uint8Array(0),
+ // @ts-ignore
+ info: encoder().encode(`${NAMESPACE}${keyInfo}`),
+ hash: 'SHA-256',
+ }, baseKey, (32 + payloadBytes) * 8);
+ const hmacKey = yield crypto.subtle.importKey('raw', new Uint8Array(keyMaterial.slice(0, 32)), {
+ name: 'HMAC',
+ hash: 'SHA-256',
+ length: 256,
+ }, true, ['verify']);
+ const xorKey = new Uint8Array(keyMaterial.slice(32));
+ return {
+ hmacKey,
+ xorKey,
+ };
+ });
+}
+export function unbundleKeyFetchResponse(key, bundle) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const b = hexToUint8(bundle);
+ const keys = yield deriveBundleKeys(key, 'account/keys');
+ const ciphertext = b.subarray(0, 64);
+ const expectedHmac = b.subarray(b.byteLength - 32);
+ const valid = yield crypto.subtle.verify('HMAC', keys.hmacKey, expectedHmac, ciphertext);
+ if (!valid) {
+ throw new Error('Bad HMac');
+ }
+ const keyAWrapB = xor(ciphertext, keys.xorKey);
+ return {
+ kA: uint8ToHex(keyAWrapB.subarray(0, 32)),
+ wrapKB: uint8ToHex(keyAWrapB.subarray(32)),
+ };
+ });
+}
+export function unwrapKB(wrapKB, unwrapBKey) {
+ return uint8ToHex(xor(hexToUint8(wrapKB), hexToUint8(unwrapBKey)));
+}
+export function hkdf(keyMaterial, salt, info, bytes) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const key = yield crypto.subtle.importKey('raw', keyMaterial, 'HKDF', false, [
+ 'deriveBits',
+ ]);
+ const result = yield crypto.subtle.deriveBits({
+ name: 'HKDF',
+ salt,
+ // @ts-ignore
+ info,
+ hash: 'SHA-256',
+ }, key, bytes * 8);
+ return new Uint8Array(result);
+ });
+}
+export function jweEncrypt(keyMaterial, kid, data, forTestingOnly) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const key = yield crypto.subtle.importKey('raw', keyMaterial, {
+ name: 'AES-GCM',
+ }, false, ['encrypt']);
+ const jweHeader = uint8ToBase64Url(encoder().encode(JSON.stringify({
+ enc: 'A256GCM',
+ alg: 'dir',
+ kid,
+ })));
+ const iv = (forTestingOnly === null || forTestingOnly === void 0 ? void 0 : forTestingOnly.testIV) || crypto.getRandomValues(new Uint8Array(12));
+ const encrypted = yield crypto.subtle.encrypt({
+ name: 'AES-GCM',
+ iv,
+ additionalData: encoder().encode(jweHeader),
+ tagLength: 128,
+ }, key, data);
+ const ciphertext = new Uint8Array(encrypted.slice(0, encrypted.byteLength - 16));
+ const authenticationTag = new Uint8Array(encrypted.slice(encrypted.byteLength - 16));
+ // prettier-ignore
+ const compactJWE = `${jweHeader}..${uint8ToBase64Url(iv)}.${uint8ToBase64Url(ciphertext)}.${uint8ToBase64Url(authenticationTag)}`;
+ return compactJWE;
+ });
+}
+export function checkWebCrypto() {
+ return __awaiter(this, void 0, void 0, function* () {
+ try {
+ yield crypto.subtle.importKey('raw', crypto.getRandomValues(new Uint8Array(16)), 'PBKDF2', false, ['deriveKey']);
+ yield crypto.subtle.importKey('raw', crypto.getRandomValues(new Uint8Array(32)), 'HKDF', false, ['deriveKey']);
+ yield crypto.subtle.importKey('raw', crypto.getRandomValues(new Uint8Array(32)), {
+ name: 'HMAC',
+ hash: 'SHA-256',
+ length: 256,
+ }, false, ['sign']);
+ yield crypto.subtle.importKey('raw', crypto.getRandomValues(new Uint8Array(32)), {
+ name: 'AES-GCM',
+ }, false, ['encrypt']);
+ yield crypto.subtle.digest('SHA-256', crypto.getRandomValues(new Uint8Array(16)));
+ return true;
+ }
+ catch (err) {
+ try {
+ console.warn('loading webcrypto shim', err);
+ // prettier-ignore
+ // @ts-ignore
+ window.asmCrypto = yield import(/* webpackChunkName: "asmcrypto.js" */ 'asmcrypto.js');
+ // prettier-ignore
+ // @ts-ignore
+ yield import(/* webpackChunkName: "webcrypto-liner" */ 'webcrypto-liner/build/shim');
+ return true;
+ }
+ catch (e) {
+ return false;
+ }
+ }
+ });
+}
diff --git a/web/js/browser/lib/hawk.d.ts b/web/js/browser/lib/hawk.d.ts
new file mode 100644
index 0000000..d4e5263
--- /dev/null
+++ b/web/js/browser/lib/hawk.d.ts
@@ -0,0 +1,24 @@
+/// <reference types="./lib/types" />
+export declare function deriveHawkCredentials(token: hexstring, context: string): Promise<{
+ id: string;
+ key: Uint8Array;
+ bundleKey: string;
+}>;
+export declare function hawkHeader(method: string, uri: string, options: {
+ credentials: {
+ id: string;
+ key: Uint8Array;
+ };
+ payload?: string;
+ timestamp?: number;
+ nonce?: string;
+ contentType?: string;
+ localtimeOffsetMsec?: number;
+}): Promise<string>;
+export declare function header(method: string, uri: string, token: string, kind: string, options: {
+ payload?: string;
+ timestamp?: number;
+ nonce?: string;
+ contentType?: string;
+ localtimeOffsetMsec?: number;
+}): Promise<Headers>;
diff --git a/web/js/browser/lib/hawk.js b/web/js/browser/lib/hawk.js
new file mode 100644
index 0000000..69c7153
--- /dev/null
+++ b/web/js/browser/lib/hawk.js
@@ -0,0 +1,145 @@
+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());
+ });
+};
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+import { hexToUint8, uint8ToBase64, uint8ToHex } from './utils';
+const encoder = () => new TextEncoder();
+const NAMESPACE = 'identity.mozilla.com/picl/v1/';
+export function deriveHawkCredentials(token, context) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const baseKey = yield crypto.subtle.importKey('raw', hexToUint8(token), 'HKDF', false, ['deriveBits']);
+ const keyMaterial = yield crypto.subtle.deriveBits({
+ name: 'HKDF',
+ salt: new Uint8Array(0),
+ // @ts-ignore
+ info: encoder().encode(`${NAMESPACE}${context}`),
+ hash: 'SHA-256',
+ }, baseKey, 32 * 3 * 8);
+ const id = new Uint8Array(keyMaterial.slice(0, 32));
+ const authKey = new Uint8Array(keyMaterial.slice(32, 64));
+ const bundleKey = new Uint8Array(keyMaterial.slice(64));
+ return {
+ id: uint8ToHex(id),
+ key: authKey,
+ bundleKey: uint8ToHex(bundleKey),
+ };
+ });
+}
+// The following is adapted from https://github.com/hapijs/hawk/blob/master/lib/browser.js
+/*
+ HTTP Hawk Authentication Scheme
+ Copyright (c) 2012-2013, Eran Hammer <eran@hueniverse.com>
+ MIT Licensed
+ */
+function parseUri(input) {
+ const parts = input.match(/^([^:]+)\:\/\/(?:[^@/]*@)?([^\/:]+)(?:\:(\d+))?([^#]*)(?:#.*)?$/);
+ if (!parts) {
+ return { host: '', port: '', resource: '' };
+ }
+ const scheme = parts[1].toLowerCase();
+ const uri = {
+ host: parts[2],
+ port: parts[3] || (scheme === 'http' ? '80' : scheme === 'https' ? '443' : ''),
+ resource: parts[4],
+ };
+ return uri;
+}
+function randomString(size) {
+ const randomSource = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ const len = randomSource.length;
+ const result = [];
+ for (let i = 0; i < size; ++i) {
+ result[i] = randomSource[Math.floor(Math.random() * len)];
+ }
+ return result.join('');
+}
+function generateNormalizedString(type, options) {
+ let normalized = 'hawk.1.' +
+ type +
+ '\n' +
+ options.ts +
+ '\n' +
+ options.nonce +
+ '\n' +
+ (options.method || '').toUpperCase() +
+ '\n' +
+ (options.resource || '') +
+ '\n' +
+ options.host.toLowerCase() +
+ '\n' +
+ options.port +
+ '\n' +
+ (options.hash || '') +
+ '\n';
+ if (options.ext) {
+ normalized += options.ext.replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
+ }
+ normalized += '\n';
+ if (options.app) {
+ normalized += options.app + '\n' + (options.dlg || '') + '\n';
+ }
+ return normalized;
+}
+function calculatePayloadHash(payload = '', contentType = '') {
+ return __awaiter(this, void 0, void 0, function* () {
+ const data = encoder().encode(`hawk.1.payload\n${contentType}\n${payload}\n`);
+ const hash = yield crypto.subtle.digest('SHA-256', data);
+ return uint8ToBase64(new Uint8Array(hash));
+ });
+}
+function calculateMac(type, credentials, options) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const normalized = generateNormalizedString(type, options);
+ const key = yield crypto.subtle.importKey('raw', credentials.key, {
+ name: 'HMAC',
+ hash: 'SHA-256',
+ length: 256,
+ }, true, ['sign']);
+ const hmac = yield crypto.subtle.sign('HMAC', key, encoder().encode(normalized));
+ return uint8ToBase64(new Uint8Array(hmac));
+ });
+}
+export function hawkHeader(method, uri, options) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const timestamp = options.timestamp ||
+ Math.floor((Date.now() + (options.localtimeOffsetMsec || 0)) / 1000);
+ const parsedUri = parseUri(uri);
+ const hash = yield calculatePayloadHash(options.payload, options.contentType);
+ const artifacts = {
+ ts: timestamp,
+ nonce: options.nonce || randomString(6),
+ method,
+ resource: parsedUri.resource,
+ host: parsedUri.host,
+ port: parsedUri.port,
+ hash,
+ };
+ const mac = yield calculateMac('header', options.credentials, artifacts);
+ const header = 'Hawk id="' +
+ options.credentials.id +
+ '", ts="' +
+ artifacts.ts +
+ '", nonce="' +
+ artifacts.nonce +
+ (artifacts.hash ? '", hash="' + artifacts.hash : '') +
+ '", mac="' +
+ mac +
+ '"';
+ return header;
+ });
+}
+export function header(method, uri, token, kind, options) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const credentials = yield deriveHawkCredentials(token, kind);
+ const authorization = yield hawkHeader(method, uri, Object.assign({ credentials }, options));
+ return new Headers({ authorization });
+ });
+}
diff --git a/web/js/browser/lib/recoveryKey.js b/web/js/browser/lib/recoveryKey.js
new file mode 100644
index 0000000..cc8604d
--- /dev/null
+++ b/web/js/browser/lib/recoveryKey.js
@@ -0,0 +1,38 @@
+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());
+ });
+};
+import { jweEncrypt, hkdf } from './crypto';
+import { hexToUint8, uint8ToHex } from './utils';
+function randomKey() {
+ return __awaiter(this, void 0, void 0, function* () {
+ // The key is displayed in base32 'Crockford' so the length should be
+ // divisible by (5 bits per character) and (8 bits per byte).
+ // 20 bytes == 160 bits == 32 base32 characters
+ const recoveryKey = yield crypto.getRandomValues(new Uint8Array(20));
+ // Flip bits to set first char to base32 'A' as a version identifier.
+ // Why 'A'? https://github.com/mozilla/fxa-content-server/pull/6323#discussion_r201211711
+ recoveryKey[0] = 0x50 | (0x07 & recoveryKey[0]);
+ return recoveryKey;
+ });
+}
+export function generateRecoveryKey(uid, keys, forTestingOnly) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const recoveryKey = (forTestingOnly === null || forTestingOnly === void 0 ? void 0 : forTestingOnly.testRecoveryKey) || (yield randomKey());
+ const encoder = new TextEncoder();
+ const salt = hexToUint8(uid);
+ const encryptionKey = yield hkdf(recoveryKey, salt, encoder.encode('fxa recovery encrypt key'), 32);
+ const recoveryKeyId = uint8ToHex(yield hkdf(recoveryKey, salt, encoder.encode('fxa recovery fingerprint'), 16));
+ const recoveryData = yield jweEncrypt(encryptionKey, recoveryKeyId, encoder.encode(JSON.stringify(keys)), forTestingOnly ? { testIV: forTestingOnly.testIV } : undefined);
+ return {
+ recoveryKey,
+ recoveryKeyId,
+ recoveryData,
+ };
+ });
+}
diff --git a/web/js/browser/lib/utils.js b/web/js/browser/lib/utils.js
new file mode 100644
index 0000000..5bf0383
--- /dev/null
+++ b/web/js/browser/lib/utils.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/;
+export function hexToUint8(str) {
+ if (!HEX_STRING.test(str)) {
+ throw new Error(`invalid hex string: ${str}`);
+ }
+ const bytes = str.match(/[a-fA-F0-9]{2}/g);
+ return new Uint8Array(bytes.map((byte) => parseInt(byte, 16)));
+}
+export function uint8ToHex(array) {
+ return array.reduce((str, byte) => str + ('00' + byte.toString(16)).slice(-2), '');
+}
+export function uint8ToBase64(array) {
+ return btoa(String.fromCharCode(...array));
+}
+export function uint8ToBase64Url(array) {
+ return uint8ToBase64(array)
+ .replace(/=/g, '')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_');
+}
+export function xor(array1, array2) {
+ return new Uint8Array(array1.map((byte, i) => byte ^ array2[i]));
+}