TeaWeb/shared/js/profiles/identities/TeamSpeakIdentity.ts

969 lines
34 KiB
TypeScript
Raw Normal View History

import {LogCategory, logError, logInfo, logTrace, logWarn} from "../../log";
import * as asn1 from "../../crypto/asn1";
import * as sha from "../../crypto/sha";
2020-03-30 11:44:18 +00:00
import {
AbstractHandshakeIdentityHandler,
HandshakeCommandHandler,
IdentitifyType,
Identity
} from "../../profiles/Identity";
import {arrayBufferBase64, base64_encode_ab, str2ab8} from "../../utils/buffers";
import {AbstractServerConnection} from "../../connection/ConnectionBase";
import {CommandResult} from "../../connection/ServerConnectionDeclaration";
import {HandshakeIdentityHandler} from "../../connection/HandshakeHandler";
2020-03-30 11:44:18 +00:00
export namespace CryptoHelper {
export function base64UrlEncode(str){
2020-03-30 11:44:18 +00:00
return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
export function base64UrlDecode(str: string, pad?: boolean){
2020-03-30 11:44:18 +00:00
if(typeof(pad) === 'undefined' || pad)
str = (str + '===').slice(0, str.length + (str.length % 4));
return str.replace(/-/g, '+').replace(/_/g, '/');
}
export function arraybufferToString(buf) : string {
2020-03-30 11:44:18 +00:00
return String.fromCharCode.apply(null, new Uint16Array(buf));
}
2020-03-30 11:44:18 +00:00
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);
}
2020-03-30 11:44:18 +00:00
*/
2020-03-27 22:36:57 +00:00
2020-03-30 11:44:18 +00:00
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;
}
try { /* Public kex X */
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
const raw = atob(base64UrlDecode(key_data.x, false));
2020-03-30 11:44:18 +00:00
if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
}
2019-01-27 12:11:40 +00:00
2020-03-30 11:44:18 +00:00
for(let i = 0; i < 32; i++)
buffer[index++] = raw.charCodeAt(i);
} catch(error) {
if(error instanceof DOMException)
throw "failed to parse x coordinate (invalid base64)";
throw error;
}
2020-03-27 22:36:57 +00:00
2020-03-30 11:44:18 +00:00
try { /* Public kex Y */
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
const raw = atob(base64UrlDecode(key_data.y, false));
2020-03-30 11:44:18 +00:00
if(raw.charCodeAt(0) > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
}
2020-03-30 11:44:18 +00:00
for(let i = 0; i < 32; i++)
buffer[index++] = raw.charCodeAt(i);
} catch(error) {
if(error instanceof DOMException)
throw "failed to parse y coordinate (invalid base64)";
throw error;
}
if(!public_key) {
try { /* Public kex K */
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
const raw = atob(base64UrlDecode(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);
2019-01-27 12:11:40 +00:00
} catch(error) {
if(error instanceof DOMException)
throw "failed to parse y coordinate (invalid base64)";
throw error;
}
2020-03-30 11:44:18 +00:00
}
2020-03-30 11:44:18 +00:00
buffer[1] = index - 2; /* set the final sequence length */
2020-03-30 11:44:18 +00:00
return base64_encode_ab(buffer.buffer.slice(0, index));
}
const kCryptKey = "b9dfaa7bee6ac57ac7b65f1094a1c155e747327bc2fe5d51c512023fe54a280201004e90ad1daaae1075d53b7d571c30e063b5a62a4a017bb394833aa0983e6e";
2020-03-30 11:44:18 +00:00
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 decryptTeaSpeakIdentity(buffer: Uint8Array) : Promise<string> {
2020-03-30 11:44:18 +00:00
/* 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];
2020-03-30 11:44:18 +00:00
const length = Math.min(buffer.length, 100);
for(let i = 0; i < length; i++)
buffer[i] ^= kCryptKey.charCodeAt(i);
return arraybufferToString(buffer);
2020-03-30 11:44:18 +00:00
}
2020-03-27 22:36:57 +00:00
export async function encryptTeaSpeakIdentity(buffer: Uint8Array) : Promise<string> {
2020-03-30 11:44:18 +00:00
const length = Math.min(buffer.length, 100);
for(let i = 0; i < length; i++)
buffer[i] ^= kCryptKey.charCodeAt(i);
2020-03-30 11:44:18 +00:00
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];
2020-03-30 11:44:18 +00:00
return base64_encode_ab(buffer);
}
2020-03-27 22:36:57 +00:00
2020-03-30 11:44:18 +00:00
/**
* @param buffer base64 encoded ASN.1 string
*/
export function decodeTomCryptKey(buffer: string) {
2020-03-30 11:44:18 +00:00
let decoded;
2020-03-30 11:44:18 +00:00
try {
decoded = asn1.decode(atob(buffer));
} catch(error) {
if(error instanceof DOMException) {
2020-03-30 11:44:18 +00:00
throw "failed to parse key buffer (invalid base64)";
}
2020-03-30 11:44:18 +00:00
throw error;
}
2020-03-30 11:44:18 +00:00
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)
};
2019-01-27 12:11:40 +00:00
2020-03-30 11:44:18 +00:00
if(x.length > 32) {
if(x.charCodeAt(0) != 0) {
2020-03-30 11:44:18 +00:00
throw "Invalid X coordinate! (Too long)";
}
2020-03-30 11:44:18 +00:00
x = x.substr(1);
}
2020-03-30 11:44:18 +00:00
if(y.length > 32) {
if(y.charCodeAt(0) != 0) {
2020-03-30 11:44:18 +00:00
throw "Invalid Y coordinate! (Too long)";
}
2020-03-30 11:44:18 +00:00
y = y.substr(1);
}
2020-03-30 11:44:18 +00:00
if(k.length > 32) {
if(k.charCodeAt(0) != 0) {
2020-03-30 11:44:18 +00:00
throw "Invalid private coordinate! (Too long)";
}
2020-03-30 11:44:18 +00:00
k = k.substr(1);
}
2020-03-30 11:44:18 +00:00
return {
crv: "P-256",
d: base64UrlEncode(btoa(k)),
x: base64UrlEncode(btoa(x)),
y: base64UrlEncode(btoa(y)),
2020-03-30 11:44:18 +00:00
ext: true,
key_ops:["deriveKey", "sign"],
kty:"EC",
};
}
}
export class TeaSpeakHandshakeHandler extends AbstractHandshakeIdentityHandler {
2020-03-30 11:44:18 +00:00
identity: TeaSpeakIdentity;
handler: HandshakeCommandHandler<TeaSpeakHandshakeHandler>;
constructor(connection: AbstractServerConnection, identity: TeaSpeakIdentity) {
super(connection);
this.identity = identity;
this.handler = new HandshakeCommandHandler(connection, this);
this.handler["handshakeidentityproof"] = this.handle_proof.bind(this);
}
executeHandshake() {
2021-04-27 11:30:33 +00:00
this.connection.getCommandHandler().registerHandler(this.handler);
2020-03-30 11:44:18 +00:00
this.connection.send_command("handshakebegin", {
intention: 0,
authentication_method: this.identity.type(),
publicKey: this.identity.publicKey
2020-03-30 11:44:18 +00:00
}).catch(error => {
logError(LogCategory.IDENTITIES, tr("Failed to initialize TeamSpeak based handshake. Error: %o"), error);
2020-03-30 11:44:18 +00:00
if(error instanceof CommandResult)
error = error.extra_message || error.message;
this.trigger_fail("failed to execute begin (" + error + ")");
});
}
2020-03-30 11:44:18 +00:00
private handle_proof(json) {
if(!json[0]["digest"]) {
this.trigger_fail("server too old");
return;
}
2020-03-30 11:44:18 +00:00
this.identity.sign_message(json[0]["message"], json[0]["digest"]).then(proof => {
this.connection.send_command("handshakeindentityproof", {proof: proof}).catch(error => {
logError(LogCategory.IDENTITIES, tr("Failed to proof the identity. Error: %o"), error);
if(error instanceof CommandResult)
error = error.extra_message || error.message;
2020-03-30 11:44:18 +00:00
this.trigger_fail("failed to execute proof (" + error + ")");
}).then(() => this.trigger_success());
2020-08-19 20:55:43 +00:00
}).catch(() => {
2020-03-30 11:44:18 +00:00
this.trigger_fail("failed to sign message");
});
}
2020-03-30 11:44:18 +00:00
protected trigger_fail(message: string) {
2021-04-27 11:30:33 +00:00
this.connection.getCommandHandler().unregisterHandler(this.handler);
2020-03-30 11:44:18 +00:00
super.trigger_fail(message);
}
2020-03-30 11:44:18 +00:00
protected trigger_success() {
2021-04-27 11:30:33 +00:00
this.connection.getCommandHandler().unregisterHandler(this.handler);
2020-03-30 11:44:18 +00:00
super.trigger_success();
}
fillClientInitData(data: any) {
super.fillClientInitData(data);
data["client_key_offset"] = this.identity.hash_number;
}
2020-03-30 11:44:18 +00:00
}
2020-03-30 11:44:18 +00:00
class IdentityPOWWorker {
private _worker: Worker;
private _current_hash: string;
private _best_level: number;
private _initialized = false;
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
async initialize(key: string) {
// @ts-ignore
this._worker = new Worker(new URL("tc-shared/workers/pow", import.meta.url));
2019-02-23 13:15:22 +00:00
2020-03-30 11:44:18 +00:00
/* initialize */
await new Promise<void>((resolve, reject) => {
const timeout_id = setTimeout(() => reject("timeout"), 1000);
2020-03-30 11:44:18 +00:00
this._worker.onmessage = event => {
clearTimeout(timeout_id);
2020-03-30 11:44:18 +00:00
if(!event.data) {
reject("invalid data");
return;
}
2020-03-30 11:44:18 +00:00
if(!event.data.success) {
reject("initialize failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")");
return;
}
2020-03-30 11:44:18 +00:00
this._worker.onmessage = event => this.handle_message(event.data);
resolve();
};
this._worker.onerror = event => {
logError(LogCategory.IDENTITIES, tr("POW Worker error %o"), event);
2020-03-30 11:44:18 +00:00
clearTimeout(timeout_id);
reject("Failed to load worker (" + event.message + ")");
};
});
this._initialized = true;
2020-03-30 11:44:18 +00:00
/* set data */
await new Promise<void>((resolve, reject) => {
this._worker.postMessage({
type: "set_data",
private_key: key,
code: "set_data"
});
2020-03-30 11:44:18 +00:00
const timeout_id = setTimeout(() => reject("timeout (data)"), 1000);
2020-03-30 11:44:18 +00:00
this._worker.onmessage = event => {
clearTimeout(timeout_id);
2020-03-30 11:44:18 +00:00
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"
});
2020-03-27 22:36:57 +00:00
2020-03-30 11:44:18 +00:00
const timeout_id = setTimeout(() => reject("timeout (mine)"), timeout || 5000);
2020-03-30 11:44:18 +00:00
this._worker.onmessage = event => {
this._worker.onmessage = event => this.handle_message(event.data);
2020-03-30 11:44:18 +00:00
clearTimeout(timeout_id);
if (!event.data) {
reject("invalid data");
return;
}
2020-03-30 11:44:18 +00:00
if (!event.data.success) {
reject("mining failed (" + event.data.success + " | " + (event.data.message || "unknown eroror") + ")");
return;
}
2020-03-30 11:44:18 +00:00
if(event.data.result) {
this._best_level = event.data.level;
this._current_hash = event.data.hash;
resolve(true);
} else {
resolve(false); /* no result */
}
};
});
}
2020-03-30 11:44:18 +00:00
current_hash() : string {
return this._current_hash;
}
2020-03-30 11:44:18 +00:00
current_level() : number {
return this._best_level;
}
2020-03-30 11:44:18 +00:00
async finalize(timeout?: number) {
if(this._initialized) {
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);
2020-03-30 11:44:18 +00:00
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) {
logError(LogCategory.IDENTITIES, tr("Failed to finalize POW worker! (%o)"), error);
}
}
2020-03-30 11:44:18 +00:00
this._worker.terminate();
this._worker = undefined;
}
2020-03-30 11:44:18 +00:00
private handle_message(message: any) {
logInfo(LogCategory.IDENTITIES, tr("Received message: %o"), message);
2020-03-30 11:44:18 +00:00
}
}
export class TeaSpeakIdentity implements Identity {
static async generateNew() : Promise<TeaSpeakIdentity> {
2020-03-30 11:44:18 +00:00
let key: CryptoKeyPair;
try {
key = await crypto.subtle.generateKey({name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
} catch(e) {
logError(LogCategory.IDENTITIES, tr("Could not generate a new key: %o"), e);
2020-03-30 11:44:18 +00:00
throw "Failed to generate keypair";
}
const private_key = await CryptoHelper.export_ecc_key(key.privateKey, false);
const identity = new TeaSpeakIdentity(private_key, "0", undefined, false);
await identity.initialize();
return identity;
}
2020-03-30 11:44:18 +00:00
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)";
2020-03-30 11:44:18 +00:00
return {
hash: string.substr(0, V_index),
data: string.substr(V_index + 1),
name: "TeaSpeak user"
}
};
2020-03-30 11:44:18 +00:00
const {hash, data, name} = (!ini ? () => parse_string(ts_string) : () => {
/* parsing with INI structure */
let identity: string, name: string;
2020-03-30 11:44:18 +00:00
for(const line of ts_string.split("\n")) {
if(line.startsWith("identity=")) {
2020-03-30 11:44:18 +00:00
identity = line.substr(9);
} else if(line.startsWith("nickname=")) {
2020-03-30 11:44:18 +00:00
name = line.substr(9);
}
2020-03-30 11:44:18 +00:00
}
2020-03-30 11:44:18 +00:00
if(!identity) throw "missing identity keyword";
const match = identity.match(/^[" ]*([0-9]+V[0-9a-zA-Z+\/]+[=]+)[" \r]*$/);
if(!match) {
logWarn(LogCategory.GENERAL, tr("Identity string '%s' seems to be invalid"), identity);
throw "identity string seems to be in an invalid format";
}
identity = match[1];
2020-03-30 11:44:18 +00:00
if(!identity) throw "invalid identity key value";
2020-03-30 11:44:18 +00:00
const result = parse_string(identity);
result.name = name || result.name;
return result;
})();
2020-03-30 11:44:18 +00:00
if(!ts_string.match(/[0-9]+/g)) throw "invalid hash!";
2020-03-30 11:44:18 +00:00
let buffer;
try {
buffer = new Uint8Array(arrayBufferBase64(data));
} catch(error) {
logError(LogCategory.IDENTITIES, tr("Failed to decode given base64 data (%s)"), data);
2020-03-30 11:44:18 +00:00
throw "failed to base data (base64 decode failed)";
}
const key64 = await CryptoHelper.decryptTeaSpeakIdentity(buffer);
2020-03-30 11:44:18 +00:00
const identity = new TeaSpeakIdentity(key64, hash, name, false);
await identity.initialize();
return identity;
2020-03-27 22:36:57 +00:00
}
2019-01-27 12:11:40 +00:00
2020-03-30 11:44:18 +00:00
hash_number: string; /* hash suffix for the private key */
private_key: string; /* base64 representation of the private key */
_name: string;
publicKey: string; /* only set when initialized */
2020-03-30 11:44:18 +00:00
private _initialized: boolean;
private _crypto_key: CryptoKey;
private _crypto_key_sign: CryptoKey;
2020-03-30 11:44:18 +00:00
private _unique_id: string;
2020-03-30 11:44:18 +00:00
constructor(private_key?: string, hash?: string, name?: string, initialize?: boolean) {
this.private_key = private_key;
this.hash_number = hash || "0";
this._name = name;
2020-03-30 11:44:18 +00:00
if(this.private_key && (typeof(initialize) === "undefined" || initialize)) {
this.initialize().catch(error => {
logError(LogCategory.IDENTITIES, "Failed to initialize TeaSpeakIdentity (%s)", error);
2020-03-30 11:44:18 +00:00
this._initialized = false;
});
}
}
2020-03-30 11:44:18 +00:00
fallback_name(): string | undefined {
return this._name;
}
2020-03-30 11:44:18 +00:00
uid(): string {
return this._unique_id;
}
2020-03-30 11:44:18 +00:00
type(): IdentitifyType {
return IdentitifyType.TEAMSPEAK;
}
2020-03-30 11:44:18 +00:00
valid(): boolean {
return this._initialized && !!this._crypto_key && !!this._crypto_key_sign;
}
2020-03-30 11:44:18 +00:00
async decode(data: string) : Promise<void> {
const json = JSON.parse(data);
if(!json) throw "invalid json";
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;
const clone = await TeaSpeakIdentity.import_ts(key, false);
this.private_key = clone.private_key;
this.hash_number = clone.hash_number;
} else
throw "invalid version";
await this.initialize();
}
2020-03-30 11:44:18 +00:00
encode?() : string {
return JSON.stringify({
key: this.private_key,
hash: this.hash_number,
name: this._name,
version: 2
});
}
2020-03-30 11:44:18 +00:00
async level() : Promise<number> {
if(!this._initialized || !this.publicKey)
2020-03-30 11:44:18 +00:00
throw "not initialized";
const hash = new Uint8Array(await sha.sha1(this.publicKey + this.hash_number));
return TeaSpeakIdentity.calculateLevel(hash);
}
private static calculateLevel(buffer: Uint8Array) : number {
2020-03-30 11:44:18 +00:00
let level = 0;
while(level < buffer.byteLength && buffer[level] === 0)
2020-03-30 11:44:18 +00:00
level++;
if(level >= buffer.byteLength) {
2020-03-30 11:44:18 +00:00
level = 256;
} else {
let byte = buffer[level];
2020-03-30 11:44:18 +00:00
level <<= 3;
while((byte & 0x1) === 0) {
2020-03-30 11:44:18 +00:00
level++;
byte >>= 1;
}
}
2020-03-30 11:44:18 +00:00
return level;
}
2020-03-30 11:44:18 +00:00
/**
* @param {string} a
* @param {string} b
* @description b must be smaller (in bytes) then a
*/
private static string_add(a: string, b: string) {
2020-03-30 11:44:18 +00:00
const char_result: number[] = [];
const char_a = [...a].reverse().map(e => e.charCodeAt(0));
const char_b = [...b].reverse().map(e => e.charCodeAt(0));
2020-03-30 11:44:18 +00:00
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);
}
2020-03-30 11:44:18 +00:00
while(char_a.length > 0) {
let result = char_a.pop_front() + (carry ? 1 : 0);
if((carry = result > 57))
result -= 10;
char_result.push(result);
}
2020-03-30 11:44:18 +00:00
if(carry)
char_result.push(49);
2020-03-30 11:44:18 +00:00
return String.fromCharCode.apply(null, char_result.slice().reverse());
}
2020-03-30 11:44:18 +00:00
async improve_level_for(time: number, threads: number) : Promise<Boolean> {
let active = true;
setTimeout(() => active = false, time);
return await this.improveLevelNative(-1, threads, () => active);
2020-03-30 11:44:18 +00:00
}
async improveLevelNative(target: number, threads: number, active_callback: () => boolean, callback_level?: (current: number) => any, callback_status?: (hash_rate: number) => any) : Promise<Boolean> {
if(!this._initialized || !this.publicKey) {
2020-03-30 11:44:18 +00:00
throw "not initialized";
}
/* get the highest level possible */
if(target == -1) {
2020-03-30 11:44:18 +00:00
target = 0;
} else if(target <= await this.level()) {
2020-03-30 11:44:18 +00:00
return true;
}
2020-03-30 11:44:18 +00:00
const workers: IdentityPOWWorker[] = [];
2020-03-30 11:44:18 +00:00
const iterations = 100000;
let current_hash;
const next_hash = () => {
if(!current_hash) {
2020-03-30 11:44:18 +00:00
return (current_hash = this.hash_number);
}
2020-03-30 11:44:18 +00:00
if(current_hash.length < iterations.toString().length) {
current_hash = TeaSpeakIdentity.string_add(iterations.toString(), current_hash);
2020-03-30 11:44:18 +00:00
} else {
current_hash = TeaSpeakIdentity.string_add(current_hash, iterations.toString());
2020-03-30 11:44:18 +00:00
}
return current_hash;
};
try {
{ /* 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.publicKey));
}
try {
await Promise.all(initialize_promise);
} catch (error) {
logError(LogCategory.IDENTITIES, error);
throw "failed to initialize";
}
2020-03-30 11:44:18 +00:00
}
let result = false;
let best_level = 0;
let target_level = target > 0 ? target : await this.level() + 1;
const worker_promise: Promise<void>[] = [];
const hash_timestamps: number[] = [];
let last_hashrate_update: number = 0;
const update_hashrate = () => {
if (!callback_status) return;
const now = Date.now();
hash_timestamps.push(now);
if (last_hashrate_update + 1000 < now) {
last_hashrate_update = now;
const timeout = now - 10 * 1000; /* 10s */
const rounds = hash_timestamps.filter(e => e > timeout);
callback_status(Math.ceil((rounds.length * iterations) / Math.ceil((now - rounds[0]) / 1000)))
}
};
try {
result = await new Promise<boolean>((resolve, reject) => {
let active = true;
const exit = () => {
const timeout = setTimeout(() => resolve(true), 1000);
2020-08-19 20:55:43 +00:00
Promise.all(worker_promise).then(() => {
clearTimeout(timeout);
resolve(true);
2020-08-19 20:55:43 +00:00
}).catch(() => 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 => {
update_hashrate();
worker_promise.remove(p);
if (result.valueOf()) {
if (worker.current_level() > best_level) {
this.hash_number = worker.current_hash();
logInfo(LogCategory.IDENTITIES, "Found new best at %s (%d). Old was %d", this.hash_number, worker.current_level(), best_level);
best_level = worker.current_level();
if (callback_level)
callback_level(best_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);
logWarn(LogCategory.IDENTITIES, "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
}
return result;
} finally {
/* shutdown */
2020-03-30 11:44:18 +00:00
const finalize_promise: Promise<void>[] = [];
for(const worker of workers)
finalize_promise.push(worker.finalize(250));
try {
2020-03-30 11:44:18 +00:00
await Promise.all(finalize_promise);
} catch(error) {
logError(LogCategory.IDENTITIES, "Failed to shutdown worker: %o", error);
}
2020-03-30 11:44:18 +00:00
}
throw "this should never be reached";
2020-03-30 11:44:18 +00:00
}
2020-03-27 22:36:57 +00:00
/* Improve the identity within the current thread */
async improveLevelJavascript(target: number, activeCallback: () => boolean) : Promise<number> {
const publicKey = str2ab8(this.publicKey);
const buffer = new Uint8Array(publicKey.byteLength + 20); /* Max 20 append digest (Appendix is a number in range of [0;2^64]) -> log10(2^64) */
buffer.set(new Uint8Array(publicKey));
buffer.set(new Uint8Array(str2ab8(this.hash_number)), publicKey.byteLength);
const kChar9 = '9'.charCodeAt(0);
const kChar0 = '0'.charCodeAt(0);
let numberIndex = publicKey.byteLength + this.hash_number.length;
let bufferView = buffer.subarray(0, numberIndex);
const incrementCounter = () => {
let currentIndex = numberIndex - 1;
while(currentIndex > publicKey.byteLength && buffer[currentIndex] == kChar9) {
buffer[currentIndex--] = kChar0;
}
if(currentIndex > publicKey.byteLength) {
buffer[currentIndex]++;
} else {
/* yeah a new diget */
if(numberIndex >= buffer.byteLength) {
throw "hash number got too big. use another identity";
}
buffer[numberIndex] = kChar0;
numberIndex++;
buffer[currentIndex] = '1'.charCodeAt(0);
bufferView = buffer.subarray(0, numberIndex);
}
};
let currentLevel = await this.level();
let iteration = 0;
const timeBegin = Date.now();
while(currentLevel < target) {
if((iteration++ % 1000) === 0 && !activeCallback()) {
break;
}
incrementCounter();
const newLevel = await TeaSpeakIdentity.calculateLevel(new Uint8Array(await crypto.subtle.digest("SHA-1", bufferView)));
if(newLevel > currentLevel) {
this.hash_number = CryptoHelper.arraybufferToString(buffer.subarray(publicKey.byteLength, numberIndex));
logTrace(LogCategory.IDENTITIES, tr("Found a new identity level at %s. Previous level %d now %d (%d hashes/second)"),
this.hash_number, currentLevel, newLevel, iteration * 1000 / (Date.now() - timeBegin));
currentLevel = newLevel;
}
}
return currentLevel;
}
2020-03-30 11:44:18 +00:00
private async initialize() {
if(!this.private_key)
throw "Invalid private key";
let jwk: any;
try {
jwk = await CryptoHelper.decodeTomCryptKey(this.private_key);
2020-03-30 11:44:18 +00:00
if(!jwk)
throw tr("result undefined");
2020-03-30 11:44:18 +00:00
} catch(error) {
throw "failed to parse key (" + error + ")";
}
2020-03-30 11:44:18 +00:00
try {
this._crypto_key_sign = await crypto.subtle.importKey("jwk", jwk, {name:'ECDSA', namedCurve: 'P-256'}, false, ["sign"]);
} catch(error) {
logError(LogCategory.IDENTITIES, error);
2020-03-30 11:44:18 +00:00
throw "failed to create crypto sign key";
}
2020-03-30 11:44:18 +00:00
try {
this._crypto_key = await crypto.subtle.importKey("jwk", jwk, {name:'ECDH', namedCurve: 'P-256'}, true, ["deriveKey"]);
} catch(error) {
logError(LogCategory.IDENTITIES, error);
2020-03-30 11:44:18 +00:00
throw "failed to create crypto key";
}
2020-03-30 11:44:18 +00:00
try {
this.publicKey = await CryptoHelper.export_ecc_key(this._crypto_key, true);
this._unique_id = base64_encode_ab(await sha.sha1(this.publicKey));
2020-03-30 11:44:18 +00:00
} catch(error) {
logError(LogCategory.IDENTITIES, error);
2020-03-30 11:44:18 +00:00
throw "failed to calculate unique id";
}
2020-03-30 11:44:18 +00:00
this._initialized = true;
}
2020-03-30 11:44:18 +00:00
async export_ts(ini?: boolean) : Promise<string> {
if(!this.private_key)
throw "Invalid private key";
const identity = this.hash_number + "V" + await CryptoHelper.encryptTeaSpeakIdentity(new Uint8Array(str2ab8(this.private_key)));
2020-03-30 11:44:18 +00:00
if(!ini) return identity;
2020-03-30 11:44:18 +00:00
return "[Identity]\n" +
"id=TeaWeb-Exported\n" +
"identity=\"" + identity + "\"\n" +
"nickname=\"" + this.fallback_name() + "\"\n" +
"phonetic_nickname=";
}
2020-03-27 22:36:57 +00:00
2020-03-30 11:44:18 +00:00
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;
2020-03-27 22:36:57 +00:00
}
2020-03-30 11:44:18 +00:00
for(let i = 0; i < 32; i++)
buffer[index++] = sign[i];
}
2020-03-30 11:44:18 +00:00
{ /* integer s */
buffer[index++] = 0x02; /* type */
buffer[index++] = 0x20; /* length */
if(sign[32] > 0x7F) {
buffer[index - 1] += 1;
buffer[index++] = 0;
}
2020-03-27 22:36:57 +00:00
2020-03-30 11:44:18 +00:00
for(let i = 0; i < 32; i++)
buffer[index++] = sign[32 + i];
}
2020-03-30 11:44:18 +00:00
buffer[1] = index - 2;
return base64_encode_ab(buffer.subarray(0, index));
}
spawn_identity_handshake_handler(connection: AbstractServerConnection): HandshakeIdentityHandler {
return new TeaSpeakHandshakeHandler(connection, this);
}
}