heimdall-tide
Version:
SDK for communicating with a Tide Enclave
440 lines • 18.1 kB
JavaScript
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