web-enc-at-rest
Version:
Encryption-at-Rest for Web Apps Library
104 lines (100 loc) • 5.55 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.matchOrCreateCredentialProof = exports.generateCredentialProof = exports.generateCredentialKey = exports.getOrCreateDeriveKeySalt = void 0;
const protectedCrypto_1 = require("./protectedCrypto");
const randomUtil_1 = require("./randomUtil");
const dataConvertUtil_1 = require("./dataConvertUtil");
const keyGenStore_1 = require("./keyGenStore");
const arrayUtil_1 = require("./arrayUtil");
const appDataEncryption_1 = require("./appDataEncryption");
/** Explanation of salt reuse:
The salt returned by getOrCreateDeriveKeySalt() is used along with the password to derive a key. It's nearly always true
in cryptographic use cases that you would want to use a new salt value every time a value is encrypted. But in this
use case, we want the same credentials to consistently derive the same key across multiple derivations. Otherwise,
the key will always be a new key that is unusable for decrypting previously-encrypted app data.
The derived key is only kept in-memory, which limits an attacker's ability to compare the derived key value against a
matching value that could reveal credentials. An attacker could gain access to browser memory, e.g. the browser
executable is patched with malware. But in this case, other attack vectors of greater opportunity will be available to
the attacker.
The app data itself is encrypted without reuse of salt/IV values.
*/
const PBKDF2_SALT_BYTE_LENGTH = 16;
function getOrCreateDeriveKeySalt() {
let deriveKeySalt = (0, keyGenStore_1.getDeriveKeySalt)();
if (!deriveKeySalt) {
deriveKeySalt = (0, randomUtil_1.randomBytes)(PBKDF2_SALT_BYTE_LENGTH);
(0, keyGenStore_1.setDeriveKeySalt)(deriveKeySalt);
}
return deriveKeySalt;
}
exports.getOrCreateDeriveKeySalt = getOrCreateDeriveKeySalt;
function _addUserNameToSalt(userName, salt) {
let saltI = 0, saltLength = salt.length;
for (let i = 0; i < userName.length; ++i) {
salt[saltI] = (salt[saltI] + userName.charCodeAt(i)) % 256;
if (++saltI === saltLength)
saltI = 0;
}
}
const DERIVE_KEY_ITERATIONS = 100000;
function _generateCredentialKeyBytes(subtle, userName, password) {
return __awaiter(this, void 0, void 0, function* () {
const salt = getOrCreateDeriveKeySalt();
_addUserNameToSalt(userName, salt);
const passphraseUint8 = (0, dataConvertUtil_1.stringToBytes)(password);
const algorithmParams = { name: 'PBKDF2', hash: 'SHA-256', salt, iterations: DERIVE_KEY_ITERATIONS };
const baseKey = yield subtle.importKey('raw', passphraseUint8, 'PBKDF2', false, ['deriveKey']);
const derivedParams = { name: 'AES-GCM', length: 128 };
const credentialKey = yield subtle.deriveKey(algorithmParams, baseKey, derivedParams, true, ['decrypt', 'encrypt']);
return new Uint8Array(yield subtle.exportKey('raw', credentialKey));
});
}
function generateCredentialKey(userName, password) {
return __awaiter(this, void 0, void 0, function* () {
const subtle = (0, protectedCrypto_1.getSubtle)();
const keyBytes = yield _generateCredentialKeyBytes(subtle, userName, password);
return yield subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['decrypt', 'encrypt']);
});
}
exports.generateCredentialKey = generateCredentialKey;
function _createCredentialProofPlainText() {
const proof = new Uint8Array(256);
for (let i = 0; i < 256; ++i) {
proof[i] = i;
}
return proof;
}
const CREDENTIAL_PROOF_PLAINTEXT = _createCredentialProofPlainText();
function generateCredentialProof(credentialKey) {
return __awaiter(this, void 0, void 0, function* () {
return (0, appDataEncryption_1.encryptAppData)(credentialKey, CREDENTIAL_PROOF_PLAINTEXT);
});
}
exports.generateCredentialProof = generateCredentialProof;
function matchOrCreateCredentialProof(credentialKey) {
return __awaiter(this, void 0, void 0, function* () {
const credentialProof = (0, keyGenStore_1.getCredentialProof)();
if (credentialProof === null) { // Store new proof if there isn't already one.
const newCredentialProof = yield generateCredentialProof(credentialKey);
(0, keyGenStore_1.setCredentialProof)(newCredentialProof);
return true;
}
try { // Otherwise, need to verify the proof value can be decrypted.
const credentialProofPlaintext = yield (0, appDataEncryption_1.decryptAppData)(credentialKey, credentialProof);
return (0, arrayUtil_1.areUint8ArraysEqual)(credentialProofPlaintext, CREDENTIAL_PROOF_PLAINTEXT);
}
catch (ignored) { // If credentials are incorrect, decrypt will fail.
return false;
}
});
}
exports.matchOrCreateCredentialProof = matchOrCreateCredentialProof;