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
* 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();
},
};