web-enc-at-rest
Version:
Encryption-at-Rest for Web Apps Library
181 lines (180 loc) • 10.5 kB
JavaScript
;
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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.decryptObject = exports.encryptObject = exports.decryptBytes = exports.encryptBytes = exports.close = exports.open = exports.changeCredentialsAndReEncrypt = exports.dangerouslyDeInitialize = exports.isInitialized = void 0;
const keyGen_1 = require("./keyGen");
const appDataEncryption_1 = require("./appDataEncryption");
const keyGenStore_1 = require("./keyGenStore");
const WearContext_1 = require("./WearContext");
const dataConvertUtil_1 = require("./dataConvertUtil");
const base64Util_1 = require("./base64Util");
/** Checks to see if a context was previously opened via open(). This can be useful
* to present appropriate UI in the app for either request existing credentials (e.g. "log in") or
* accept new credentials (e.g. "create account").
*
* @global
*
* @return {boolean} True if context was previously opened, or false if not. */
function isInitialized() {
return (0, keyGenStore_1.getCredentialProof)() !== null;
}
exports.isInitialized = isInitialized;
/** Clears any information used to verify credentials or generate credential keys. Read the warning below before
* calling this.
*
* WARNING: After this call, you won't be able to generate the same key from credentials. If you've got user data
* encrypted with it, that data will be bricked.
*
* @global */
function dangerouslyDeInitialize() {
(0, keyGenStore_1.setCredentialProof)(null);
(0, keyGenStore_1.setDeriveKeySalt)(null);
}
exports.dangerouslyDeInitialize = dangerouslyDeInitialize;
/** Returns a context derived from new credentials and calls a specified callback that will perform re-encryption.
* The function's logic is meant as a safeguard to avoid bricking user data when credentials change. If app code
* follows implementation instructions, this function will succeed or fail atomically, leaving user data in an
* accessible state.
*
* @async
*
* @param {WearContext} oldContext Context containing key under which data is currently encrypted. oldContext will be closed
* and be unusable if this function returns successfully.
* @param {string} newUserName New user name, which would commonly be the same as previous user name, but doesn't have to be.
* @param {string} newPassword Along with new user name, this comprises the credentials from which a new key will be derived.
* @param {ReEncryptCallback} onReEncrypt Callback to app-supplied function which will perform re-encryption of user data.
* @return {Promise<WearContext>} Promise resolving to new context generated from new credentials if everything was successful. */
function changeCredentialsAndReEncrypt(oldContext, newUserName, newPassword, onReEncrypt) {
return __awaiter(this, void 0, void 0, function* () {
if (oldContext.isClear())
throw Error('oldContext is unusable because it was closed.');
const newCredentialKey = yield (0, keyGen_1.generateCredentialKey)(newUserName, newPassword);
const newContext = new WearContext_1.default(newCredentialKey);
const newCredentialProof = yield (0, keyGen_1.generateCredentialProof)(newCredentialKey);
if (!(yield onReEncrypt(oldContext, newContext)))
throw Error('Re-encryption failed. The current context has not been changed.');
oldContext.clear();
(0, keyGenStore_1.setCredentialProof)(newCredentialProof);
return newContext;
});
}
exports.changeCredentialsAndReEncrypt = changeCredentialsAndReEncrypt;
/** Returns a context that is needed for passing to other APIs or null if passed credentials are incorrect.
*
* A natural time to call this is right after user has entered credentials and you've successfully performed any
* authentication that your app requires. open() is idempotent and you can call it multiple times.
*
* @async
*
* @param {string} userName Uniquely identifies user.
* @param {string} password Password for user.
* @returns {Promise<WearContext>} Promise resolving to context that can be passed to other APIs. Treat this opaquely.
* DO NOT store in any place but memory. */
function open(userName, password) {
return __awaiter(this, void 0, void 0, function* () {
const credentialKey = yield (0, keyGen_1.generateCredentialKey)(userName, password);
if (!(yield (0, keyGen_1.matchOrCreateCredentialProof)(credentialKey)))
return null;
return new WearContext_1.default(credentialKey);
});
}
exports.open = open;
/** Prevent any further encryption/decryption with the passed-in context. Useful for preventing attacks based on
* physical access to the user's device, e.g. user leaves browser open on an unlocked, unattended laptop.
*
* A natural time to call this is whenever a user logs out. If you generated multiple contexts that
* were stored in separate variables, then call `close()` on each. If the user closes the tab or browser before
* you can call open(), there is no risk as the context is already cleared from memory.
*
* @param {WearContext} context From a previous call to open(). */
function close(context) {
context.clear();
}
exports.close = close;
/** Encrypts byte array to a string that you can use for writing to persistent storage.
*
* @async
*
* @param {WearContext} context From a previous call to open().
* @param {Uint8Array} bytes Value to encrypt.
* @return {Promise<string>} Promise resolving to base64-encoded string of encrypted data. */
function encryptBytes(context, bytes) {
return __awaiter(this, void 0, void 0, function* () {
if (context.isClear())
throw Error('Attempted to use a closed context.');
const credentialKey = context.dangerouslyGetKey();
return (0, base64Util_1.bytesToBase64)(yield (0, appDataEncryption_1.encryptAppData)(credentialKey, bytes));
});
}
exports.encryptBytes = encryptBytes;
/** Decrypts string to a byte array.
*
* @async
*
* @param {WearContext} context From a previous call to open(). The credentials that generated the context must match
* credentials provided earlier in session where encryptedData was encrypted.
* @param {string} encryptedData Must have been generated with a previous call to `encryptBytes()`.
* @return {Promise<Uint8Array>} Promise resolving to Unencrypted data. */
function decryptBytes(context, encryptedData) {
return __awaiter(this, void 0, void 0, function* () {
if (context.isClear())
throw Error('Attempted to use a closed context.');
const credentialKey = context.dangerouslyGetKey();
const ciphertextBytes = (0, base64Util_1.base64ToBytes)(encryptedData);
return yield (0, appDataEncryption_1.decryptAppData)(credentialKey, ciphertextBytes);
});
}
exports.decryptBytes = decryptBytes;
/** Encrypts object to a ciphertext string that you can use for writing to persistent storage.
*
* If the object is not entirely representable in JSON, you'll need to pass a replacer function to handle
* serialization, and pass a reviver function later to `decryptObject()` to symmetrically perform deserialization.
* To understand if your object is JSON-representable, call `JSON.parse(JSON.stringify(yourObject))` and see if it
* returns the same `yourObject` value.
*
* WEaR adds support for (de)serializing the following primitive values: null, Infinity, -Infinity, and NaN.
*
* @async
*
* @param {WearContext} context From a previous call to open().
* @param {object} object Value to encrypt.
* @param {function} replacer Optional function to serialize values correctly. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter
* @return {Promise<string>} Promise resolving to ciphertext string. */
function encryptObject(context, object, replacer) {
return __awaiter(this, void 0, void 0, function* () {
const plainTextBytes = (0, dataConvertUtil_1.anyToBytes)(object, replacer);
return yield encryptBytes(context, plainTextBytes);
});
}
exports.encryptObject = encryptObject;
/** Decrypts ciphertext string to an object.
*
* If the object is not entirely representable in JSON, you'll need to pass a reviver function to handle
* deserialization that matches a replacer function previously passed to `encryptObject()` for the same data.
* To understand if your object is JSON-representable, call `JSON.parse(JSON.stringify(yourObject))` and see if it
* returns the same `yourObject` value.
*
* WEaR adds support for (de)serializing the following primitive values: null, Infinity, -Infinity, and NaN.
*
* @async
*
* @param {WearContext} context From a previous call to open(). The credentials that generated the context must match
* credentials provided earlier in session where encryptedData was encrypted.
* @param {string} encryptedData Must have been generated with a previous call to `encryptObject()`.
* @param {function} reviver Optional function to deserialize values correctly. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#using_the_reviver_parameter
* @return {Promise<object>} Promise resolving to Unencrypted data. */
function decryptObject(context, encryptedData, reviver) {
return __awaiter(this, void 0, void 0, function* () {
const plainTextBytes = yield decryptBytes(context, encryptedData);
return (0, dataConvertUtil_1.bytesToAny)(plainTextBytes, reviver);
});
}
exports.decryptObject = decryptObject;