summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/OSCrypto_win.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/passwordmgr/OSCrypto_win.sys.mjs')
-rw-r--r--toolkit/components/passwordmgr/OSCrypto_win.sys.mjs284
1 files changed, 284 insertions, 0 deletions
diff --git a/toolkit/components/passwordmgr/OSCrypto_win.sys.mjs b/toolkit/components/passwordmgr/OSCrypto_win.sys.mjs
new file mode 100644
index 0000000000..5b3f1d7929
--- /dev/null
+++ b/toolkit/components/passwordmgr/OSCrypto_win.sys.mjs
@@ -0,0 +1,284 @@
+/* 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 { ctypes } from "resource://gre/modules/ctypes.sys.mjs";
+
+const FLAGS_NOT_SET = 0;
+
+const wintypes = {
+ BOOL: ctypes.bool,
+ BYTE: ctypes.uint8_t,
+ DWORD: ctypes.uint32_t,
+ PBYTE: ctypes.unsigned_char.ptr,
+ PCHAR: ctypes.char.ptr,
+ PDWORD: ctypes.uint32_t.ptr,
+ PVOID: ctypes.voidptr_t,
+ WORD: ctypes.uint16_t,
+};
+
+export function OSCrypto() {
+ this._structs = {};
+ this._functions = new Map();
+ this._libs = new Map();
+ this._structs.DATA_BLOB = new ctypes.StructType("DATA_BLOB", [
+ { cbData: wintypes.DWORD },
+ { pbData: wintypes.PVOID },
+ ]);
+
+ try {
+ this._libs.set("crypt32", ctypes.open("Crypt32"));
+ this._libs.set("kernel32", ctypes.open("Kernel32"));
+
+ this._functions.set(
+ "CryptProtectData",
+ this._libs
+ .get("crypt32")
+ .declare(
+ "CryptProtectData",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr
+ )
+ );
+ this._functions.set(
+ "CryptUnprotectData",
+ this._libs
+ .get("crypt32")
+ .declare(
+ "CryptUnprotectData",
+ ctypes.winapi_abi,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.PVOID,
+ wintypes.DWORD,
+ this._structs.DATA_BLOB.ptr
+ )
+ );
+ this._functions.set(
+ "LocalFree",
+ this._libs
+ .get("kernel32")
+ .declare("LocalFree", ctypes.winapi_abi, wintypes.DWORD, wintypes.PVOID)
+ );
+ } catch (ex) {
+ console.error(ex);
+ this.finalize();
+ throw ex;
+ }
+}
+
+OSCrypto.prototype = {
+ /**
+ * 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;
+ },
+
+ /**
+ * Convert a string to an array.
+ * @param {string} str - the string that needs to be converted.
+ * @returns {number[]} the array representation of the string.
+ */
+ stringToArray(str) {
+ let arr = [];
+ for (let i = 0; i < str.length; i++) {
+ arr.push(str.charCodeAt(i));
+ }
+ return arr;
+ },
+
+ /**
+ * Calculate the hash value used by IE as the name of the registry value where login details are
+ * stored.
+ * @param {string} data - the string value that needs to be hashed.
+ * @returns {string} the hash value of the string.
+ */
+ getIELoginHash(data) {
+ // return the two-digit hexadecimal code for a byte
+ function toHexString(charCode) {
+ return ("00" + charCode.toString(16)).slice(-2);
+ }
+
+ // the data needs to be encoded in null terminated UTF-16
+ data += "\0";
+
+ // dataArray is an array of bytes
+ let dataArray = new Array(data.length * 2);
+ for (let i = 0; i < data.length; i++) {
+ let c = data.charCodeAt(i);
+ let lo = c & 0xff;
+ let hi = (c & 0xff00) >> 8;
+ dataArray[i * 2] = lo;
+ dataArray[i * 2 + 1] = hi;
+ }
+
+ // calculation of SHA1 hash value
+ let cryptoHash = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ cryptoHash.init(cryptoHash.SHA1);
+ cryptoHash.update(dataArray, dataArray.length);
+ let hash = cryptoHash.finish(false);
+
+ let tail = 0; // variable to calculate value for the last 2 bytes
+ // convert to a character string in hexadecimal notation
+ for (let c of hash) {
+ tail += c.charCodeAt(0);
+ }
+ hash += String.fromCharCode(tail % 256);
+
+ // convert the binary hash data to a hex string.
+ let hashStr = Array.from(hash, (c, i) =>
+ toHexString(hash.charCodeAt(i))
+ ).join("");
+ return hashStr.toUpperCase();
+ },
+
+ _getEntropyParam(entropy) {
+ if (!entropy) {
+ return null;
+ }
+ let entropyArray = this.stringToArray(entropy);
+ entropyArray.push(0); // Null-terminate the data.
+ let entropyData = wintypes.WORD.array(entropyArray.length)(entropyArray);
+ let optionalEntropy = new this._structs.DATA_BLOB(
+ entropyData.length * wintypes.WORD.size,
+ entropyData
+ );
+ return optionalEntropy.address();
+ },
+
+ _convertWinArrayToJSArray(dataBlob) {
+ // Convert DATA_BLOB to JS accessible array
+ return ctypes.cast(
+ dataBlob.pbData,
+ wintypes.BYTE.array(dataBlob.cbData).ptr
+ ).contents;
+ },
+
+ /**
+ * Decrypt a string using the windows CryptUnprotectData API.
+ * @param {string} data - the encrypted string that needs to be decrypted.
+ * @param {?string} entropy - the entropy value of the decryption (could be null). Its value must
+ * be the same as the one used when the data was encrypted.
+ * @param {?string} output - how to return the decrypted data default string
+ * @returns {string|Uint8Array} the decryption of the string.
+ */
+ decryptData(data, entropy = null, output = "string") {
+ let array = this.stringToArray(data);
+ let encryptedData = wintypes.BYTE.array(array.length)(array);
+ let inData = new this._structs.DATA_BLOB(
+ encryptedData.length,
+ encryptedData
+ );
+ let outData = new this._structs.DATA_BLOB();
+ let entropyParam = this._getEntropyParam(entropy);
+
+ try {
+ let status = this._functions.get("CryptUnprotectData")(
+ inData.address(),
+ null,
+ entropyParam,
+ null,
+ null,
+ FLAGS_NOT_SET,
+ outData.address()
+ );
+ if (status === 0) {
+ throw new Error("decryptData failed: " + ctypes.winLastError);
+ }
+
+ let decrypted = this._convertWinArrayToJSArray(outData);
+
+ // Return raw bytes to handle non-string results
+ const bytes = new Uint8Array(decrypted);
+ if (output === "bytes") {
+ return bytes;
+ }
+
+ // Output that may include characters outside of the 0-255 (byte) range needs to use TextDecoder.
+ return new TextDecoder().decode(bytes);
+ } finally {
+ if (outData.pbData) {
+ this._functions.get("LocalFree")(outData.pbData);
+ }
+ }
+ },
+
+ /**
+ * Encrypt a string using the windows CryptProtectData API.
+ * @param {string} data - the string that is going to be encrypted.
+ * @param {?string} entropy - the entropy value of the encryption (could be null). Its value must
+ * be the same as the one that is going to be used for the decryption.
+ * @returns {string} the encrypted string.
+ */
+ encryptData(data, entropy = null) {
+ // Input that may include characters outside of the 0-255 (byte) range needs to use TextEncoder.
+ let decryptedByteData = [...new TextEncoder().encode(data)];
+ let decryptedData = wintypes.BYTE.array(decryptedByteData.length)(
+ decryptedByteData
+ );
+
+ let inData = new this._structs.DATA_BLOB(
+ decryptedData.length,
+ decryptedData
+ );
+ let outData = new this._structs.DATA_BLOB();
+ let entropyParam = this._getEntropyParam(entropy);
+
+ try {
+ let status = this._functions.get("CryptProtectData")(
+ inData.address(),
+ null,
+ entropyParam,
+ null,
+ null,
+ FLAGS_NOT_SET,
+ outData.address()
+ );
+ if (status === 0) {
+ throw new Error("encryptData failed: " + ctypes.winLastError);
+ }
+
+ let encrypted = this._convertWinArrayToJSArray(outData);
+ return this.arrayToString(encrypted);
+ } finally {
+ if (outData.pbData) {
+ this._functions.get("LocalFree")(outData.pbData);
+ }
+ }
+ },
+
+ /**
+ * Must be invoked once after last use of any of the provided helpers.
+ */
+ finalize() {
+ this._structs = {};
+ this._functions.clear();
+ for (let lib of this._libs.values()) {
+ try {
+ lib.close();
+ } catch (ex) {
+ console.error(ex);
+ }
+ }
+ this._libs.clear();
+ },
+};