Files
minisrv/hacktv_updsrv/wtvsec.js
2021-07-15 01:04:50 -04:00

265 lines
9.7 KiB
JavaScript

const CryptoJS = require('crypto-js');
const endianness = require('endianness');
class WTVNetworkSecurity {
initial_shared_key = null;
current_shared_key = null;
challenge_key = null;
challenge_response = null;
ticket_b64 = null;
incarnation = 1;
session_key1 = null;
session_key2 = null;
hRC4_Key1 = null;
hRC4_Key2 = null;
zdebug = true;
constructor(wtv_initial_key = CryptoJS.lib.WordArray.random(8), wtv_incarnation = 1) {
var initial_key = wtv_initial_key;
this.zdebug = true;
if (initial_key.sigBytes === 8) {
this.incarnation = wtv_incarnation;
this.initial_shared_key = initial_key;
this.current_shared_key = initial_key;
} else {
throw ("Invalid initial key length");
}
}
set_incarnation(wtv_incarnation) {
this.incarnation = wtv_incarnation;
this.SecureOn();
}
increment_incarnation() {
this.incarnation = this.incarnation + 1;
}
PrepareTicket() {
// store last challenge response in ticket
var ticket_data = this.challenge_response;
try {
var ticket_data_enc = CryptoJS.DES.encrypt(ticket_data, this.current_shared_key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding
});
this.ticket_b64 = this.current_shared_key.concat(ticket_data_enc.ciphertext).toString(CryptoJS.enc.Base64);
} catch (e) {
console.log("Error encrypting ticket: " + e.toString());
return null;
}
return this.ticket_b64;
}
DecodeTicket(ticket_b64) {
var ticket_hex = CryptoJS.enc.Base64.parse(ticket_b64);
var ticket_key = CryptoJS.enc.Hex.parse(ticket_hex.substring(0, this.current_shared_key.sigBytes));
var ticket_dec = CryptoJS.DES.decrypt(
{
ciphertext: challenge_enc
},
ticket_key,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.ZeroPadding
}
);
this.ProcessChallenge(ticket_dec);
}
ProcessChallenge(wtv_challenge) {
var challenge_raw = CryptoJS.enc.Base64.parse(wtv_challenge);
if (challenge_raw.sigBytes > 8) {
var challenge_raw_hex = challenge_raw.toString(CryptoJS.enc.Hex);
var challenge_enc_hex = challenge_raw_hex.substring((8*2));
var challenge_enc = CryptoJS.enc.Hex.parse(challenge_enc_hex);
var challenge_decrypted = CryptoJS.DES.decrypt(
{
ciphertext: challenge_enc
},
this.current_shared_key,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.ZeroPadding
}
);
var challenge_dec_hex = challenge_decrypted.toString(CryptoJS.enc.Hex);
var challenge_md5_challenge = CryptoJS.MD5(CryptoJS.enc.Hex.parse(challenge_dec_hex.substring(0, (80 * 2))));
if (challenge_dec_hex.substring((80 * 2), (96 * 2)) == challenge_md5_challenge.toString(CryptoJS.enc.Hex)) {
this.current_shared_key = CryptoJS.enc.Hex.parse(challenge_dec_hex.substring((72*2), (80*2)));
var challenge_echo = CryptoJS.enc.Hex.parse(challenge_dec_hex.substr(0, (40*2)));
// RC4 encryption keys.Stored in the wtv-ticket on the server side.
this.session_key1 = CryptoJS.enc.Hex.parse(challenge_dec_hex.substring((40*2), (56*2)));
this.session_key2 = CryptoJS.enc.Hex.parse(challenge_dec_hex.substring((56*2), (72*2)));
var echo_encrypted = CryptoJS.DES.encrypt(CryptoJS.MD5(challenge_echo).concat(challenge_echo), this.current_shared_key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding
});
// Last bytes is just extra padding
var challenge_response = CryptoJS.enc.Hex.parse(challenge_raw_hex.substr(0, (8 * 2))).concat(echo_encrypted.ciphertext.concat(CryptoJS.enc.Utf8.parse("\x00".repeat(8))));
return challenge_response;
} else {
throw ("Couldn't solve challenge");
return "";
}
} else {
throw ("Invalid challenge length");
}
}
IssueChallenge() {
/*
* bytes 0-8: Random id? Just echoed in the response
* bytes 8 - XX: DES encrypted block.Encrypted with the initial key or subsequent keys from the challenge.
* bytes 8 - 48: hidden random data we echo back in the response
* bytes 48 - 64: session key 1 used in RC4 encryption triggered by SECURE ON
* bytes 64 - 80: session key 2 used in RC4 encryption triggered by SECURE ON
* bytes 80 - 88: new key for future challenges
* bytes 88 - 104: MD5 of 8 - 88
* bytes 104 - 112: padding.not important
*/
var random_id_question_mark = CryptoJS.lib.WordArray.random(8);
var echo_me = CryptoJS.lib.WordArray.random(40);
this.session_key1 = CryptoJS.lib.WordArray.random(16);
this.session_key2 = CryptoJS.lib.WordArray.random(16);
var new_shared_key = CryptoJS.lib.WordArray.random(8);
var challenge_puzzle = echo_me.concat(this.session_key1.concat(this.session_key2.concat(new_shared_key)));
var challenge_secret = challenge_puzzle.concat(CryptoJS.MD5(challenge_puzzle).concat(CryptoJS.enc.Hex.parse("\x00".repeat(8))));
// Shhhh!!
var challenge_secreted = CryptoJS.DES.encrypt(challenge_secret, this.current_shared_key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding
});
var challenge = random_id_question_mark.concat(challenge_secreted.ciphertext);
var challenge_b64 = challenge.toString(CryptoJS.enc.Base64);
// get the expected response for when client sends it
this.challenge_response = this.ProcessChallenge(challenge_b64);
this.challenge_key = this.current_shared_key;
this.current_shared_key = new_shared_key;
return challenge_b64;
}
wordToByteArray(word, length) {
var ba = [],
i,
xFF = 0xFF;
if (length > 0)
ba.push(word >>> 24);
if (length > 1)
ba.push((word >>> 16) & xFF);
if (length > 2)
ba.push((word >>> 8) & xFF);
if (length > 3)
ba.push(word & xFF);
return ba;
}
wordArrayToUint8Array(wordArray, length = 0) {
if (wordArray.hasOwnProperty("sigBytes") && wordArray.hasOwnProperty("words")) {
length = wordArray.sigBytes;
wordArray = wordArray.words;
}
var result = [],
bytes,
i = 0;
while (length > 0) {
bytes = this.wordToByteArray(wordArray[i], Math.min(4, length));
length -= bytes.length;
result.push(bytes);
i++;
}
return new Uint8Array([].concat.apply([], result));
}
SecureOn() {
var buf = new Uint8Array([0xff & this.incarnation, 0xff & (this.incarnation >> 8), 0xff & (this.incarnation >> 16), 0xff & (this.incarnation >> 24)]);
endianness(buf, 2);
var md5_digest_key1 = CryptoJS.MD5(this.session_key1.concat(CryptoJS.lib.WordArray.create(buf).concat(this.session_key1)));
var next_incarnation = this.incarnation + 1;
buf = new Uint8Array([0xff & next_incarnation, 0xff & (next_incarnation >> 8), 0xff & (next_incarnation >> 16), 0xff & (next_incarnation >> 24)]);
endianness(buf, 2);
var md5_digest_key2 = CryptoJS.MD5(this.session_key2.concat(CryptoJS.lib.WordArray.create(buf).concat(this.session_key2)));
this.hRC4_Key1 = md5_digest_key1;
this.hRC4_Key2 = md5_digest_key2;
}
EncryptKey1(data) {
return Buffer.from(this.wordArrayToUint8Array(this.Encrypt(this.hRC4_Key1, CryptoJS.lib.WordArray.create(data)).ciphertext));
}
EncryptKey2(data) {
return Buffer.from(this.wordArrayToUint8Array(this.Encrypt(this.hRC4_Key2, CryptoJS.lib.WordArray.create(data)).ciphertext));
}
Encrypt(key, data) {
try {
if (key != null) {
return CryptoJS.RC4.encrypt(data, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding
});
}
} catch (e) {
throw ("Invalid RC4 encryption key: " + e.toString());
}
}
DecryptKey1(data) {
return Buffer.from(this.wordArrayToUint8Array(this.Decrypt(this.hRC4_Key1, data)));
}
DecryptKey2(data) {
return Buffer.from(this.wordArrayToUint8Array(this.Decrypt(this.hRC4_Key2, data)));
}
Decrypt(key, data) {
try {
return CryptoJS.RC4.decrypt(data, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.NoPadding
});
} catch (e) {
throw ("Invalid RC4 encryption key: " + e.toString());
}
}
Test() {
console.log("TEST RUN");
console.log("Test python challenge");
this.current_shared_key = CryptoJS.enc.Base64.parse("CC5rWmRUE0o=");
var current_challenge = "0kjyqIYAu0ziFBbSERN6DGaZ6S0fT+DBUCtpHCJ4lpuM7CbXdAm+x83BIDoJYztd1Z+5KFZ7ghmb3LJCT/6mhWUYkqqKOyfPRW8ZIdbICK/CV+Kxm8EUjRXZSk/97tsmFpH3hcCJ7C2TBw+TX38uQQ==";
var expected_result = "0kjyqIYAu0zI5QrLhSuEUFgKkoVSxI3zBlUMfhnIYoMy0ExfIX4s/mHvILseDFx+17trk7YO+xG9D2qSY6v9XVUS1OP1m8ee";
console.log("Expected: " + expected_result);
console.log("Got: " + this.ProcessChallenge(current_challenge));
}
}
module.exports = WTVNetworkSecurity;