diff --git a/ChangeLog.md b/ChangeLog.md index 795d43b9..aba62fca 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -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 diff --git a/TeaWeb.zip b/TeaWeb.zip new file mode 100644 index 00000000..d6f9b88b Binary files /dev/null and b/TeaWeb.zip differ diff --git a/files.php b/files.php index f6db47bc..79ba1724 100644 --- a/files.php +++ b/files.php @@ -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)/", diff --git a/package.json b/package.json index 9d6ff730..15ec0a76 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/shared/js/connection.ts b/shared/js/connection.ts index 8d38afbb..4613a2f7 100644 --- a/shared/js/connection.ts +++ b/shared/js/connection.ts @@ -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); } diff --git a/shared/js/crypto/asn1.ts b/shared/js/crypto/asn1.ts new file mode 100644 index 00000000..8184a4aa --- /dev/null +++ b/shared/js/crypto/asn1.ts @@ -0,0 +1,547 @@ +// ASN.1 JavaScript decoder +// Copyright (c) 2008-2018 Lapo Luchini +// Copyright (c) 2019-2019 Markus Hadenfeldt + +// 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)); + } +} \ No newline at end of file diff --git a/shared/js/crypto/sha.ts b/shared/js/crypto/sha.ts index c30f48f5..b13f3c80 100644 --- a/shared/js/crypto/sha.ts +++ b/shared/js/crypto/sha.ts @@ -392,6 +392,8 @@ namespace sha { return result.buffer; } export function sha1(message: string | ArrayBuffer) : PromiseLike { + 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)) diff --git a/shared/js/load.ts b/shared/js/load.ts index 66195f61..7ff175a8 100644 --- a/shared/js/load.ts +++ b/shared/js/load.ts @@ -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", diff --git a/shared/js/main.ts b/shared/js/main.ts index 534919af..00ea86f0 100644 --- a/shared/js/main.ts +++ b/shared/js/main.ts @@ -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); diff --git a/shared/js/profiles/ConnectionProfile.ts b/shared/js/profiles/ConnectionProfile.ts index f3d86eac..aaf834bc 100644 --- a/shared/js/profiles/ConnectionProfile.ts +++ b/shared/js/profiles/ConnectionProfile.ts @@ -70,7 +70,7 @@ namespace profiles { } } - function decode_profile(data) : ConnectionProfile | string { + async function decode_profile(data) : Promise { 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; diff --git a/shared/js/profiles/Identity.ts b/shared/js/profiles/Identity.ts index ab4d6628..91dafb8e 100644 --- a/shared/js/profiles/Identity.ts +++ b/shared/js/profiles/Identity.ts @@ -13,12 +13,12 @@ namespace profiles.identities { valid() : boolean; encode?() : string; - decode(data: string) : boolean; + decode(data: string) : Promise; 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 { 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; diff --git a/shared/js/profiles/identities/NameIdentity.ts b/shared/js/profiles/identities/NameIdentity.ts index b1721686..790ca7b7 100644 --- a/shared/js/profiles/identities/NameIdentity.ts +++ b/shared/js/profiles/identities/NameIdentity.ts @@ -51,13 +51,13 @@ namespace profiles.identities { return this._name != undefined && this._name.length >= 3; } - decode(data) { + decode(data) : Promise { data = JSON.parse(data); if(data.version !== 1) - return false; + throw "invalid version"; this._name = data["name"]; - return true; + return; } encode?() : string { diff --git a/shared/js/profiles/identities/TeaForumIdentity.ts b/shared/js/profiles/identities/TeaForumIdentity.ts index f8059f39..5194499f 100644 --- a/shared/js/profiles/identities/TeaForumIdentity.ts +++ b/shared/js/profiles/identities/TeaForumIdentity.ts @@ -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 { 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 { diff --git a/shared/js/profiles/identities/TeamSpeakIdentity.ts b/shared/js/profiles/identities/TeamSpeakIdentity.ts index 3f8e53c2..3a91adcc 100644 --- a/shared/js/profiles/identities/TeamSpeakIdentity.ts +++ b/shared/js/profiles/identities/TeamSpeakIdentity.ts @@ -1,86 +1,180 @@ /// 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 function last_error() : string { - return unwarpString(functionLastError()); - } - - export function unwarpString(str) : string { - if(str == "") return ""; - try { - if(!$.isFunction(window.Pointer_stringify)) { - displayCriticalError(tr("Missing required wasm function!
Please reload the page!")); + 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); } - let message: string = window.Pointer_stringify(str); - functionDestroyString(str); - return message; - } catch (error) { - console.error(error); - return ""; + + */ + + 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 */ } - } - - 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); + { /* 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)*/ } - return undefined; + { /* 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; + } + + for(let i = 0; i < 32; i++) + buffer[index++] = raw.charCodeAt(i); + } + { /* 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)); } - export function loadIdentityFromFileContains(contains: string) : TeamSpeakIdentity { - let handle = funcationParseIdentityByFile(contains); - if(!handle) return undefined; - return new TeamSpeakIdentity(handle, "TeaWeb user"); + 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 function load_identity(handle: TeamSpeakIdentity, key) : boolean { - let native_handle = funcationParseIdentity(key); - if(!native_handle) return false; + export async function decrypt_ts_identity(buffer: Uint8Array) : Promise { + /* 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]; - handle["handle"] = native_handle; - return true; + 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 { + 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", + }; } } - class TeamSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { - identity: TeamSpeakIdentity; + class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler { + identity: TeaSpeakIdentity; - 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,83 +196,590 @@ 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.connection.sendCommand("handshakeindentityproof", {proof: proof}).catch(error => { - console.error(tr("Failed to proof the identity. Error: %o"), error); + 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); - if(error instanceof CommandResult) - error = error.extra_message || error.message; - this.trigger_fail("failed to execute proof (" + error + ")"); - }).then(() => this.trigger_success()); + if(error instanceof CommandResult) + 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((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((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 { + this._current_hash = hash; + if(target < this._best_level) + return true; + + return await new Promise((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((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 { + 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 { + 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)); + uid(): string { + 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 { + 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 { + 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()); } - spawn_identity_handshake_handler(connection: ServerConnection) : HandshakeIdentityHandler { - return new TeamSpeakHandshakeHandler(connection, this); - } - } + async improve_level_for(time: number, threads: number) : Promise { + let active = true; + setTimeout(() => active = false, time); - export function setup_teamspeak() : boolean { - return TSIdentityHelper.setup(); + return await this.improve_level(-1, threads, () => active); + } + + async improve_level(target: number, threads: number, active_callback: () => boolean) : Promise { + 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[] = []; + 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[] = []; + try { + result = await new Promise((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[] = []; + 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 { + 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 { + /* 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 TeaSpeakHandshakeHandler(connection, this); + } } } \ No newline at end of file diff --git a/shared/js/ui/modal/ModalConnect.ts b/shared/js/ui/modal/ModalConnect.ts index 2c26adb9..cd31a280 100644 --- a/shared/js/ui/modal/ModalConnect.ts +++ b/shared/js/ui/modal/ModalConnect.ts @@ -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()); diff --git a/shared/js/ui/modal/ModalSettings.ts b/shared/js/ui/modal/ModalSettings.ts index ff2cc437..97a67c08 100644 --- a/shared/js/ui/modal/ModalSettings.ts +++ b/shared/js/ui/modal/ModalSettings.ts @@ -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"); + teamspeak_tag.find(".identity_string").val(""); if(identity) - teamspeak_tag.find(".identity_string").val((identity as profiles.identities.TeamSpeakIdentity).exported()); - else - teamspeak_tag.find(".identity_string").val(""); + (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.
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.
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) { + 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("Failed to parse identity string!"); + display_error(undefined); + }).catch(error => { + display_error(tr("Failed to parse identity.
Reason: ") + error); return; - } - - selected_profile.set_identity(IdentitifyType.TEAMSPEAK, identity as any); - profiles.mark_need_save(); - display_error(undefined); + }); } }); diff --git a/shared/js/voice/VoiceRecorder.ts b/shared/js/voice/VoiceRecorder.ts index 88c320b9..67de2bdd 100644 --- a/shared/js/voice/VoiceRecorder.ts +++ b/shared/js/voice/VoiceRecorder.ts @@ -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; diff --git a/shared/js/workers/pow/POWWorker.ts b/shared/js/workers/pow/POWWorker.ts new file mode 100644 index 00000000..4800440b --- /dev/null +++ b/shared/js/workers/pow/POWWorker.ts @@ -0,0 +1,97 @@ +declare namespace WebAssembly { + export function instantiateStreaming(stream: Promise, imports?: any) : Promise; +} + +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, 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); + } +}; \ No newline at end of file diff --git a/shared/js/workers/tsconfig_worker_pow.json b/shared/js/workers/tsconfig_worker_pow.json new file mode 100644 index 00000000..41b6a740 --- /dev/null +++ b/shared/js/workers/tsconfig_worker_pow.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "none", + "target": "es6", + "sourceMap": true, + "outFile": "WorkerPOW.js" + }, + "files": [ + "pow/POWWorker.ts", + ] +} \ No newline at end of file diff --git a/shared/wat/pow/sha1.wasm b/shared/wat/pow/sha1.wasm new file mode 100644 index 00000000..0f96e515 Binary files /dev/null and b/shared/wat/pow/sha1.wasm differ diff --git a/shared/wat/pow/sha1.wat b/shared/wat/pow/sha1.wat new file mode 100644 index 00000000..b3d5f3e0 --- /dev/null +++ b/shared/wat/pow/sha1.wat @@ -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)) + ) +) \ No newline at end of file diff --git a/tools/dtsgen/out.d.ts b/tools/dtsgen/out.d.ts new file mode 100644 index 00000000..d637235b --- /dev/null +++ b/tools/dtsgen/out.d.ts @@ -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; +} diff --git a/vendor/bbcode b/vendor/bbcode index 8f2626cf..8304246b 160000 --- a/vendor/bbcode +++ b/vendor/bbcode @@ -1 +1 @@ -Subproject commit 8f2626cf1e24edca8484545841967525177ef3aa +Subproject commit 8304246b4f651b9de141e4d3252b4f78e5c55391