diff --git a/src/allPages.ts b/src/allPages.ts index fa03bf7..a692452 100644 --- a/src/allPages.ts +++ b/src/allPages.ts @@ -15,6 +15,7 @@ import { SetVaultURLPage } from "./pages/SetVaultURL"; import { TOTPViewPage } from "./pages/TOTP/TOTPView"; import { TransitDecryptPage } from "./pages/Transit/TransitDecrypt"; import { TransitEncryptPage } from "./pages/Transit/TransitEncrypt"; +import { TransitRewrapPage } from "./pages/Transit/TransitRewrap"; import { TransitViewPage } from "./pages/Transit/TransitView"; import { TransitViewSecretPage } from "./pages/Transit/TransitViewSecret"; import { UnsealPage } from "./pages/Unseal"; @@ -36,6 +37,7 @@ export const allPages: pagesList = { TRANSIT_VIEW_SECRET: new TransitViewSecretPage(), TRANSIT_ENCRYPT: new TransitEncryptPage(), TRANSIT_DECRYPT: new TransitDecryptPage(), + TRANSIT_REWRAP: new TransitRewrapPage(), KEY_VALUE_VIEW: new KeyValueViewPage(), KEY_VALUE_SECRET: new KeyValueSecretPage(), KEY_VALUE_VERSIONS: new KeyValueVersionsPage(), diff --git a/src/api/transit/transitRewrap.ts b/src/api/transit/transitRewrap.ts new file mode 100644 index 0000000..5175578 --- /dev/null +++ b/src/api/transit/transitRewrap.ts @@ -0,0 +1,34 @@ +import { appendAPIURL, getHeaders } from "../apiUtils"; +import { removeDoubleSlash } from "../../utils"; + +type RewrapResult = { + ciphertext: string; +} + +type RewrapPayload = { + ciphertext: string; + key_version?: number; +} + +export async function transitRewrap( + baseMount: string, + name: string, + payload: RewrapPayload +): Promise { + const request = new Request(appendAPIURL(removeDoubleSlash(`/v1/${baseMount}/rewrap/${name}`)), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...getHeaders(), + }, + body: JSON.stringify(payload) + }); + const response = await fetch(request); + if (!response.ok) { + const json = await response.json(); + throw new Error(json.errors[0]); + } else { + const json = await response.json(); + return json.data; + } +} diff --git a/src/api/types/transit.ts b/src/api/types/transit.ts index 074103c..6004787 100644 --- a/src/api/types/transit.ts +++ b/src/api/types/transit.ts @@ -24,7 +24,7 @@ export type TransitKeyBaseType = { // Type returned when calling getTransitKey export type TransitKeyType = TransitKeyBaseType & { keys: { - [version: string]: number; + [version: number]: number; }; min_decryption_version: number; min_encryption_version: number; diff --git a/src/elements/Option.ts b/src/elements/Option.ts new file mode 100644 index 0000000..0a5fad7 --- /dev/null +++ b/src/elements/Option.ts @@ -0,0 +1,12 @@ +import { makeElement } from "../htmlUtils"; + +export function Option(label: string, value: string): HTMLElement { + return makeElement({ + tag: "option", + text: label, + attributes: { + label: label, + value: value, + } + }) +} \ No newline at end of file diff --git a/src/pages/PwGen.ts b/src/pages/PwGen.ts index e1d971e..f0c72c3 100644 --- a/src/pages/PwGen.ts +++ b/src/pages/PwGen.ts @@ -4,6 +4,7 @@ import { Page } from "../types/Page"; import { makeElement } from "../htmlUtils"; import { setPageContent } from "../pageUtils"; import i18next from 'i18next'; +import { Option } from "../elements/Option"; const passwordLengthMin = 1; const passwordLengthMax = 64; @@ -43,17 +44,6 @@ function genPassword(options = passwordOptionsDefault) { return pw; } -function Option(label, value) { - return makeElement({ - tag: "option", - text: label, - attributes: { - label: label, - value: value, - } - }) -} - export class PwGenPage extends Page { constructor() { super(); diff --git a/src/pages/Transit/TransitRewrap.ts b/src/pages/Transit/TransitRewrap.ts new file mode 100644 index 0000000..19fa709 --- /dev/null +++ b/src/pages/Transit/TransitRewrap.ts @@ -0,0 +1,117 @@ +import { CopyableModal } from "../../elements/CopyableModal"; +import { FileUploadInput } from "../../elements/FileUploadInput"; +import { Margin } from "../../elements/Margin"; +import { Page } from "../../types/Page"; +import { changePage, setErrorText, setPageContent, setTitleElement } from "../../pageUtils"; +import { makeElement } from "../../htmlUtils"; +import { pageState } from "../../globalPageState"; +import UIkit from 'uikit/dist/js/uikit.min.js'; +import i18next from "i18next"; +import { getTransitKey } from "../../api/transit/getTransitKey"; +import { objectToMap } from "../../utils"; +import { Option } from "../../elements/Option"; +import { transitRewrap } from "../../api/transit/transitRewrap"; + +type versionOption = { version: string; label: string } + +export class TransitRewrapPage extends Page { + constructor() { + super(); + } + + goBack(): void { + changePage("TRANSIT_VIEW_SECRET"); + } + + transitRewrapForm: HTMLFormElement; + + async render(): Promise { + setTitleElement(pageState); + let transitKey = await getTransitKey(pageState.currentBaseMount, pageState.currentSecret); + + let stringVersions = Array.from(objectToMap(transitKey.keys).keys()).reverse() as any as string[]; + let versions = stringVersions.map((val) => parseInt(val, 10)) as any as number[]; + + // get the selectable version options in the same + // format the official UI uses. + // e.g: ["2 (latest)", "1"] + + let options: versionOption[] = versions.map((val): versionOption => { + let i18nkey = val == Math.max(...versions) ? + "transit_rewrap_latest_version_option_text" + : + "transit_rewrap_version_option_text"; + return { + version: String(val), + label: i18next.t(i18nkey, { version_num: String(val) }), + } + }) + + setPageContent(""); + this.transitRewrapForm = makeElement({ + tag: "form", + children: [ + makeElement({ + tag: "select", + name: "version", + class: ["uk-select", "uk-width-1-2"], + children: options.map((option): HTMLElement => Option(option.label, option.version)) + }), + Margin(makeElement({ + tag: "textarea", + class: ["uk-textarea", "uk-width-1-2"], + attributes: { + placeholder: i18next.t("transit_rewrap_input_placeholder"), + name: "ciphertext", + } + })), + makeElement({ + tag: "p", + id: "errorText", + class: "uk-text-danger" + }), + makeElement({ + tag: "button", + class: ["uk-button", "uk-button-primary"], + text: i18next.t("transit_rewrap_rewrap_btn"), + attributes: { + type: "submit", + } + }) + ] + }) as HTMLFormElement; + setPageContent(this.transitRewrapForm); + this.transitRewrapForm.addEventListener("submit", async function (e: Event) { + e.preventDefault(); + await this.transitRewrapFormHandler(); + }.bind(this)); + } + + async transitRewrapFormHandler(): Promise { + const formData = new FormData(this.transitRewrapForm); + const ciphertext = formData.get("ciphertext") as string; + try { + let res = await transitRewrap( + pageState.currentBaseMount, + pageState.currentSecret, + { + ciphertext: formData.get("ciphertext") as string, + key_version: parseInt(formData.get("version") as string, 10), + } + ); + const modal = CopyableModal(i18next.t("transit_rewrap_result_modal_title"), res.ciphertext); + document.body.querySelector("#pageContent").appendChild(modal); + UIkit.modal(modal).show(); + } catch (e) { + setErrorText(`API Error: ${e.message}`); + } + } + + get titleSuffix(): string { + return i18next.t("transit_rewrap_suffix"); + } + + get name(): string { + return i18next.t("transit_rewrap_title"); + } +} diff --git a/src/pages/Transit/TransitViewSecret.ts b/src/pages/Transit/TransitViewSecret.ts index 5464216..60498f6 100644 --- a/src/pages/Transit/TransitViewSecret.ts +++ b/src/pages/Transit/TransitViewSecret.ts @@ -41,6 +41,14 @@ export class TransitViewSecretPage extends Page { iconText: i18next.t("transit_view_decrypt_icon_text"), onclick: () => { changePage("TRANSIT_DECRYPT"); } }), + Tile({ + condition: transitKey.supports_decryption, + title: i18next.t("transit_view_rewrap_text"), + description: i18next.t("transit_view_rewrap_description"), + icon: "code", + iconText: i18next.t("transit_view_rewrap_icon_text"), + onclick: () => { changePage("TRANSIT_REWRAP"); } + }), ] })); } diff --git a/src/translations/en.js b/src/translations/en.js index 78df1ca..bdc4bd0 100644 --- a/src/translations/en.js +++ b/src/translations/en.js @@ -133,6 +133,9 @@ module.exports = { "transit_view_decrypt_text": "Decrypt", "transit_view_decrypt_description": "Decrypt some cyphertext.", "transit_view_decrypt_icon_text": "Decryption Icon", + "transit_view_rewrap_text": "Rewrap", + "transit_view_rewrap_description": "Rewrap ciphertext using a different key version.", + "transit_view_rewrap_icon_text": "Rewrap Icon", // Transit Encrypt Page "transit_encrypt_title": "Transit Encrypt", @@ -142,11 +145,20 @@ module.exports = { "transit_encrypt_encrypt_btn": "Encrypt", "transit_encrypt_encryption_result_modal_title": "Encryption Result", - // Transit decrypt Page + // Transit Decrypt Page "transit_decrypt_title": "Transit Decrypt", "transit_decrypt_suffix": " (decrypt)", "transit_decrypt_input_placeholder": "Cyphertext", "transit_decrypt_decode_checkbox": "Should the plaintext be base64 decoded?", "transit_decrypt_decrypt_btn": "Decrypt", "transit_decrypt_decryption_result_modal_title": "Decryption Result", + + // Transit Rewrap Page + "transit_rewrap_title": "Transit Rewrap", + "transit_rewrap_suffix": " (rewrap)", + "transit_rewrap_version_option_text": "{{version_num}}", + "transit_rewrap_latest_version_option_text": "{{version_num}} (latest)", + "transit_rewrap_input_placeholder": "Cyphertext", + "transit_rewrap_rewrap_btn": "Rewrap", + "transit_rewrap_result_modal_title": "Rewrap Result", } \ No newline at end of file