Reworked the identity system and changed some stuff

canary
WolverinDEV 2019-01-26 17:10:15 +01:00
parent c5e7029c81
commit cd74e94925
23 changed files with 2047 additions and 169 deletions

View File

@ -1,4 +1,8 @@
# Changelog:
* **26.01.19**
- Improved TeaSpeak identities (now generates automatic and are saveable)
- Fixed `connect_profile` parameter within URL
* **20.01.19**
- Added the possibility to change the remote volume of a bot
- Added a playlist management system

BIN
TeaWeb.zip Normal file

Binary file not shown.

View File

@ -21,7 +21,7 @@
],
[ /* shared generated worker codec */
"type" => "js",
"search-pattern" => "/WorkerCodec.js$/",
"search-pattern" => "/(WorkerCodec.js|WorkerPOW.js)$/",
"build-target" => "dev|rel",
"path" => "js/workers/",
@ -75,6 +75,14 @@
"path" => "wasm/",
"local-path" => "./asm/generated/"
],
[ /* own webassembly files */
"type" => "wasm",
"search-pattern" => "/.*\.(wasm)/",
"build-target" => "dev|rel",
"path" => "wat/",
"local-path" => "./shared/wat/"
],
[ /* translations */
"type" => "i18n",
"search-pattern" => "/.*\.(translation|json)/",

View File

@ -9,7 +9,8 @@
"build-worker": "tsc -p shared/js/workers/tsconfig_worker_codec.json",
"dtsgen": "node tools/dtsgen/index.js",
"trgen": "node tools/trgen/index.js",
"ttsc": "ttsc"
"ttsc": "ttsc",
"rebuild-structure-web-dev": "php files.php generate web dev"
},
"author": "TeaSpeak (WolverinDEV)",
"license": "ISC",
@ -26,7 +27,8 @@
"sass": "^1.14.1",
"sha256": "^0.2.0",
"ttypescript": "^1.5.5",
"typescript": "^3.1.1"
"typescript": "^3.1.1",
"wat2wasm": "^1.0.2"
},
"repository": {
"type": "git",

View File

@ -295,6 +295,7 @@ interface HandshakeIdentityHandler {
class HandshakeHandler {
private connection: ServerConnection;
private handshake_handler: HandshakeIdentityHandler;
private failed = false;
readonly profile: profiles.ConnectionProfile;
readonly name: string;
@ -328,6 +329,9 @@ class HandshakeHandler {
}
private handshake_failed(message: string) {
if(this.failed) return;
this.failed = true;
this.connection._client.handleDisconnect(DisconnectReason.HANDSHAKE_FAILED, message);
}

547
shared/js/crypto/asn1.ts Normal file
View File

@ -0,0 +1,547 @@
// ASN.1 JavaScript decoder
// Copyright (c) 2008-2018 Lapo Luchini <lapo@lapo.it>
// Copyright (c) 2019-2019 Markus Hadenfeldt <git@teaspeak.de>
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
namespace asn1 {
declare class Int10 {
constructor(value?: any);
sub(sub: number);
mulAdd(mul: number, add: number);
simplify();
}
const ellipsis = "\u2026";
function string_cut(str, len) {
if (str.length > len)
str = str.substring(0, len) + ellipsis;
return str;
}
export class Stream {
private static HEX_DIGITS = "0123456789ABCDEF";
private static reTimeS = /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/;
private static reTimeL = /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/;
position: number;
data: string | ArrayBuffer;
constructor(data: string | Stream | ArrayBuffer, position: number) {
if (data instanceof Stream)
this.data = data.data;
else
this.data = data;
this.position = position;
}
length() : number {
if (this.data instanceof ArrayBuffer)
return this.data.byteLength;
return this.data.length;
}
get(position?: number) {
if (position === undefined)
position = this.position++;
if (position >= this.length())
throw 'Requesting byte offset ' + this.position + ' on a stream of length ' + this.length();
return (typeof(this.data) === "string") ? this.data.charCodeAt(position) : this.data[position];
}
hexByte(byte: number) {
return Stream.HEX_DIGITS.charAt((byte >> 4) & 0xF) + Stream.HEX_DIGITS.charAt(byte & 0xF);
}
parseStringISO(start, end) {
let s = "";
for (let i = start; i < end; ++i)
s += String.fromCharCode(this.get(i));
return s;
}
parseStringUTF(start, end) {
let s = "";
for (let i = start; i < end;) {
let c = this.get(i++);
if (c < 128)
s += String.fromCharCode(c);
else if ((c > 191) && (c < 224))
s += String.fromCharCode(((c & 0x1F) << 6) | (this.get(i++) & 0x3F));
else
s += String.fromCharCode(((c & 0x0F) << 12) | ((this.get(i++) & 0x3F) << 6) | (this.get(i++) & 0x3F));
}
return s;
}
parseStringBMP(start, end) {
let str = "", hi, lo;
for (let i = start; i < end;) {
hi = this.get(i++);
lo = this.get(i++);
str += String.fromCharCode((hi << 8) | lo);
}
return str;
}
parseTime(start, end, shortYear) {
let s = this.parseStringISO(start, end),
m = (shortYear ? Stream.reTimeS : Stream.reTimeL).exec(s);
if (!m)
return "Unrecognized time: " + s;
if (shortYear) {
// to avoid querying the timer, use the fixed range [1970, 2069]
// it will conform with ITU X.400 [-10, +40] sliding window until 2030
//m[1] = +m[1];
//m[1] += (parseInt(m[1]) < 70) ? 2000 : 1900;
throw "fixme!";
}
s = m[1] + "-" + m[2] + "-" + m[3] + " " + m[4];
if (m[5]) {
s += ":" + m[5];
if (m[6]) {
s += ":" + m[6];
if (m[7])
s += "." + m[7];
}
}
if (m[8]) {
s += " UTC";
if (m[8] != 'Z') {
s += m[8];
if (m[9])
s += ":" + m[9];
}
}
return s;
};
parseInteger(start, end) {
let current: number = this.get(start);
let negative = (current > 127);
let padding = negative ? 255 : 0;
let length;
let descriptor: number | string;
// skip unuseful bits (not allowed in DER)
while (current == padding && ++start < end)
current = this.get(start);
length = end - start;
if (length === 0)
return negative ? '-1' : '0';
// show bit length of huge integers
if (length > 4) {
descriptor = current;
length <<= 3; /* calculate bit length */
while (((descriptor ^ padding) & 0x80) == 0) {
descriptor <<= 1;
--length;
}
descriptor = "(" + length + " bit)\n";
}
// decode the integer
if (negative) current = current - 256;
let number = "";
if(typeof(Int10) !== "undefined") {
let n = new Int10(current);
for (let i = start + 1; i < end; ++i)
n.mulAdd(256, this.get(i));
number = n.toString();
} else {
let n = 0;
for (let i = start + 1; i < end; ++i) {
n <<= 8;
n += this.get(i);
}
number = n.toString();
}
return descriptor + number;
};
isASCII(start: number, end: number) {
for (let i = start; i < end; ++i) {
const c = this.get(i);
if (c < 32 || c > 176)
return false;
}
return true;
};
parseBitString(start, end, maxLength) {
let unusedBit = this.get(start),
lenBit = ((end - start - 1) << 3) - unusedBit,
intro = "(" + lenBit + " bit)\n",
s = "";
for (let i = start + 1; i < end; ++i) {
let b = this.get(i),
skip = (i == end - 1) ? unusedBit : 0;
for (let j = 7; j >= skip; --j)
s += (b >> j) & 1 ? "1" : "0";
if (s.length > maxLength)
return intro + string_cut(s, maxLength);
}
return intro + s;
};
parseOctetString(start, end, maxLength) {
if (this.isASCII(start, end))
return string_cut(this.parseStringISO(start, end), maxLength);
let len = end - start,
s = "(" + len + " byte)\n";
maxLength /= 2; // we work in bytes
if (len > maxLength)
end = start + maxLength;
for (let i = start; i < end; ++i)
s += this.hexByte(this.get(i));
if (len > maxLength)
s += ellipsis;
return s;
};
parseOID(start, end, maxLength) {
let s = '',
n = new Int10(),
bits = 0;
for (let i = start; i < end; ++i) {
let v = this.get(i);
n.mulAdd(128, v & 0x7F);
bits += 7;
if (!(v & 0x80)) { // finished
if (s === '') {
n = n.simplify();
if (n instanceof Int10) {
n.sub(80);
s = "2." + n.toString();
} else {
let m = n < 80 ? n < 40 ? 0 : 1 : 2;
s = m + "." + (n - m * 40);
}
} else
s += "." + n.toString();
if (s.length > maxLength)
return string_cut(s, maxLength);
n = new Int10();
bits = 0;
}
}
if (bits > 0)
s += ".incomplete";
/* FIXME
if (typeof oids === 'object') {
let oid = oids[s];
if (oid) {
if (oid.d) s += "\n" + oid.d;
if (oid.c) s += "\n" + oid.c;
if (oid.w) s += "\n(warning!)";
}
}
*/
return s;
};
}
export enum TagClass {
UNIVERSAL = 0x00,
APPLICATION = 0x01,
CONTEXT = 0x02,
PRIVATE = 0x03
}
export enum TagType {
EOC = 0x00,
BOOLEAN = 0x01,
INTEGER = 0x02,
BIT_STRING = 0x03,
OCTET_STRING = 0x04,
NULL = 0x05,
OBJECT_IDENTIFIER = 0x06,
ObjectDescriptor = 0x07,
EXTERNAL = 0x08,
REAL = 0x09,
ENUMERATED = 0x0A,
EMBEDDED_PDV = 0x0B,
UTF8String = 0x0C,
SEQUENCE = 0x10,
SET = 0x11,
NumericString = 0x12,
PrintableString = 0x13, // ASCII subset
TeletextString = 0x14, // aka T61String
VideotexString = 0x15,
IA5String = 0x16, // ASCII
UTCTime = 0x17,
GeneralizedTime = 0x18,
GraphicString = 0x19,
VisibleString = 0x1A, // ASCII subset
GeneralString = 0x1B,
UniversalString = 0x1C,
BMPString = 0x1E
}
class ASN1Tag {
tagClass: TagClass;
type: TagType;
tagConstructed: boolean;
tagNumber: number;
constructor(stream: Stream) {
let buf = stream.get();
this.tagClass = buf >> 6;
this.tagConstructed = ((buf & 0x20) !== 0);
this.tagNumber = buf & 0x1F;
if (this.tagNumber == 0x1F) { // long tag
let n = new Int10();
do {
buf = stream.get();
n.mulAdd(128, buf & 0x7F);
} while (buf & 0x80);
this.tagNumber = n.simplify();
}
}
isUniversal() {
return this.tagClass === 0x00;
};
isEOC() {
return this.tagClass === 0x00 && this.tagNumber === 0x00;
};
}
export class ASN1 {
stream: Stream;
header: number;
length: number;
tag: ASN1Tag;
children: ASN1[];
constructor(stream: Stream, header: number, length: number, tag: ASN1Tag, children: ASN1[]) {
this.stream = stream;
this.header = header;
this.length = length;
this.tag = tag;
this.children = children;
}
content(max_length?: number, type?: TagType) { // a preview of the content (intended for humans)
if (this.tag === undefined) return null;
if (max_length === undefined)
max_length = Infinity;
let content = this.posContent(),
len = Math.abs(this.length);
if (!this.tag.isUniversal()) {
if (this.children !== null)
return "(" + this.children.length + " elem)";
return this.stream.parseOctetString(content, content + len, max_length);
}
switch (type || this.tag.tagNumber) {
case 0x01: // BOOLEAN
return (this.stream.get(content) === 0) ? "false" : "true";
case 0x02: // INTEGER
return this.stream.parseInteger(content, content + len);
case 0x03: // BIT_STRING
return this.children ? "(" + this.children.length + " elem)" :
this.stream.parseBitString(content, content + len, max_length);
case 0x04: // OCTET_STRING
return this.children ? "(" + this.children.length + " elem)" :
this.stream.parseOctetString(content, content + len, max_length);
//case 0x05: // NULL
case 0x06: // OBJECT_IDENTIFIER
return this.stream.parseOID(content, content + len, max_length);
//case 0x07: // ObjectDescriptor
//case 0x08: // EXTERNAL
//case 0x09: // REAL
//case 0x0A: // ENUMERATED
//case 0x0B: // EMBEDDED_PDV
case 0x10: // SEQUENCE
case 0x11: // SET
if (this.children !== null)
return "(" + this.children.length + " elem)";
else
return "(no elem)";
case 0x0C: // UTF8String
return string_cut(this.stream.parseStringUTF(content, content + len), max_length);
case 0x12: // NumericString
case 0x13: // PrintableString
case 0x14: // TeletexString
case 0x15: // VideotexString
case 0x16: // IA5String
//case 0x19: // GraphicString
case 0x1A: // VisibleString
//case 0x1B: // GeneralString
//case 0x1C: // UniversalString
return string_cut(this.stream.parseStringISO(content, content + len), max_length);
case 0x1E: // BMPString
return string_cut(this.stream.parseStringBMP(content, content + len), max_length);
case 0x17: // UTCTime
case 0x18: // GeneralizedTime
return this.stream.parseTime(content, content + len, (this.tag.tagNumber == 0x17));
}
return null;
};
typeName(): string {
switch (this.tag.tagClass) {
case 0: // universal
return TagType[this.tag.tagNumber] || ("Universal_" + this.tag.tagNumber.toString());
case 1:
return "Application_" + this.tag.tagNumber.toString();
case 2:
return "[" + this.tag.tagNumber.toString() + "]"; // Context
case 3:
return "Private_" + this.tag.tagNumber.toString();
}
};
toString() {
return this.typeName() + "@" + this.stream.position + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.children === null) ? 'null' : this.children.length) + "]";
}
toPrettyString(indent) {
if (indent === undefined) indent = '';
let s = indent + this.typeName() + " @" + this.stream.position;
if (this.length >= 0)
s += "+";
s += this.length;
if (this.tag.tagConstructed)
s += " (constructed)";
else if ((this.tag.isUniversal() && ((this.tag.tagNumber == 0x03) || (this.tag.tagNumber == 0x04))) && (this.children !== null))
s += " (encapsulates)";
let content = this.content();
if (content)
s += ": " + content.replace(/\n/g, '|');
s += "\n";
if (this.children !== null) {
indent += ' ';
for (let i = 0, max = this.children.length; i < max; ++i)
s += this.children[i].toPrettyString(indent);
}
return s;
};
posStart() {
return this.stream.position;
};
posContent() {
return this.stream.position + this.header;
};
posEnd() {
return this.stream.position + this.header + Math.abs(this.length);
};
static decodeLength(stream: Stream) {
let buf = stream.get();
const len = buf & 0x7F;
if (len == buf)
return len;
if (len > 6) // no reason to use Int10, as it would be a huge buffer anyways
throw "Length over 48 bits not supported at position " + (stream.position - 1);
if (len === 0)
return null; // undefined
buf = 0;
for (let i = 0; i < len; ++i)
buf = (buf << 8) + stream.get();
return buf;
};
static encodeLength(buffer: Uint8Array, offset: number, length: number) {
if(length < 0x7F) {
buffer[offset] = length;
} else {
buffer[offset] = 0x80;
let index = 1;
while(length > 0) {
buffer[offset + index++] = length & 0xFF;
length >>= 8;
buffer[offset] += 1;
}
}
}
}
function decode0(stream: Stream) {
const streamStart = new Stream(stream, 0); /* copy */
const tag = new ASN1Tag(stream);
let len = ASN1.decodeLength(stream);
const start = stream.position;
const length_header = start - streamStart.position;
let children = null;
const query_children = () => {
children = [];
if (len !== null) {
const end = start + len;
if (end > stream.length())
throw 'Container at offset ' + start + ' has a length of ' + len + ', which is past the end of the stream';
while (stream.position < end)
children[children.length] = decode0(stream);
if (stream.position != end)
throw 'Content size is not correct for container at offset ' + start;
} else {
// undefined length
try {
while (true) {
const s = decode0(stream);
if (s.tag.isEOC()) break;
children[children.length] = s;
}
len = start - stream.position; // undefined lengths are represented as negative values
} catch (e) {
throw 'Exception while decoding undefined length content at offset ' + start + ': ' + e;
}
}
};
if (tag.tagConstructed) {
// must have valid content
query_children();
} else if (tag.isUniversal() && ((tag.tagNumber == 0x03) || (tag.tagNumber == 0x04))) {
// sometimes BitString and OctetString are used to encapsulate ASN.1
try {
if (tag.tagNumber == 0x03)
if (stream.get() != 0)
throw "BIT STRINGs with unused bits cannot encapsulate.";
query_children();
for (let i = 0; i < children.length; ++i)
if (children[i].tag.isEOC())
throw 'EOC is not supposed to be actual content.';
} catch (e) {
// but silently ignore when they don't
children = null;
//DEBUG console.log('Could not decode structure at ' + start + ':', e);
}
}
if (children === null) {
if (len === null)
throw "We can't skip over an invalid tag with undefined length at offset " + start;
stream.position = start + Math.abs(len);
}
return new ASN1(streamStart, length_header, len, tag, children);
}
export function decode(stream: string | ArrayBuffer) {
return decode0(new Stream(stream, 0));
}
}

View File

@ -392,6 +392,8 @@ namespace sha {
return result.buffer;
}
export function sha1(message: string | ArrayBuffer) : PromiseLike<ArrayBuffer> {
if(!(typeof(message) === "string" || message instanceof ArrayBuffer)) throw "Invalid type!";
let buffer = message instanceof ArrayBuffer ? message : encode_text(message as string);
if(!crypto || !crypto.subtle || !crypto.subtle.digest || /Edge/.test(navigator.userAgent))

View File

@ -312,6 +312,7 @@ const loader_javascript = {
"js/crypto/sha.js",
"js/crypto/hex.js",
"js/crypto/asn1.js",
//load the profiles
"js/profiles/ConnectionProfile.js",

View File

@ -74,12 +74,12 @@ function setup_jsrender() : boolean {
return (Math.round(Math.random() * (min + max + 1) - min)).toString();
});
js_render.views.tags("fmt_date", (...arguments) => {
return moment(arguments[0]).format(arguments[1]);
js_render.views.tags("fmt_date", (...args) => {
return moment(args[0]).format(args[1]);
});
js_render.views.tags("tr", (...arguments) => {
return tr(arguments[0]);
js_render.views.tags("tr", (...args) => {
return tr(args[0]);
});
$(".jsrender-template").each((idx, _entry) => {
@ -129,11 +129,7 @@ async function initialize() {
}
AudioController.initializeAudioController();
if(!profiles.identities.setup_teamspeak()) {
console.error(tr("Could not setup the TeamSpeak identity parser!"));
return;
}
profiles.load();
await profiles.load();
try {
await ppt.initialize();
@ -144,6 +140,92 @@ async function initialize() {
}
}
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
function str2ab8(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
/* FIXME Dont use atob, because it sucks for non UTF-8 tings */
function arrayBufferBase64(base64: string) {
base64 = atob(base64);
const buf = new ArrayBuffer(base64.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = base64.length; i < strLen; i++) {
bufView[i] = base64.charCodeAt(i);
}
return buf;
}
function base64ArrayBuffer(arrayBuffer) {
var base64 = ''
var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
var bytes = new Uint8Array(arrayBuffer)
var byteLength = bytes.byteLength
var byteRemainder = byteLength % 3
var mainLength = byteLength - byteRemainder
var a, b, c, d
var chunk
// Main loop deals with bytes in chunks of 3
for (var i = 0; i < mainLength; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2]
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6
d = chunk & 63 // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d]
}
// Deal with the remaining bytes and padding
if (byteRemainder == 1) {
chunk = bytes[mainLength]
a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4 // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + '=='
} else if (byteRemainder == 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1]
a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2 // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + '='
}
return base64
}
function Base64EncodeUrl(str){
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '');
}
function Base64DecodeUrl(str: string, pad?: boolean){
if(typeof(pad) === 'undefined' || pad)
str = (str + '===').slice(0, str.length + (str.length % 4));
return str.replace(/-/g, '+').replace(/_/g, '/');
}
function main() {
//http://localhost:63343/Web-Client/index.php?_ijt=omcpmt8b9hnjlfguh8ajgrgolr&default_connect_url=true&default_connect_type=teamspeak&default_connect_url=localhost%3A9987&disableUnloadDialog=1&loader_ignore_age=1
@ -164,7 +246,9 @@ function main() {
if(settings.static("connect_default", false) && settings.static("connect_address", "")) {
const profile_uuid = settings.static("connect_profile") as string;
console.log("UUID: %s", profile_uuid);
const profile = profiles.find_profile(profile_uuid) || profiles.default_profile();
console.log("UUID: %s", profile.id);
const address = settings.static("connect_address", "");
const username = settings.static("connect_username", "Another TeaSpeak user");
@ -204,7 +288,7 @@ function main() {
setup_close();
let _resize_timeout: NodeJS.Timer;
let _resize_timeout: number;
$(window).on('resize', () => {
if(_resize_timeout)
clearTimeout(_resize_timeout);

View File

@ -70,7 +70,7 @@ namespace profiles {
}
}
function decode_profile(data) : ConnectionProfile | string {
async function decode_profile(data) : Promise<ConnectionProfile | string> {
data = JSON.parse(data);
if(data.version !== 1)
return "invalid version";
@ -87,7 +87,7 @@ namespace profiles {
const _data = data.identity_data[key];
if(type == undefined) continue;
const identity = identities.decode_identity(type, _data);
const identity = await identities.decode_identity(type, _data);
if(identity == undefined) continue;
result.identities[key.toLowerCase()] = identity;
@ -103,7 +103,7 @@ namespace profiles {
}
let available_profiles: ConnectionProfile[] = [];
export function load() {
export async function load() {
available_profiles = [];
const profiles_json = localStorage.getItem("profiles");
@ -117,7 +117,7 @@ namespace profiles {
}
if(profiles_data.version == 1) {
for(const profile_data of profiles_data.profiles) {
const profile = decode_profile(profile_data);
const profile = await decode_profile(profile_data);
if(typeof(profile) === 'string') {
console.error(tr("Failed to load profile. Reason: %s, Profile data: %s"), profile, profiles_data);
continue;

View File

@ -13,12 +13,12 @@ namespace profiles.identities {
valid() : boolean;
encode?() : string;
decode(data: string) : boolean;
decode(data: string) : Promise<void>;
spawn_identity_handshake_handler(connection: ServerConnection) : HandshakeIdentityHandler;
}
export function decode_identity(type: IdentitifyType, data: string) : Identity {
export async function decode_identity(type: IdentitifyType, data: string) : Promise<Identity> {
let identity: Identity;
switch (type) {
case IdentitifyType.NICKNAME:
@ -28,14 +28,19 @@ namespace profiles.identities {
identity = new TeaForumIdentity(undefined, undefined);
break;
case IdentitifyType.TEAMSPEAK:
identity = new TeamSpeakIdentity(undefined, undefined);
identity = new TeaSpeakIdentity(undefined, undefined);
break;
}
if(!identity)
return undefined;
if(!identity.decode(data))
try {
await identity.decode(data)
} catch(error) {
/* todo better error handling! */
console.error(error);
return undefined;
}
return identity;
}
@ -50,7 +55,7 @@ namespace profiles.identities {
identity = new TeaForumIdentity(undefined, undefined);
break;
case IdentitifyType.TEAMSPEAK:
identity = new TeamSpeakIdentity(undefined, undefined);
identity = new TeaSpeakIdentity(undefined, undefined);
break;
}
return identity;

View File

@ -51,13 +51,13 @@ namespace profiles.identities {
return this._name != undefined && this._name.length >= 3;
}
decode(data) {
decode(data) : Promise<void> {
data = JSON.parse(data);
if(data.version !== 1)
return false;
throw "invalid version";
this._name = data["name"];
return true;
return;
}
encode?() : string {

View File

@ -61,15 +61,15 @@ namespace profiles.identities {
uid() : string { return "TeaForo#" + this.identityData["user_id"]; }
type() : IdentitifyType { return IdentitifyType.TEAFORO; }
decode(data) {
decode(data) : Promise<void> {
data = JSON.parse(data);
if(data.version !== 1)
return false;
throw "invalid version";
this.identityDataJson = data["identity_data"];
this.identitySign = data["identity_sign"];
this.identityData = JSON.parse(this.identityData);
return true;
return;
}
encode?() : string {

View File

@ -1,86 +1,180 @@
/// <reference path="../Identity.ts" />
namespace profiles.identities {
export namespace TSIdentityHelper {
export let funcationParseIdentity: any;
export let funcationParseIdentityByFile: any;
export let funcationCalculateSecurityLevel: any;
export let functionUid: any;
export let funcationExportIdentity: any;
export let funcationPublicKey: any;
export let funcationSignMessage: any;
let functionLastError: any;
let functionClearLastError: any;
let functionDestroyString: any;
let functionDestroyIdentity: any;
export function setup() : boolean {
functionDestroyString = Module.cwrap("destroy_string", "pointer", []);
functionLastError = Module.cwrap("last_error_message", null, ["string"]);
funcationParseIdentity = Module.cwrap("parse_identity", "pointer", ["string"]);
funcationParseIdentityByFile = Module.cwrap("parse_identity_file", "pointer", ["string"]);
functionDestroyIdentity = Module.cwrap("delete_identity", null, ["pointer"]);
funcationCalculateSecurityLevel = Module.cwrap("identity_security_level", "pointer", ["pointer"]);
funcationExportIdentity = Module.cwrap("identity_export", "pointer", ["pointer"]);
funcationPublicKey = Module.cwrap("identity_key_public", "pointer", ["pointer"]);
funcationSignMessage = Module.cwrap("identity_sign", "pointer", ["pointer", "string", "number"]);
functionUid = Module.cwrap("identity_uid", "pointer", ["pointer"]);
return Module.cwrap("tomcrypt_initialize", "number", [])() == 0;
export namespace CryptoHelper {
export async function export_ecc_key(crypto_key: CryptoKey, public_key: boolean) {
/*
Tomcrypt public key export:
if (type == PK_PRIVATE) {
flags[0] = 1;
err = der_encode_sequence_multi(out, outlen,
LTC_ASN1_BIT_STRING, 1UL, flags,
LTC_ASN1_SHORT_INTEGER, 1UL, &key_size,
LTC_ASN1_INTEGER, 1UL, key->pubkey.x,
LTC_ASN1_INTEGER, 1UL, key->pubkey.y,
LTC_ASN1_INTEGER, 1UL, key->k,
LTC_ASN1_EOL, 0UL, NULL);
} else {
flags[0] = 0;
err = der_encode_sequence_multi(out, outlen,
LTC_ASN1_BIT_STRING, 1UL, flags,
LTC_ASN1_SHORT_INTEGER, 1UL, &key_size,
LTC_ASN1_INTEGER, 1UL, key->pubkey.x,
LTC_ASN1_INTEGER, 1UL, key->pubkey.y,
LTC_ASN1_EOL, 0UL, NULL);
}
export function last_error() : string {
return unwarpString(functionLastError());
*/
const key_data = await crypto.subtle.exportKey("jwk", crypto_key);
let index = 0;
const length = public_key ? 79 : 114; /* max lengths! Depends on the padding could be less */
const buffer = new Uint8Array(length); /* fixed ASN1 length */
{ /* the initial sequence */
buffer[index++] = 0x30; /* type */
buffer[index++] = 0x00; /* we will set the sequence length later */
}
{ /* the flags bit string */
buffer[index++] = 0x03; /* type */
buffer[index++] = 0x02; /* length */
buffer[index++] = 0x07; /* data */
buffer[index++] = public_key ? 0x00 : 0x80; /* flag 1 or 0 (1 = private key)*/
}
{ /* key size (const 32 for P-256) */
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x01; /* length */
buffer[index++] = 0x20;
}
{ /* Public kex X */
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
const raw = atob(Base64DecodeUrl(key_data.x, false));
if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
}
export function unwarpString(str) : string {
if(str == "") return "";
try {
if(!$.isFunction(window.Pointer_stringify)) {
displayCriticalError(tr("Missing required wasm function!<br>Please reload the page!"));
for(let i = 0; i < 32; i++)
buffer[index++] = raw.charCodeAt(i);
}
let message: string = window.Pointer_stringify(str);
functionDestroyString(str);
return message;
} catch (error) {
console.error(error);
return "";
{ /* Public kex Y */
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
const raw = atob(Base64DecodeUrl(key_data.y, false));
if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
}
for(let i = 0; i < 32; i++)
buffer[index++] = raw.charCodeAt(i);
}
if(!public_key) { /* Public kex K */
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
const raw = atob(Base64DecodeUrl(key_data.d, false));
if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
}
for(let i = 0; i < 32; i++)
buffer[index++] = raw.charCodeAt(i);
}
buffer[1] = index - 2; /* set the final sequence length */
return base64ArrayBuffer(buffer.buffer.slice(0, index));
}
const crypt_key = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e";
function c_strlen(buffer: Uint8Array, offset: number) : number {
let index = 0;
while(index + offset < buffer.length && buffer[index + offset] != 0)
index++;
return index;
}
export async function decrypt_ts_identity(buffer: Uint8Array) : Promise<string> {
/* buffer could contains a zero! */
const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20))));
for(let i = 0; i < 20; i++)
buffer[i] ^= hash[i];
const length = Math.min(buffer.length, 100);
for(let i = 0; i < length; i++)
buffer[i] ^= crypt_key.charCodeAt(i);
return ab2str(buffer);
}
export async function encrypt_ts_identity(buffer: Uint8Array) : Promise<string> {
const length = Math.min(buffer.length, 100);
for(let i = 0; i < length; i++)
buffer[i] ^= crypt_key.charCodeAt(i);
const hash = new Uint8Array(await sha.sha1(buffer.buffer.slice(20, 20 + c_strlen(buffer, 20))));
for(let i = 0; i < 20; i++)
buffer[i] ^= hash[i];
return base64ArrayBuffer(buffer);
}
/**
* @param buffer base64 encoded ASN.1 string
*/
export function decode_tomcrypt_key(buffer: string) {
const decoded = asn1.decode(atob(buffer));
let {x, y, k} = {
x: decoded.children[2].content(Infinity, asn1.TagType.VisibleString),
y: decoded.children[3].content(Infinity, asn1.TagType.VisibleString),
k: decoded.children[4].content(Infinity, asn1.TagType.VisibleString)
};
if(x.length > 32) {
if(x.charCodeAt(0) != 0)
throw "Invalid X coordinate! (Too long)";
x = x.substr(1);
}
if(y.length > 32) {
if(y.charCodeAt(0) != 0)
throw "Invalid Y coordinate! (Too long)";
y = y.substr(1);
}
if(k.length > 32) {
if(k.charCodeAt(0) != 0)
throw "Invalid private coordinate! (Too long)";
k = k.substr(1);
}
/*
console.log("Key x: %s (%d)", btoa(x), x.length);
console.log("Key y: %s (%d)", btoa(y), y.length);
console.log("Key k: %s (%d)", btoa(k), k.length);
*/
return {
crv: "P-256",
d: Base64EncodeUrl(btoa(k)),
x: Base64EncodeUrl(btoa(x)),
y: Base64EncodeUrl(btoa(y)),
ext: true,
key_ops:["deriveKey", "sign"],
kty:"EC",
};
}
}
export function loadIdentity(key: string) : TeamSpeakIdentity {
try {
let handle = funcationParseIdentity(key);
if(!handle) return undefined;
return new TeamSpeakIdentity(handle, "TeaWeb user");
} catch(error) {
console.error(error);
}
return undefined;
}
class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
identity: TeaSpeakIdentity;
export function loadIdentityFromFileContains(contains: string) : TeamSpeakIdentity {
let handle = funcationParseIdentityByFile(contains);
if(!handle) return undefined;
return new TeamSpeakIdentity(handle, "TeaWeb user");
}
export function load_identity(handle: TeamSpeakIdentity, key) : boolean {
let native_handle = funcationParseIdentity(key);
if(!native_handle) return false;
handle["handle"] = native_handle;
return true;
}
}
class TeamSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
identity: TeamSpeakIdentity;
constructor(connection: ServerConnection, identity: TeamSpeakIdentity) {
constructor(connection: ServerConnection, identity: TeaSpeakIdentity) {
super(connection);
this.identity = identity;
}
@ -91,7 +185,7 @@ namespace profiles.identities {
this.connection.sendCommand("handshakebegin", {
intention: 0,
authentication_method: this.identity.type(),
publicKey: this.identity.publicKey()
publicKey: this.identity.public_key
}).catch(error => {
console.error(tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
@ -102,8 +196,12 @@ namespace profiles.identities {
}
private handle_proof(json) {
const proof = this.identity.signMessage(json[0]["message"]);
if(!json[0]["digest"]) {
this.trigger_fail("server too old");
return;
}
this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => {
this.connection.sendCommand("handshakeindentityproof", {proof: proof}).catch(error => {
console.error(tr("Failed to proof the identity. Error: %o"), error);
@ -111,74 +209,577 @@ namespace profiles.identities {
error = error.extra_message || error.message;
this.trigger_fail("failed to execute proof (" + error + ")");
}).then(() => this.trigger_success());
}).catch(error => {
this.trigger_fail("failed to sign message");
});
}
}
export class TeamSpeakIdentity implements Identity {
private handle: any;
private _name: string;
class IdentityPOWWorker {
private _worker: Worker;
private _current_hash: string;
private _best_level: number;
constructor(handle: any, name: string) {
this.handle = handle;
async initialize(key: string) {
this._worker = new Worker(settings.static("worker_directory", "js/workers/") + "WorkerPOW.js");
/* initialize */
await new Promise<void>((resolve, reject) => {
const timeout_id = setTimeout(() => reject("timeout"), 1000);
this._worker.onmessage = event => {
clearTimeout(timeout_id);
if(!event.data) {
reject("invalid data");
return;
}
if(!event.data.success) {
reject("initialize failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")");
return;
}
this._worker.onmessage = event => this.handle_message(event.data);
resolve();
};
this._worker.onerror = event => {
console.error("POW Worker error %o", event);
clearTimeout(timeout_id);
reject("Failed to load worker (" + event.message + ")");
};
});
/* set data */
await new Promise<void>((resolve, reject) => {
this._worker.postMessage({
type: "set_data",
private_key: key,
code: "set_data"
});
const timeout_id = setTimeout(() => reject("timeout (data)"), 1000);
this._worker.onmessage = event => {
clearTimeout(timeout_id);
if (!event.data) {
reject("invalid data");
return;
}
if (!event.data.success) {
reject("initialize of data failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")");
return;
}
this._worker.onmessage = event => this.handle_message(event.data);
resolve();
};
});
}
async mine(hash: string, iterations: number, target: number, timeout?: number) : Promise<Boolean> {
this._current_hash = hash;
if(target < this._best_level)
return true;
return await new Promise<Boolean>((resolve, reject) => {
this._worker.postMessage({
type: "mine",
hash: this._current_hash,
iterations: iterations,
target: target,
code: "mine"
});
const timeout_id = setTimeout(() => reject("timeout (mine)"), timeout || 5000);
this._worker.onmessage = event => {
this._worker.onmessage = event => this.handle_message(event.data);
clearTimeout(timeout_id);
if (!event.data) {
reject("invalid data");
return;
}
if (!event.data.success) {
reject("mining failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")");
return;
}
if(event.data.result) {
this._best_level = event.data.level;
this._current_hash = event.data.hash;
resolve(true);
} else {
resolve(false); /* no result */
}
};
});
}
current_hash() : string {
return this._current_hash;
}
current_level() : number {
return this._best_level;
}
async finalize(timeout?: number) {
try {
await new Promise<void>((resolve, reject) => {
this._worker.postMessage({
type: "finalize",
code: "finalize"
});
const timeout_id = setTimeout(() => reject("timeout"), timeout || 250);
this._worker.onmessage = event => {
this._worker.onmessage = event => this.handle_message(event.data);
clearTimeout(timeout_id);
if (!event.data) {
reject("invalid data");
return;
}
if (!event.data.success) {
reject("failed to finalize (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")");
return;
}
resolve();
};
});
} catch(error) {
console.warn("Failed to finalize POW worker! (%o)", error);
}
this._worker.terminate();
this._worker = undefined;
}
private handle_message(message: any) {
console.log("Received message: %o", message);
}
}
export class TeaSpeakIdentity implements Identity {
static async generate_new() : Promise<TeaSpeakIdentity> {
const key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false);
console.log("Private key: %s (%d)", private_key, atob(private_key).length);
const identity = new TeaSpeakIdentity(private_key, "0", undefined, false);
await identity.initialize();
return identity;
}
static async import_ts(ts_string: string, ini?: boolean) : Promise<TeaSpeakIdentity> {
const parse_string = string => {
/* parsing without INI structure */
const V_index = string.indexOf('V');
if(V_index == -1) throw "invalid input (missing V)";
return {
hash: string.substr(0, V_index),
data: string.substr(V_index + 1),
name: "TeaSpeak user"
}
};
const {hash, data, name} = (!ini ? () => parse_string(ts_string) : () => {
/* parsing with INI structure */
let identity: string, name: string;
for(const line of ts_string.split("\n")) {
if(line.startsWith("identity="))
identity = line.substr(9);
else if(line.startsWith("nickname="))
name = line.substr(9);
}
if(!identity) throw "missing identity keyword";
if(identity[0] == "\"" && identity[identity.length - 1] == "\"")
identity = identity.substr(1, identity.length - 1);
const result = parse_string(identity);
result.name = name || result.name;
return result;
})();
if(!ts_string.match(/[0-9]+/g)) throw "invalid hash!";
const key64 = await CryptoHelper.decrypt_ts_identity(new Uint8Array(arrayBufferBase64(data)));
const identity = new TeaSpeakIdentity(key64, hash, name, false);
await identity.initialize();
return identity;
}
hash_number: string; /* hash suffix for the private key */
private_key: string; /* base64 representation of the private key */
_name: string;
public_key: string; /* only set when initialized */
private _initialized: boolean;
private _crypto_key: CryptoKey;
private _crypto_key_sign: CryptoKey;
private _unique_id: string;
constructor(private_key?: string, hash?: string, name?: string, initialize?: boolean) {
this.private_key = private_key;
this.hash_number = hash || "0";
this._name = name;
if(this.private_key && (typeof(initialize) === "undefined" || initialize)) {
this.initialize().catch(error => {
console.error("Failed to initialize TeaSpeakIdentity (%s)", error);
this._initialized = false;
});
}
}
securityLevel() : number | undefined {
return parseInt(TSIdentityHelper.unwarpString(TSIdentityHelper.funcationCalculateSecurityLevel(this.handle)));
name(): string {
return this._name;
}
name() : string { return this._name; }
uid(): string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.functionUid(this.handle));
return this._unique_id;
}
type() : IdentitifyType { return IdentitifyType.TEAMSPEAK; }
signMessage(message: string): string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.funcationSignMessage(this.handle, message, message.length));
type(): IdentitifyType {
return IdentitifyType.TEAMSPEAK;
}
exported() : string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.funcationExportIdentity(this.handle));
valid(): boolean {
return this._initialized && !!this._crypto_key && !!this._crypto_key_sign;
}
publicKey() : string {
return TSIdentityHelper.unwarpString(TSIdentityHelper.funcationPublicKey(this.handle));
}
async decode(data: string) : Promise<void> {
const json = JSON.parse(data);
if(!json) throw "invalid json";
valid() : boolean { return this.handle !== undefined; }
if(json.version == 2) {
this.private_key = json.key;
this.hash_number = json.hash;
this._name = json.name;
} else if(json.version == 1) {
const key = json.key;
this._name = json.name;
decode(data) : boolean {
data = JSON.parse(data);
if(data.version != 1) return false;
const clone = await TeaSpeakIdentity.import_ts(key, false);
this.private_key = clone.private_key;
this.hash_number = clone.hash_number;
} else
throw "invalid version";
if(!TSIdentityHelper.load_identity(this, data["key"]))
return false;
this._name = data["name"];
return true;
await this.initialize();
}
encode?() : string {
if(!this.handle) return undefined;
const key = this.exported();
if(!key) return undefined;
return JSON.stringify({
key: key,
key: this.private_key,
hash: this.hash_number,
name: this._name,
version: 1
})
version: 2
});
}
async level() : Promise<number> {
if(!this._initialized || !this.public_key)
throw "not initialized";
const hash = new Uint8Array(await sha.sha1(this.public_key + this.hash_number));
let level = 0;
while(level < hash.byteLength && hash[level] == 0)
level++;
if(level >= hash.byteLength) {
level = 256;
} else {
let byte = hash[level];
level <<= 3;
while((byte & 0x1) == 0) {
level++;
byte >>= 1;
}
}
return level;
}
/**
* @param {string} a
* @param {string} b
* @description b must be smaller (in bytes) then a
*/
private string_add(a: string, b: string) {
const char_result: number[] = [];
const char_a = [...a].reverse().map(e => e.charCodeAt(0));
const char_b = [...b].reverse().map(e => e.charCodeAt(0));
let carry = false;
while(char_b.length > 0) {
let result = char_b.pop_front() + char_a.pop_front() + (carry ? 1 : 0) - 48;
if((carry = result > 57))
result -= 10;
char_result.push(result);
}
while(char_a.length > 0) {
let result = char_a.pop_front() + (carry ? 1 : 0);
if((carry = result > 57))
result -= 10;
char_result.push(result);
}
if(carry)
char_result.push(49);
return String.fromCharCode.apply(null, char_result.reverse());
}
async improve_level_for(time: number, threads: number) : Promise<Boolean> {
let active = true;
setTimeout(() => active = false, time);
return await this.improve_level(-1, threads, () => active);
}
async improve_level(target: number, threads: number, active_callback: () => boolean) : Promise<Boolean> {
if(!this._initialized || !this.public_key)
throw "not initialized";
if(target == -1) /* get the highest level possible */
target = 0;
else if(target <= await this.level())
return true;
const workers: IdentityPOWWorker[] = [];
const iterations = 100000;
let current_hash;
const next_hash = () => {
if(!current_hash)
return (current_hash = this.hash_number);
if(current_hash.length < iterations.toString().length) {
current_hash = this.string_add(iterations.toString(), current_hash);
} else {
current_hash = this.string_add(current_hash, iterations.toString());
}
return current_hash;
};
{ /* init */
const initialize_promise: Promise<void>[] = [];
for(let index = 0; index < threads; index++) {
const worker = new IdentityPOWWorker();
workers.push(worker);
initialize_promise.push(worker.initialize(this.public_key));
}
try {
await Promise.all(initialize_promise);
} catch(error) {
console.error(error);
throw "failed to initialize";
}
}
let result = false;
let best_level = 0;
let target_level = target > 0 ? target : await this.level() + 1;
const worker_promise: Promise<void>[] = [];
try {
result = await new Promise<boolean>((resolve, reject) => {
let active = true;
const exit = () => {
const timeout = setTimeout(() => resolve(true), 1000);
Promise.all(worker_promise).then(result => {
clearTimeout(timeout);
resolve(true);
}).catch(error => resolve(true));
active = false;
};
for(const worker of workers) {
const worker_mine = () => {
if(!active) return;
const promise = worker.mine(next_hash(), iterations, target_level);
const p = promise.then(result => {
worker_promise.remove(p);
if(result.valueOf()) {
if(worker.current_level() > best_level) {
this.hash_number = worker.current_hash();
console.log("Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level);
best_level = worker.current_level();
}
if(active) {
if(target > 0)
exit();
else
target_level = best_level + 1;
}
}
if(active && (active = active_callback()))
setTimeout(() => worker_mine(), 0);
else {
exit();
}
return Promise.resolve();
}).catch(error => {
worker_promise.remove(p);
console.warn("POW worker error %o", error);
reject(error);
return Promise.resolve();
});
worker_promise.push(p);
};
worker_mine();
}
});
} catch(error) {
//error already printed before reject had been called
}
{ /* shutdown */
const finalize_promise: Promise<void>[] = [];
for(const worker of workers)
finalize_promise.push(worker.finalize(250));
try {
await Promise.all(finalize_promise);
} catch(error) {
console.error(error);
throw "failed to finalize";
}
}
return result;
}
private async initialize() {
if(!this.private_key)
throw "Invalid private key";
let jwk: any;
try {
jwk = await CryptoHelper.decode_tomcrypt_key(this.private_key);
if(!jwk)
throw "result undefined";
} catch(error) {
throw "failed to parse key (" + error + ")";
}
try {
this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]);
} catch(error) {
console.error(error);
throw "failed to create crypto sign key";
}
try {
this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
} catch(error) {
console.error(error);
throw "failed to create crypto key";
}
try {
this.public_key = await CryptoHelper.export_ecc_key(this._crypto_key, true);
this._unique_id = base64ArrayBuffer(await sha.sha1(this.public_key));
} catch(error) {
console.error(error);
throw "failed to calculate unique id";
}
this._initialized = true;
//const public_key = await profiles.identities.CryptoHelper.export_ecc_key(key, true);
}
async export_ts(ini?: boolean) : Promise<string> {
if(!this.private_key)
throw "Invalid private key";
const identity = this.hash_number + "V" + await CryptoHelper.encrypt_ts_identity(new Uint8Array(str2ab8(this.private_key)));
if(!ini) return identity;
return "[Identity]\n" +
"id=TeaWeb-Exported\n" +
"identity=\"" + identity + "\"\n" +
"nickname=\"" + this.name() + "\"\n" +
"phonetic_nickname=";
}
async sign_message(message: string, hash: string = "SHA-256") : Promise<string> {
/* bring this to libtomcrypt format */
const sign_buffer = await crypto.subtle.sign({
name: "ECDSA",
hash: hash
}, this._crypto_key_sign, str2ab8(message));
const sign = new Uint8Array(sign_buffer);
/* first 32 r bits | last 32 s bits */
const buffer = new Uint8Array(72);
let index = 0;
{ /* the initial sequence */
buffer[index++] = 0x30; /* type */
buffer[index++] = 0x00; /* we will set the sequence length later */
}
{ /* integer r */
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
if(sign[0] > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
}
for(let i = 0; i < 32; i++)
buffer[index++] = sign[i];
}
{ /* integer s */
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
if(sign[32] > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
}
for(let i = 0; i < 32; i++)
buffer[index++] = sign[32 + i];
}
buffer[1] = index - 2;
return base64ArrayBuffer(buffer.subarray(0, index));
}
spawn_identity_handshake_handler(connection: ServerConnection): HandshakeIdentityHandler {
return new TeamSpeakHandshakeHandler(connection, this);
return new TeaSpeakHandshakeHandler(connection, this);
}
}
export function setup_teamspeak() : boolean {
return TSIdentityHelper.setup();
}
}

View File

@ -93,7 +93,7 @@ namespace Modals {
select_invalid_tag.hide();
updateFields();
});
select_tag.val('default').trigger('change');
select_tag.val(connect_profile && connect_profile.enforce ? connect_profile.profile.id : connect_profile && connect_profile.profile ? connect_profile.profile.id : 'default').trigger('change');
}
tag.find(".connect_nickname").on("keyup", () => updateFields());

View File

@ -496,10 +496,9 @@ namespace Modals {
} else if(selected_type == IdentitifyType.TEAMSPEAK) {
console.log("Set: " + identity);
const teamspeak_tag = tag = settings_tag.find(".identity-settings-teamspeak");
if(identity)
teamspeak_tag.find(".identity_string").val((identity as profiles.identities.TeamSpeakIdentity).exported());
else
teamspeak_tag.find(".identity_string").val("");
if(identity)
(identity as profiles.identities.TeaSpeakIdentity).export_ts().then(e => teamspeak_tag.find(".identity_string").val(e));
} else if(selected_type == IdentitifyType.NICKNAME) {
const name_tag = tag = settings_tag.find(".identity-settings-nickname");
if(identity)
@ -571,16 +570,16 @@ namespace Modals {
const element = event.target as HTMLInputElement;
const file_reader = new FileReader();
file_reader.onload = function() {
const identity = profiles.identities.TSIdentityHelper.loadIdentityFromFileContains(file_reader.result as string);
if(!identity) {
display_error(tr("Failed to parse identity.<br>Reason: ") + profiles.identities.TSIdentityHelper.last_error());
return;
} else {
teamspeak_tag.find(".identity_string").val(identity.exported());
const identity_promise = profiles.identities.TeaSpeakIdentity.import_ts(file_reader.result, true);
identity_promise.then(identity => {
(identity as profiles.identities.TeaSpeakIdentity).export_ts().then(e => teamspeak_tag.find(".identity_string").val(e));
selected_profile.set_identity(IdentitifyType.TEAMSPEAK, identity as any);
profiles.mark_need_save();
display_error(undefined);
}
}).catch(error => {
display_error(tr("Failed to parse identity.<br>Reason: ") + error);
return;
});
};
file_reader.onerror = ev => {
@ -602,18 +601,16 @@ namespace Modals {
selected_profile.set_identity(IdentitifyType.TEAMSPEAK, undefined as any);
profiles.mark_need_save();
} else {
const identity = profiles.identities.TSIdentityHelper.loadIdentity(element.value);
if(!identity) {
selected_profile.set_identity(IdentitifyType.TEAMSPEAK, identity as any);
profiles.mark_need_save();
display_error("Failed to parse identity string!");
return;
}
const identity_promise = profiles.identities.TeaSpeakIdentity.import_ts(element.value, false);
identity_promise.then(identity => {
(identity as profiles.identities.TeaSpeakIdentity).export_ts().then(e => teamspeak_tag.find(".identity_string").val(e));
selected_profile.set_identity(IdentitifyType.TEAMSPEAK, identity as any);
profiles.mark_need_save();
display_error(undefined);
}).catch(error => {
display_error(tr("Failed to parse identity.<br>Reason: ") + error);
return;
});
}
});

View File

@ -331,6 +331,8 @@ interface PPTKeySettings extends ppt.KeyDescriptor{
class PushToTalkVAD extends VoiceActivityDetector {
private _key: ppt.KeyDescriptor;
private _key_hook: ppt.KeyHook;
private _timeout: NodeJS.Timer;
private _delay = /* 300 */ 0; //TODO configurable
private _pushed: boolean = false;
@ -338,8 +340,21 @@ class PushToTalkVAD extends VoiceActivityDetector {
super();
this._key = key;
this._key_hook = {
callback_release: () => this._pushed = false,
callback_press: () => this._pushed = true,
callback_release: () => {
if(this._timeout)
clearTimeout(this._timeout);
if(this._delay > 0)
this._timeout = setTimeout(() => this._pushed = false, this._delay);
else
this._pushed = false;
},
callback_press: () => {
if(this._timeout)
clearTimeout(this._timeout);
this._pushed = true;
},
cancel: false
} as ppt.KeyHook;

View File

@ -0,0 +1,97 @@
declare namespace WebAssembly {
export function instantiateStreaming(stream: Promise<Response>, imports?: any) : Promise<ResultObject>;
}
const prefix = "[POWWorker] ";
let initialized = false;
let memory: WebAssembly.Memory;
let memory_u8: Uint8Array;
let wasm_object: WebAssembly.ResultObject;
function post_status(code: string | undefined, result: boolean | string | any) {
let data: any = {};
data.code = code;
if(typeof(result) === "string") {
data.success = false;
data.message = result;
} else if(typeof(result) === "boolean") {
data.success = result;
} else {
data.success = true;
Object.assign(data, result);
}
postMessage(data);
}
{ /* initialize WASM handle */
memory = new WebAssembly.Memory({ initial: 1 });
memory_u8 = new Uint8Array(memory.buffer);
if(typeof(WebAssembly.instantiateStreaming) === "undefined") {
WebAssembly.instantiateStreaming = async (stream: Promise<Response>, imports?: any) => {
const response = await stream;
const buffer = await response.arrayBuffer();
return WebAssembly.instantiate(buffer, imports);
}
}
WebAssembly.instantiateStreaming(fetch('../../wat/pow/sha1.wasm'), {
env: {
memory: memory
}
}).then(object => {
wasm_object = object;
post_status("initialize", true);
}).catch(error => {
post_status("initialize", "failed to initialize WASM handle (" + error + ")");
});
}
let key_offset = 0;
let hash_offset = 0;
onmessage = function(e: MessageEvent) {
let data = e.data;
if(data.type == "set_data") {
const key = data.private_key;
key_offset = 0;
for(const char of key)
memory_u8[0x0A0 + key_offset++] = char.charCodeAt(0);
post_status(data.code, true);
} else if(data.type == "mine") {
let hash: string = data.hash;
const iterations: number = data.iterations;
const target: number = data.target;
hash_offset = 0;
for(const char of hash) {
memory_u8[0x0A0 + key_offset + hash_offset++] = char.charCodeAt(0);
}
let level = wasm_object.instance.exports.mine(key_offset, hash_offset, iterations, target > 1 ? target - 1 : target);
hash = "";
hash_offset = 0;
while(memory_u8[0x0A0 + key_offset + hash_offset] != 0)
hash = hash + String.fromCharCode(memory_u8[0x0A0 + key_offset + hash_offset++]);
// console.log(prefix + "New hash: %s, level %o", hash, level);
post_status(data.code, {
result: level >= target,
hash: hash,
level: level
});
} else if(data.type == "finalize") {
wasm_object = undefined;
memory = undefined;
memory_u8 = undefined;
post_status(data.code, true);
}
};

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "none",
"target": "es6",
"sourceMap": true,
"outFile": "WorkerPOW.js"
},
"files": [
"pow/POWWorker.ts",
]
}

BIN
shared/wat/pow/sha1.wasm Normal file

Binary file not shown.

490
shared/wat/pow/sha1.wat Normal file
View File

@ -0,0 +1,490 @@
;; SHA-1 code from https://github.com/Snack-X/wasm-works/blob/master/modules/sha1.wat by Snack-X
(module
;; import 1 page of memory from env.memory
;; [0x000;0x03f] will be used as input chunk
;; [0x040;0x053] will be used as output value
;; [0x0A0;0x1FF] base64 memory
(import "env" "memory" (memory 1))
;; functions to export
(export "mine" (func $mine))
;; global variables
(global $message_len (mut i32) (i32.const 0))
(global $h0 (mut i32) (i32.const 0))
(global $h1 (mut i32) (i32.const 0))
(global $h2 (mut i32) (i32.const 0))
(global $h3 (mut i32) (i32.const 0))
(global $h4 (mut i32) (i32.const 0))
;; helper function `get_word`
;; input - word index
;; output - offset
(func $get_word (param $w i32) (result i32)
;; offset = ($w & 0xf) * 4
(return (call $flip_endian (i32.load
(i32.mul
(i32.and (get_local $w) (i32.const 0xf))
(i32.const 4)
)
)))
)
;; helper function `set_word`
;; input - word index, value
(func $set_word (param $w i32) (param $v i32)
;; offset = ($w & 0xf) * 4
(i32.store (call $flip_endian
(i32.mul
(i32.and (get_local $w) (i32.const 0xf))
(i32.const 4)
)
(get_local $v)
))
)
;; helper function `flip_endian`
;; once `i32.bswap` is landed, this function is useless
(func $flip_endian (param $w i32) (result i32)
;; (w & 0xff000000 >>> 24) |
;; (w & 0x00ff0000 >>> 8) |
;; (w & 0x0000ff00 << 8) |
;; (w & 0x000000ff << 24)
(return (i32.or
(i32.or
(i32.shr_u (i32.and (get_local $w) (i32.const 0xff000000)) (i32.const 24))
(i32.shr_u (i32.and (get_local $w) (i32.const 0x00ff0000)) (i32.const 8))
)
(i32.or
(i32.shl (i32.and (get_local $w) (i32.const 0x0000ff00)) (i32.const 8))
(i32.shl (i32.and (get_local $w) (i32.const 0x000000ff)) (i32.const 24))
)
))
)
;; function `sha1_init`
;; initialize memory
(func $sha1_init
(i64.store (i32.const 0x00) (i64.const 0))
(i64.store (i32.const 0x08) (i64.const 0))
(i64.store (i32.const 0x10) (i64.const 0))
(i64.store (i32.const 0x18) (i64.const 0))
(i64.store (i32.const 0x20) (i64.const 0))
(i64.store (i32.const 0x28) (i64.const 0))
(i64.store (i32.const 0x30) (i64.const 0))
(i64.store (i32.const 0x38) (i64.const 0))
(set_global $message_len (i32.const 0))
(set_global $h0 (i32.const 0x67452301))
(set_global $h1 (i32.const 0xefcdab89))
(set_global $h2 (i32.const 0x98badcfe))
(set_global $h3 (i32.const 0x10325476))
(set_global $h4 (i32.const 0xc3d2e1f0))
)
;; function `sha1_update`
;; process full block
(func $sha1_update
;; word counter
(local $w i32)
;; internal variables
(local $a i32) (local $b i32) (local $c i32) (local $d i32) (local $e i32)
(local $f i32) (local $k i32) (local $t i32)
;; message_len += 64 bytes (512 bits)
(set_global $message_len (i32.add (get_global $message_len) (i32.const 64)))
;; load h0 ~ h4
(set_local $a (get_global $h0))
(set_local $b (get_global $h1))
(set_local $c (get_global $h2))
(set_local $d (get_global $h3))
(set_local $e (get_global $h4))
;; loop
(set_local $w (i32.const 0))
(block $done
(loop $loop
;; word 0 ~ 15 will be used as-is on memory
;; word 16 ~ 79 will be calculated and replaced on memory
(if
;; if 16 <= $w
(i32.ge_s (get_local $w) (i32.const 16))
;; calculate word to use
(call $set_word
(get_local $w)
;; value = (words[w-3] ^ words[w-8] ^ words[w-14] ^ words[w-16]) rotl 1
(i32.rotl
(i32.xor
(i32.xor
(call $get_word (i32.sub (get_local $w) (i32.const 3)))
(call $get_word (i32.sub (get_local $w) (i32.const 8)))
)
(i32.xor
(call $get_word (i32.sub (get_local $w) (i32.const 14)))
(call $get_word (i32.sub (get_local $w) (i32.const 16)))
)
)
(i32.const 1)
)
)
)
;; calculate f and determine k
(block $get_key
(if (i32.lt_s (get_local $w) (i32.const 20))
(block
;; f = (b & c) | (~b & d)
(set_local $f
(i32.or
(i32.and (get_local $b) (get_local $c))
;; ~a == a ^ 0xffffffff
(i32.and (i32.xor (get_local $b) (i32.const 0xffffffff)) (get_local $d))
)
)
(set_local $k (i32.const 0x5a827999))
(br $get_key)
)
)
(if (i32.lt_s (get_local $w) (i32.const 40))
(block
;; f = b ^ c ^ d
(set_local $f
(i32.xor
(i32.xor (get_local $b) (get_local $c))
(get_local $d)
)
)
(set_local $k (i32.const 0x6ed9eba1))
(br $get_key)
)
)
(if (i32.lt_s (get_local $w) (i32.const 60))
(block
;; f = (b & c) | (b & d) | (c & d)
(set_local $f
(i32.or
(i32.or
(i32.and (get_local $b) (get_local $c))
(i32.and (get_local $b) (get_local $d))
)
(i32.and (get_local $c) (get_local $d))
)
)
(set_local $k (i32.const 0x8f1bbcdc))
(br $get_key)
)
)
(if (i32.lt_s (get_local $w) (i32.const 80))
(block
;; f = b ^ c ^ d
(set_local $f
(i32.xor
(i32.xor (get_local $b) (get_local $c))
(get_local $d)
)
)
(set_local $k (i32.const 0xca62c1d6))
(br $get_key)
)
)
)
;; t = a rotl 5 + f + e + k + words[w]
(set_local $t
(i32.add
(i32.add
(i32.add
(i32.rotl (get_local $a) (i32.const 5))
(get_local $f)
)
(i32.add
(get_local $e)
(get_local $k)
)
)
(call $get_word (get_local $w))
)
)
;; rotate variables
(set_local $e (get_local $d))
(set_local $d (get_local $c))
(set_local $c (i32.rotl (get_local $b) (i32.const 30)))
(set_local $b (get_local $a))
(set_local $a (get_local $t))
;; w += 1
(set_local $w (i32.add (get_local $w) (i32.const 1)))
;; if 80 <= w, break
(br_if $done (i32.ge_s (get_local $w) (i32.const 80)))
;; else, continue
(br $loop)
)
)
;; feed to h0 ~ h4
(set_global $h0 (i32.add (get_local $a) (get_global $h0)))
(set_global $h1 (i32.add (get_local $b) (get_global $h1)))
(set_global $h2 (i32.add (get_local $c) (get_global $h2)))
(set_global $h3 (i32.add (get_local $d) (get_global $h3)))
(set_global $h4 (i32.add (get_local $e) (get_global $h4)))
)
;; function `sha1_end`
;; input - length of final chunk
(func $sha1_end (param $final_len i32)
(local $total_len i32)
(local $i i32)
;; total_len = message_len + final_len
(set_local $total_len (i32.add
(get_global $message_len)
(get_local $final_len)
))
;; append 0x80
(i32.store8 (get_local $final_len) (i32.const 0x80))
(set_local $i (i32.add (get_local $final_len) (i32.const 1)))
(if (i32.gt_s (get_local $i) (i32.const 56))
;; if 56 < i
(block
(block $done_pad
;; zero pad
(loop $loop
(br_if $done_pad (i32.ge_s (get_local $i) (i32.const 64)))
(i32.store8 (get_local $i) (i32.const 0))
(set_local $i (i32.add (get_local $i) (i32.const 1)))
(br $loop)
)
)
;; update
(call $sha1_update)
;; fill 14 words with 0
(i64.store (i32.const 0x00) (i64.const 0))
(i64.store (i32.const 0x08) (i64.const 0))
(i64.store (i32.const 0x10) (i64.const 0))
(i64.store (i32.const 0x18) (i64.const 0))
(i64.store (i32.const 0x20) (i64.const 0))
(i64.store (i32.const 0x28) (i64.const 0))
(i64.store (i32.const 0x30) (i64.const 0))
)
;; else
(block $done_pad
;; zero pad
(loop $loop
(br_if $done_pad (i32.ge_s (get_local $i) (i32.const 56)))
(i32.store8 (get_local $i) (i32.const 0))
(set_local $i (i32.add (get_local $i) (i32.const 1)))
(br $loop)
)
)
)
;; append length (in bits)
(call $set_word (i32.const 14) (i32.const 0))
(call $set_word (i32.const 15) (i32.mul (get_local $total_len) (i32.const 8)))
;; update final block
(call $sha1_update)
;; copy h0~4 to memory
(i32.store (i32.const 0x40) (get_global $h0))
(i32.store (i32.const 0x44) (get_global $h1))
(i32.store (i32.const 0x48) (get_global $h2))
(i32.store (i32.const 0x4c) (get_global $h3))
(i32.store (i32.const 0x50) (get_global $h4))
)
(func $increase_counter
;; $offset must be absolute in memory
(param $offset i32)
(param $length i32)
;; The new length
(result i32)
(local $index i32)
(local $character i32)
(set_local $index (i32.add (get_local $offset) (get_local $length)))
(loop $main_loop
;; Decrease variable
(set_local $index (i32.sub (get_local $index) (i32.const 1)))
(if
;; Test if we're over the number bound
(i32.lt_u (get_local $index) (get_local $offset))
(block
;; Increase the index by one again to set the first character to 1
(i32.store8 (i32.add (get_local $index) (i32.const 1)) (i32.const 49))
(i32.store8 (i32.add (get_local $offset) (get_local $length)) (i32.const 48))
(set_local $length (i32.add (get_local $length) (i32.const 1)))
(return (get_local $length))
)
)
(set_local $character (i32.load8_u (get_local $index)))
(if
;; $character == '9'
(i32.eq (get_local $character) (i32.const 57))
(block
;; Set it to '0' and decrease $index
(i32.store8 (get_local $index) (i32.const 48))
(br $main_loop)
)
)
;; Increase by one
(i32.store8 (get_local $index) (i32.add (get_local $character) (i32.const 1)))
)
(return (get_local $length))
)
(func $mine
;; Length of the base 64 string
(param $length64 i32)
;; Length of the counter
(param $length_counter i32)
;; Iterations to do
(param $iterations i32)
;; The current best level we want to overreach
(param $target_level i32)
;; Returns the best found level
(result i32)
(local $level i32)
(local $best_level i32)
(local $write_offset i32)
(local $write_index i32)
(local $max_write_index i32)
(set_local $best_level (get_local $target_level))
(block $done
(loop $main_loop
call $sha1_init
;; Load the first 64 bytes
(i64.store (i32.const 0x00) (i64.load (i32.const 0x0A0)))
(i64.store (i32.const 0x08) (i64.load (i32.const 0x0A8)))
(i64.store (i32.const 0x10) (i64.load (i32.const 0x0B0)))
(i64.store (i32.const 0x18) (i64.load (i32.const 0x0B8)))
(i64.store (i32.const 0x20) (i64.load (i32.const 0x0C0)))
(i64.store (i32.const 0x28) (i64.load (i32.const 0x0C8)))
(i64.store (i32.const 0x30) (i64.load (i32.const 0x0D0)))
(i64.store (i32.const 0x38) (i64.load (i32.const 0x0D8)))
call $sha1_update
(set_local $max_write_index (i32.add (i32.add (get_local $length64) (get_local $length_counter)) (i32.const 0x0A0)))
(set_local $write_index (i32.const 0x0E0))
(set_local $write_offset (i32.const 0))
(loop $write_loop
(i32.store8 (get_local $write_offset) (i32.load8_u (get_local $write_index)))
(set_local $write_offset (i32.add (get_local $write_offset) (i32.const 1)))
(if (i32.eq (get_local $write_offset) (i32.const 64))
(block
(call $sha1_update)
(set_local $write_offset (i32.const 0))
)
)
(set_local $write_index (i32.add (get_local $write_index) (i32.const 1)))
(br_if $write_loop (i32.lt_s (get_local $write_index) (get_local $max_write_index)))
)
(call $sha1_end (get_local $write_offset))
;; Count for each block the tailing zero bits. If the bits are 32 then add the next block
(set_local $level (i32.ctz (call $flip_endian (get_global $h0)))) ;; First block [0;32[
(if
(i32.eq (get_local $level) (i32.const 32))
(block
(set_local $level
(i32.add (i32.ctz (call $flip_endian (get_global $h1))) (get_local $level)) ;; Second block [32;64[
)
(if
(i32.eq (get_local $level) (i32.const 64))
(block
(set_local $level
(i32.add (i32.ctz (call $flip_endian (get_global $h2))) (get_local $level)) ;; Third block [64;86[
)
(if
(i32.eq (get_local $level) (i32.const 84))
(block
(set_local $level
(i32.add (i32.ctz (call $flip_endian (get_global $h3))) (get_local $level)) ;; Fourth block [86;128[
)
(if
(i32.eq (get_local $level) (i32.const 128))
(block
(set_local $level
(i32.add (i32.ctz (call $flip_endian (get_global $h4))) (get_local $level)) ;; Fifth block [128;160[
)
)
)
)
)
)
)
)
)
(if (i32.lt_u (get_local $best_level) (get_local $level))
(block
(set_local $best_level (get_local $level))
;; If we have a target level then break here
(if
(i32.ne (get_local $target_level) (i32.const 0))
(br $done)
)
)
)
;; Increase everything
(set_local $iterations (i32.sub (get_local $iterations) (i32.const 1)))
(set_local $length_counter
(call $increase_counter (i32.add (i32.const 0x0A0) (get_local $length64)) (get_local $length_counter))
)
(br_if $main_loop (i32.gt_u (get_local $iterations) (i32.const 0)))
)
)
;; May length had changed, so we null terminate it
(i64.store (i32.add (i32.const 0x0A0) (i32.add (get_local $length64) (get_local $length_counter))) (i64.const 0x0))
(return (get_local $best_level))
)
)

10
tools/dtsgen/out.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* File: /home/wolverindev/TeaSpeak/TeaSpeak/Web-Client/tools/dtsgen/test/test_03.ts */
declare enum YY {
H,
B
}
declare interface X {
type: any;
c: YY.B;
}

2
vendor/bbcode vendored

@ -1 +1 @@
Subproject commit 8f2626cf1e24edca8484545841967525177ef3aa
Subproject commit 8304246b4f651b9de141e4d3252b4f78e5c55391