UNPKG

heimdall-tide

Version:

SDK for communicating with a Tide Enclave

344 lines (302 loc) 12.4 kB
import { BaseTideRequest } from "asgard-tide"; import { Heimdall, HiddenInit, windowType } from "../heimdall"; import { TideMemory } from "asgard-tide"; export class RequestEnclave extends Heimdall<RequestEnclave>{ name: string = "request"; protected doken: string; protected dokenRefreshCallback: () => Promise<string> | undefined; protected requireReloginCallback: () => Promise<string>; _windowType: windowType = windowType.Hidden; protected initDone: Promise<any> = this.recieve("init done"); init(data: HiddenInit): RequestEnclave { if(!data.doken) throw 'Doken not provided'; 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.recieve("hidden enclave").then((data) => this.handleHiddenEnclaveResponse(data)); this.open().then((success: boolean) => { if(success){ this.send({ type: "init", message: { doken: this.doken } }); }else{ // If injecting iframe fails, try setting it as a popup and opening it this._windowType = windowType.Popup; this.open().then((success: boolean) => { if(success){ this.send({ type: "init", message: { doken: this.doken } }); } else throw 'Error opening all types of Request Enclave'; }); } }); return this; } async handleHiddenEnclaveResponse(msg: any){ // 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(); this.requireReloginCallback(); // should initiate a full client page reload, killing this } else if(msg == "storage issue"){ // Convert hidden enclave into popup this.close(); this._windowType = windowType.Popup; this.open().then((success: boolean) => { if(success){ this.send({ type: "init", message: { doken: this.doken } }); }else{ 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"); throw "Could not open popup"; } }); } this.recieve("hidden enclave").then((data) => this.handleHiddenEnclaveResponse(data)); } getOrkUrl(): URL { // 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); return url; } checkEnclaveOpen(){ if(this.enclaveClosed()){ // Enclave was closed! // We need to reopen the enclave and await the init again this.initDone = this.recieve("init done"); this.open().then((success: boolean) => { if(success){ this.send({ type: "init", message:{ doken: this.doken } }); }else throw 'Error opening enclave'; }); } } async initializeRequest(request: TideMemory): Promise<Uint8Array>{ // 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: TideMemory): Promise<Uint8Array[]>{ this.checkEnclaveOpen(); await this.initDone; const pre_resp = this.recieve("sign request completed"); this.send({ type: "request", message:{ flow: "sign", request: data, } }) const resp = await pre_resp; if(!Array.isArray(resp)) throw 'Expecting request completed data to be an array, not' + resp; if(!resp.every((d: any) => d instanceof Uint8Array)) throw 'Expecting all entries in response to be Uint8Arrays'; return resp; } async decrypt(data: decryptRequest): Promise<Uint8Array[]>{ this.checkEnclaveOpen(); await this.initDone; const pre_resp = this.recieve("decrypt request completed"); this.send({ type: "request", message:{ flow: "decrypt", request: data } }) const resp = await pre_resp; if(!Array.isArray(resp)) throw 'Expecting request completed data to be an array, not' + resp; if(!resp.every((d: any) => d instanceof Uint8Array)) throw 'Expecting all entries in response to be Uint8Arrays'; return resp; } async encrypt(data: encryptRequest): Promise<Uint8Array[]>{ this.checkEnclaveOpen(); await this.initDone; const pre_resp = this.recieve("encrypt request completed"); this.send({ type: "request", message: { flow: "encrypt", request: data } }) const resp = await pre_resp; if(!Array.isArray(resp)) throw 'Expecting request completed data to be an array, not' + resp; if(!resp.every((d: any) => d instanceof Uint8Array)) throw 'Expecting all entries in response to be Uint8Arrays'; return resp; } async updateDoken(doken: string){ this.doken = doken; this.send({ type: "doken refresh", message:{ doken: this.doken } }); } async onerror(data: any) { 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: string): any { 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: string): string { 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: string): string { 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; })); } interface decryptRequest{ encrypted: Uint8Array; tags: string[] } interface encryptRequest{ data: Uint8Array; tags: string[] }