UNPKG

heimdall-tide

Version:

SDK for communicating with a Tide Enclave

440 lines 18.1 kB
import { Heimdall, windowType } from "../heimdall"; import { Models, Tools } from "@tideorg/js"; const TideMemory = Tools.TideMemory; const BaseTideRequest = Models.BaseTideRequest; var State; (function (State) { State[State["Closed"] = 0] = "Closed"; State[State["InitializingIFrame"] = 1] = "InitializingIFrame"; State[State["FailedIFrameInitialization"] = 2] = "FailedIFrameInitialization"; State[State["InitializingPopUp"] = 3] = "InitializingPopUp"; State[State["FailedPopUpInitialization"] = 4] = "FailedPopUpInitialization"; State[State["Ready"] = 5] = "Ready"; })(State || (State = {})); export class RequestEnclave extends Heimdall { constructor() { super(...arguments); this.name = "request"; this.state = State.Closed; this._windowType = windowType.Hidden; this.initDone = this.recieve("init done"); } init(data) { if (!data.doken) throw 'Doken not provided'; if (data.backgroundUrl) this.bgUrl = data.backgroundUrl; if (data.logoUrl) this.logoUrl = data.logoUrl; this.doken = data.doken; let parsedDoken = decodeToken(this.doken); if (parsedDoken["t.uho"]) this.enclaveOrigin = parsedDoken["t.uho"]; // use tidecloak set user home ork from doken this.dokenRefreshCallback = data.dokenRefreshCallback; this.requireReloginCallback = data.requireReloginCallback; this.handleSessionCheck(); if (this.state !== State.Closed) return; // someone has already called init if (this._windowType === windowType.Hidden) { this.recieve("hidden enclave").then((data) => this.handleHiddenEnclaveResponse(data)); this.checkEnclaveOpen(); // try iframe immediately } return this; } async handleHiddenEnclaveResponse(msg) { // Below is the session key mismatch flow that was implemented but then it was decided a basic relogin was more elegent // Keeping it though because it is nearly identical to the flow where a tide user delegates a token to another tide user // This would require the second tide user to sign a new delegated token with their current session key // This would be gold in a cvk scenario // if(msg == "session key mismatch" && this._windowType == windowType.Hidden){ // this.initDone = this.recieve("init done"); // await the REOPENED HIDDEN ENCLAVE INIT DONE SIGNAL // // looks like the hidden iframe has not allowed data to be stored on the browser OR the session key is mismatched with whats on the enclave vs doken // // either way we gotta get a doken with the appropriate session key // // Close the hidden enclave // this.close(); // // We're now going to open the request enclave as a popup with the mismatched doken // // The page should recognise the doken is mismatched, generate a new one, then close // this._windowType = windowType.Popup; // // open popup // await this.open(); // // send doken to refresh // this.send({ // type: "init", // message:{ // doken: this.doken // } // }); // // wait for new doken // const resp = await this.recieve("refreshed doken"); // this.doken = resp.doken; // if(this.requireReloginCallback) this.requireReloginCallback(); // // close pop up enclave // this.close(); // // reset page to hidden iframe // this._windowType = windowType.Hidden; // // open hidden iframe // this.open().then((success: boolean) => { // if(success){ // this.send({ // type: "init", // message: { // doken: this.doken // } // }); // }else throw 'Error opening enclave'; // }); // } if (msg == "session key mismatch") { this.close(); console.log(`[HEIMDALL] Session key mismatch between enclave and doken. Reinitiating login`); this.requireReloginCallback(); // should initiate a full client page reload, killing this this.state = State.Closed; } else if (msg == "storage issue") { // Convert hidden enclave into popup this.close(); console.log(`[HEIMDALL] Storage issue found on hidden iframe. Trying popup window next`); this.state = State.FailedIFrameInitialization; this.checkEnclaveOpen(); } this.recieve("hidden enclave").then((data) => this.handleHiddenEnclaveResponse(data)); } getOrkUrl() { // construct ork url const url = new URL(this.enclaveOrigin); // Set hidden status url.searchParams.set("hidden", this._windowType == windowType.Hidden ? "true" : "false"); // Set vendor public url.searchParams.set("vendorId", this.vendorId); // Set client origin url.searchParams.set("origin", encodeURIComponent(window.location.origin)); // Set client origin signature (by vendor) url.searchParams.set("originsig", encodeURIComponent(this.signed_client_origin)); // Set voucher url url.searchParams.set("voucherURL", encodeURIComponent(this.voucherURL)); // Set requestsed enclave url.searchParams.set("type", this.name); this.doken; if (this.bgUrl) url.searchParams.set("backgroundUrl", this.bgUrl); if (this.logoUrl) url.searchParams.set("logoUrl", this.logoUrl); return url; } checkEnclaveOpen() { if (this.enclaveClosed()) { switch (this.state) { case State.InitializingIFrame: case State.InitializingPopUp: return; case State.Closed: // if closed try iframe if (this._windowType === windowType.Hidden) this.state = State.InitializingIFrame; else if (this._windowType === windowType.Popup) this.state = State.InitializingPopUp; break; case State.FailedIFrameInitialization: // if failed iframe try popup this.state = State.InitializingPopUp; this._windowType = windowType.Popup; break; case State.FailedPopUpInitialization: // if failed popup give up console.warn("[HEIMDALL] Failed to initialize the popup enclave"); window.alert("There was an issue opening the fallback popup on this page. Please enable popups or let the administrator know about this problem. For more information, visit https://tide.org/browserwindow"); return; case State.Ready: // if ready (but no enclave) try popup again console.error("[HEIMDALL] State says ready but no enclave open. Trying popup again"); this.state = State.InitializingPopUp; this._windowType = windowType.Popup; break; } // Enclave is closed! // We need to reopen the enclave and await the init again this.initDone = this.recieve("init done"); console.log(`[HEIMDALL] Attempting to open ${windowType[this._windowType]} window`); this.open().then((success) => { if (success) { const session = new Uint32Array(5); self.crypto.getRandomValues(session); this.sessionId = Array.from(session, v => v.toString(16).padStart(8, '0')).join(''); this.send({ type: "init", message: { doken: this.doken, sessionId: this.sessionId } }); console.log(`[HEIMDALL] Successfully opened ${windowType[this._windowType]} window`); this.state = State.Ready; } else { if (this.state === State.InitializingIFrame) { this.state = State.FailedIFrameInitialization; console.error('Error opening enclave of type: ' + windowType[this._windowType] + '. Trying popup.'); this.checkEnclaveOpen(); } else if (this.state === State.InitializingPopUp) { this.state = State.FailedIFrameInitialization; // so next checkEnclaveOpen call tries popup again console.error('Error opening enclave of type: ' + windowType[this._windowType]); } } }); } } async initializeRequest(request) { // construct request to sign this request's creation const requestToInitialize = BaseTideRequest.decode(request); const requestToInitializeDetails = await requestToInitialize.getRequestInitDetails(); const initRequest = new BaseTideRequest("TideRequestInitialization", "1", "Doken:1", TideMemory.CreateFromArray([ requestToInitializeDetails.creationTime, requestToInitializeDetails.expireTime, requestToInitializeDetails.modelId, requestToInitializeDetails.draftHash ]), new TideMemory()); const creationSig = (await this.execute(initRequest.encode()))[0]; // returns the same request provided except with the policy authorized creation datas included return requestToInitialize.addCreationSignature(requestToInitializeDetails.creationTime, creationSig).encode(); } async execute(data, waitForAll = false) { this.checkEnclaveOpen(); await this.initDone; const pre_resp = this.recieve("sign request completed"); this.send({ type: "request", message: { flow: "sign", request: data, waitForAll, } }); const resp = await pre_resp; if (typeof resp.error === 'string') { throw Error(resp.error); } if (!Array.isArray(resp)) throw 'Expecting request completed data to be an array, not' + resp; if (!resp.every((d) => d instanceof Uint8Array)) throw 'Expecting all entries in response to be Uint8Arrays'; return resp; } async decrypt(data, policy) { this.checkEnclaveOpen(); await this.initDone; const pre_resp = this.recieve("decrypt request completed"); this.send({ type: "request", message: { flow: policy ? "policy decrypt" : "decrypt", request: data, policy: policy } }); const resp = await pre_resp; if (typeof resp.error === 'string') { throw Error(resp.error); } if (!Array.isArray(resp)) throw 'Expecting request completed data to be an array, not' + resp; if (!resp.every((d) => d instanceof Uint8Array)) throw 'Expecting all entries in response to be Uint8Arrays'; return resp; } async encrypt(data, policy) { this.checkEnclaveOpen(); await this.initDone; const pre_resp = this.recieve("encrypt request completed"); this.send({ type: "request", message: { flow: policy ? "policy encrypt" : "encrypt", request: data, policy: policy } }); const resp = await pre_resp; if (typeof resp.error === 'string') { throw Error(resp.error); } if (!Array.isArray(resp)) throw 'Expecting request completed data to be an array, not' + resp; if (!resp.every((d) => d instanceof Uint8Array)) throw 'Expecting all entries in response to be Uint8Arrays'; return resp; } async draftEncryption(data) { this.checkEnclaveOpen(); await this.initDone; const pre_resp = this.recieve("encrypt request completed"); this.send({ type: "request", message: { flow: "policy encrypt", request: data, } }); const resp = await pre_resp; if (typeof resp.error === 'string') { throw Error(resp.error); } if (!(resp instanceof Uint8Array)) throw `Expecting response type to be Uint8Array`; return resp; } async draftDecryption(data) { this.checkEnclaveOpen(); await this.initDone; const pre_resp = this.recieve("decrypt request completed"); this.send({ type: "request", message: { flow: "policy decrypt", request: data, } }); const resp = await pre_resp; if (typeof resp.error === 'string') { throw Error(resp.error); } if (!(resp instanceof Uint8Array)) throw `Expecting response type to be Uint8Array`; return resp; } async commitEncryption(readyEncryptionRequest, policy) { this.checkEnclaveOpen(); await this.initDone; const pre_resp = this.recieve("commit encrypt request completed"); this.send({ type: "request", message: { flow: "commit encrypt", request: readyEncryptionRequest, policy: policy } }); const resp = await pre_resp; if (typeof resp.error === 'string') { throw Error(resp.error); } if (!Array.isArray(resp)) throw 'Expecting request completed data to be an array, not' + resp; if (!resp.every((d) => d instanceof Uint8Array)) throw 'Expecting all entries in response to be Uint8Arrays'; return resp; } async commitDecryption(readyDecryptionRequest, policy) { this.checkEnclaveOpen(); await this.initDone; const pre_resp = this.recieve("commit decrypt request completed"); this.send({ type: "request", message: { flow: "commit decrypt", request: readyDecryptionRequest, policy: policy } }); const resp = await pre_resp; if (typeof resp.error === 'string') { throw Error(resp.error); } if (!Array.isArray(resp)) throw 'Expecting request completed data to be an array, not' + resp; if (!resp.every((d) => d instanceof Uint8Array)) throw 'Expecting all entries in response to be Uint8Arrays'; return resp; } async updateDoken(doken) { this.doken = doken; this.send({ type: "doken refresh", message: { doken: this.doken } }); } async handleSessionCheck() { this.recieve("session check", true).then(() => this.handleSessionCheck()); this.send({ type: "current session", message: this.sessionId }); } async onerror(data) { if (typeof data.message === "string") { switch (data.message) { case "expired": if (!this.dokenRefreshCallback) { console.error("[HEIMDALL] Doken on enclave has expired but there is no Doken Refresh Callback registered"); return; } console.log("[HEIMDALL] Refreshing doken"); this.doken = await this.dokenRefreshCallback(); this.send({ type: "doken refresh", message: { doken: this.doken } }); break; default: this.close(); throw new Error("[HEIMDALL] Recieved enclave error: " + data.message); } } } } function decodeToken(token) { const [header, payload] = token.split("."); if (typeof payload !== "string") { throw new Error("Unable to decode token, payload not found."); } let decoded; try { decoded = base64UrlDecode(payload); } catch (error) { throw new Error("Unable to decode token, payload is not a valid Base64URL value. Error: " + error); } try { return JSON.parse(decoded); } catch (error) { throw new Error("Unable to decode token, payload is not a valid JSON value. Error: " + error); } } function base64UrlDecode(input) { let output = input .replace(/-/g, "+") .replace(/_/g, "/"); switch (output.length % 4) { case 0: break; case 2: output += "=="; break; case 3: output += "="; break; default: throw new Error("Input is not of the correct length."); } try { return b64DecodeUnicode(output); } catch (error) { return atob(output); } } function b64DecodeUnicode(input) { return decodeURIComponent(atob(input).replace(/(.)/g, (m, p) => { let code = p.charCodeAt(0).toString(16).toUpperCase(); if (code.length < 2) { code = "0" + code; } return "%" + code; })); } //# sourceMappingURL=RequestEnclave.js.map