Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/**
* Class to handle encryption and decryption of logins stored in Chrome/Chromium
* on macOS.
*/
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
XPCOMUtils.defineLazyServiceGetter(
lazy,
"gKeychainUtils",
"@mozilla.org/profile/migrator/keychainmigrationutils;1",
"nsIKeychainMigrationUtils"
);
const gTextEncoder = new TextEncoder();
const gTextDecoder = new TextDecoder();
/**
* From macOS' CommonCrypto/CommonCryptor.h
*/
const kCCBlockSizeAES128 = 16;
/* Chromium constants */
/**
* kSalt from Chromium.
*
*/
const SALT = "saltysalt";
/**
* kDerivedKeySizeInBits from Chromium.
*
*/
const DERIVED_KEY_SIZE_BITS = 128;
/**
* kEncryptionIterations from Chromium.
*
*/
const ITERATIONS = 1003;
/**
* kEncryptionVersionPrefix from Chromium.
*
*/
const ENCRYPTION_VERSION_PREFIX = "v10";
/**
* The initialization vector is 16 space characters (character code 32 in decimal).
*
*/
const IV = new Uint8Array(kCCBlockSizeAES128).fill(32);
/**
* Instances of this class have a shape similar to OSCrypto so it can be dropped
* into code which uses that. This isn't implemented as OSCrypto_mac.js since
* it isn't calling into encryption functions provided by macOS but instead
* relies on OS encryption key storage in Keychain. The algorithms here are
* specific to what is needed for Chrome login storage on macOS.
*/
export class ChromeMacOSLoginCrypto {
/**
* @param {string} serviceName of the Keychain Item to use to derive a key.
* @param {string} accountName of the Keychain Item to use to derive a key.
* @param {string?} [testingPassphrase = null] A string to use as the passphrase
* to derive a key for testing purposes rather than retrieving
* it from the macOS Keychain since we don't yet have a way to
* mock the Keychain auth dialog.
*/
constructor(serviceName, accountName, testingPassphrase = null) {
// We still exercise the keychain migration utils code when using a
// `testingPassphrase` in order to get some test coverage for that
// component, even though it's expected to throw since a login item with the
// service name and account name usually won't be found.
let encKey = testingPassphrase;
try {
encKey = lazy.gKeychainUtils.getGenericPassword(serviceName, accountName);
} catch (ex) {
if (!testingPassphrase) {
throw ex;
}
}
this.ALGORITHM = "AES-CBC";
this._keyPromise = crypto.subtle
.importKey("raw", gTextEncoder.encode(encKey), "PBKDF2", false, [
"deriveKey",
])
.then(key => {
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: gTextEncoder.encode(SALT),
iterations: ITERATIONS,
hash: "SHA-1",
},
key,
{ name: this.ALGORITHM, length: DERIVED_KEY_SIZE_BITS },
false,
["decrypt", "encrypt"]
);
})
.catch(console.error);
}
/**
* Convert an array containing only two bytes unsigned numbers to a string.
*
* @param {number[]} arr - the array that needs to be converted.
* @returns {string} the string representation of the array.
*/
arrayToString(arr) {
let str = "";
for (let i = 0; i < arr.length; i++) {
str += String.fromCharCode(arr[i]);
}
return str;
}
stringToArray(binary_string) {
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes;
}
/**
* @param {Array} ciphertextArray ciphertext prefixed by the encryption version
* (see ENCRYPTION_VERSION_PREFIX).
* @returns {string} plaintext password
*/
async decryptData(ciphertextArray) {
let ciphertext = this.arrayToString(ciphertextArray);
if (!ciphertext.startsWith(ENCRYPTION_VERSION_PREFIX)) {
throw new Error("Unknown encryption version");
}
let key = await this._keyPromise;
if (!key) {
throw new Error("Cannot decrypt without a key");
}
let plaintext = await crypto.subtle.decrypt(
{ name: this.ALGORITHM, iv: IV },
key,
this.stringToArray(ciphertext.substring(ENCRYPTION_VERSION_PREFIX.length))
);
return gTextDecoder.decode(plaintext);
}
/**
* @param {USVString} plaintext to encrypt
* @returns {string} encrypted string consisting of UTF-16 code units prefixed
* by the ENCRYPTION_VERSION_PREFIX.
*/
async encryptData(plaintext) {
let key = await this._keyPromise;
if (!key) {
throw new Error("Cannot encrypt without a key");
}
let ciphertext = await crypto.subtle.encrypt(
{ name: this.ALGORITHM, iv: IV },
key,
gTextEncoder.encode(plaintext)
);
return (
ENCRYPTION_VERSION_PREFIX +
String.fromCharCode(...new Uint8Array(ciphertext))
);
}
}