blob: 4fd781c539561aab07c0f83f3ec86cbf9fb60caf [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* @fileoverview A general-purpose cache for user credentials in the form of
* a Uint8Array.
*/
/**
* A general-purpose cache for user credentials in the form of a Uint8Array.
*
* The data in the cache is encrypted at rest using a random Web Crypto AES-CBC
* key.
*
* Security considerations:
*
* - The cache is only used by the current Secure Shell window and
* cleared when the screen locks.
* - Data in the cache is encrypted using a random Web Crypto AES-CBC
* key configured to be non-extractable and a randomly generated IV for
* every encryption operation.
* - Since the Web Crypto spec does not require non-extractable keys to be
* kept in a secure hardware element or protected memory, a leak of
* memory contents should realistically be considered equivalent to a
* full compromise.
* - Malicious servers can abuse the caching behavior for e.g. user passwords
* and smart card PINs if the user has agent forwarding enabled.
*
* @constructor
*/
lib.CredentialCache = function() {
/**
* The underlying cache. Keys are strings and the cache entries consist of the
* encrypted data and the initialization vector (IV) used for the AES
* encryption in CBC mode.
*
* @private {?Object<string, {
* encryptedData: !ArrayBuffer,
* iv: !ArrayBufferView,
* }>}
*/
this.cache_ = null;
/**
* The Web Crypto API AES-CBC CryptoKey that has been used to encrypt the data
* in the cache. The key is randomly generated during initialization or after
* the cache has been cleared.
*
* @private {?webCrypto.CryptoKey}
*/
this.cryptoKey_ = null;
/**
* Set to true if caching is enabled; false if caching is disabled and null if
* the user has not yet made a decision.
*
* @private {?boolean}
*/
this.enabled_ = null;
// Clear the cache on screen lock.
if (window.chrome && chrome.idle) {
chrome.idle.onStateChanged.addListener((state) => {
if (state === 'locked') {
this.clear_();
}
});
}
};
/**
* Initialize the cache and generate a new, non-extractable encryption key.
*
* @return {!Promise.<void>}
* @private
*/
lib.CredentialCache.prototype.init_ = async function() {
this.cache_ = {};
this.cryptoKey_ = /** @type {!webCrypto.CryptoKey} */ (
await window.crypto.subtle.generateKey(
{name: 'AES-CBC', length: 128}, false, ['encrypt', 'decrypt']));
};
/**
* Clear the cache and delete the reference to the encryption key.
*
* @private
*/
lib.CredentialCache.prototype.clear_ = function() {
this.cache_ = null;
this.cryptoKey_ = null;
this.enabled_ = null;
};
/**
* Retrieve the cached data for the given key string.
*
* Note: The data bytes in the returned Uint8Array should be overwritten after
* use.
*
* Note: In order to ensure consistency, upon successful retrieval the cached
* data is deleted and should be added again after its validity has been
* verified.
*
* @param {string} key The key to which the corresponding data should be
* looked up.
* @return {?Promise<?Uint8Array>} The data bytes if the key is present in the
* cache; null otherwise.
*/
lib.CredentialCache.prototype.retrieve = async function(key) {
if (!this.cache_) {
await this.init_();
}
if (key in this.cache_) {
const {encryptedData, iv} = this.cache_[key];
// Remove cache entry to be added again only if data verification succeeds.
delete this.cache_[key];
return new Uint8Array(await window.crypto.subtle.decrypt(
{name: 'AES-CBC', iv}, lib.notNull(this.cryptoKey_), encryptedData));
}
return null;
};
/**
* Store the data in the cache under the given key.
*
* Note: The provided data array is overwritten with zeroes after the data has
* been added to the cache.
*
* @param {string} key The key under which the data should be stored in the
* cache.
* @param {!Uint8Array} data The data bytes to be stored.
* @return {?Promise<void>}
*/
lib.CredentialCache.prototype.store = async function(key, data) {
if (!this.cache_) {
await this.init_();
}
// AES-CBC requires a new, cryptographically random IV for every operation.
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const encryptedData = await window.crypto.subtle.encrypt(
{name: 'AES-CBC', iv}, lib.notNull(this.cryptoKey_), data.buffer);
data.fill(0);
this.cache_[key] = {encryptedData, iv};
};
/**
* Check whether caching is enabled.
*
* @return {?boolean} True if the user enabled caching; false if the user
* disabled caching; null if the user has not made a decision yet.
*/
lib.CredentialCache.prototype.isEnabled = function() {
return this.enabled_;
};
/**
* Enable or disable caching.
*
* Note: Caching can only be enabled or disabled once.
*
* @param {boolean} enable
*/
lib.CredentialCache.prototype.setEnabled = function(enable) {
if (this.enabled_ === null) {
this.enabled_ = enable;
}
};