diff options
Diffstat (limited to 'toolkit/components/passwordmgr/OSCrypto_win.sys.mjs')
-rw-r--r-- | toolkit/components/passwordmgr/OSCrypto_win.sys.mjs | 284 |
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(); + }, +}; |