1
0
Fork 0

Add vault-generated totp code support.

This commit is contained in:
ChaotiCryptidz 2022-01-11 12:45:35 +00:00
parent 5be6596295
commit 7608df51b3
12 changed files with 264 additions and 37 deletions

View file

@ -2,32 +2,32 @@ import { Component } from "preact";
import { settings } from "./globalSettings";
// @ts-ignore
import style_dark from "./scss/main-dark.scss" assert {type: "css"};
import style_dark from "./scss/main-dark.scss" assert { type: "css" };
// @ts-ignore
import style_light from "./scss/main-light.scss" assert {type: "css"};
import style_light from "./scss/main-light.scss" assert { type: "css" };
export const default_theme = "dark";
const themes: { [key: string]: string } = {
"dark": style_dark,
"light": style_light,
}
dark: style_dark as string,
light: style_light as string,
};
export class ThemeLoader extends Component<{}, { sheet: string }> {
export class ThemeLoader extends Component<unknown, { sheet: string }> {
componentDidMount() {
this.setCorrectStyle(settings.theme);
settings.registerListener((key: string) => {
if (key != "theme") return;
this.setCorrectStyle(settings.theme);
})
});
}
setCorrectStyle(theme: string) {
this.setState({ sheet: themes[theme] })
this.setState({ sheet: themes[theme] });
}
render() {
if (!this.state.sheet) return;
return <style>{this.state.sheet}</style>
return <style>{this.state.sheet}</style>;
}
}

View file

@ -12,6 +12,7 @@ import {
} from "./types/transit";
import { DoesNotExistError } from "../types/internalErrors";
import { MountType, MountsType, NewMountParams } from "./types/mount";
import { NewTOTPData, NewTOTPResp } from "./types/totp";
import { SealStatusType } from "./types/seal";
import { SecretMetadataType } from "./types/secret";
import { Settings } from "../settings/Settings";
@ -461,7 +462,7 @@ export class API {
return data.data.keys;
}
async addNewTOTP(baseMount: string, parms: { name: string }): Promise<void> {
async addNewTOTP(baseMount: string, parms: NewTOTPData): Promise<NewTOTPResp> {
const request = new Request(
this.appendAPIURL(removeDoubleSlash(`/v1/${baseMount}/keys/${parms.name}`)),
{
@ -475,6 +476,8 @@ export class API {
);
const resp = await fetch(request);
await checkResponse(resp);
const data = (await resp.json()) as { data: NewTOTPResp };
return data.data;
}
async deleteTOTP(baseMount: string, name: string): Promise<void> {

18
src/api/types/totp.ts Normal file
View file

@ -0,0 +1,18 @@
export type NewTOTPData = {
name: string;
generate: boolean;
exported?: boolean;
key_size?: number;
url?: string;
key?: string;
issuer?: string;
account_name?: string;
period?: number | string;
algorithm?: string;
digits?: number;
};
export type NewTOTPResp = {
url: string;
barcode: string;
};

View file

@ -25,13 +25,13 @@ import { formatDistance } from "./formatDistance";
//import { pageList } from "./allPages";
import { Main } from "./pages";
import { NavBar } from "./ui/elements/NavBar";
import { ThemeLoader } from "./ThemeLoader";
import { api } from "./globalAPI";
import { getCurrentUrl, route } from "preact-router";
import { playground } from "./playground";
import { render } from "preact";
import { settings } from "./globalSettings";
import i18next from "i18next";
import { ThemeLoader } from "./ThemeLoader";
async function onLoad(): Promise<void> {
document.documentElement.dir = settings.pageDirection;

View file

@ -30,6 +30,7 @@ import { SetVaultURL } from "./ui/pages/SetVaultURL";
import { TOTPDelete } from "./ui/pages/Secrets/TOTP/TOTPDelete";
import { TOTPList } from "./ui/pages/Secrets/TOTP/TOTPList";
import { TOTPNew } from "./ui/pages/Secrets/TOTP/TOTPNew";
import { TOTPNewGenerated } from "./ui/pages/Secrets/TOTP/TOTPNewGenerated";
import { TransitDecrypt } from "./ui/pages/Secrets/Transit/TransitDecrypt";
import { TransitEncrypt } from "./ui/pages/Secrets/Transit/TransitEncrypt";
import { TransitList } from "./ui/pages/Secrets/Transit/TransitList";
@ -81,6 +82,7 @@ export const Main = () => (
<TOTPList path="/secrets/totp/list/:baseMount" settings={settings} api={api} />
<TOTPNew path="/secrets/totp/new/:baseMount" settings={settings} api={api} />
<TOTPNewGenerated path="/secrets/totp/new_generated/:baseMount" settings={settings} api={api} />
<TOTPDelete path="/secrets/totp/delete/:baseMount/:item" settings={settings} api={api} />
<TransitNew path="/secrets/transit/new/:baseMount" settings={settings} api={api} />

View file

@ -1,5 +1,5 @@
import { settings } from "./globalSettings";
import { Settings } from "./settings/Settings";
import { settings } from "./globalSettings";
// Playground is a way to debug and test things.
// Anything you put in here is gonna be run on page initial load
@ -12,7 +12,6 @@ declare global {
}
}
// Please empty this function before committing.
export async function playground(): Promise<void> {
console.log("Welcome to Playground!");

View file

@ -1,5 +1,5 @@
import { default_theme } from "../ThemeLoader";
import { StorageType } from "./storage/StorageType";
import { default_theme } from "../ThemeLoader";
type OnChangeListener = (key: string) => void;
@ -17,7 +17,7 @@ export class Settings {
}
alertChange(key: string) {
for (let listener of this.listeners) {
for (const listener of this.listeners) {
listener(key);
}
}

View file

@ -13,6 +13,7 @@ module.exports = {
// Common buttons / placeholders
common_new: "New",
common_view: "View",
common_back: "Back",
common_edit: "Edit",
common_create: "Create",
common_delete: "Delete",
@ -176,6 +177,18 @@ module.exports = {
totp_new_switch_to_qr_btn: "Switch to QR Input",
totp_new_switch_back_to_manual_input_btn: "Switch back to manual input",
totp_new_generated: "New Generated",
totp_new_generated_suffix: "(new generated)",
totp_new_generated_warning:
"Make sure to save this information somewhere safe as there is no way to get it back short of Vault's raw api.",
totp_new_generated_issuer: "Issuer",
totp_new_generated_account_name: "Account Name",
totp_new_generated_algorithm: "Algorithm",
totp_new_generated_key_size: "Key Size (bytes)",
totp_new_generated_period: "Period (Refresh Time)",
totp_new_generated_digits: "Digits (Length 6/8)",
totp_new_generated_export: "Show QR & URI (Recommended)",
// TOTP Delete Page
totp_delete_title: "Delete TOTP Key",
totp_delete_suffix: " (delete)",

View file

@ -1,11 +1,11 @@
import { Component, JSX, createRef } from "preact";
import { DefaultPageProps } from "../../types/DefaultPageProps";
import { InputWithTitle } from "../elements/InputWithTitle";
import { PageTitle } from "../elements/PageTitle";
import { addClipboardNotifications, setErrorText } from "../../pageUtils";
import { route } from "preact-router";
import ClipboardJS from "clipboard";
import i18next from "i18next";
import { InputWithTitle } from "../elements/InputWithTitle";
export class CopyLink extends Component<{ text: string; data: string }, unknown> {
linkRef = createRef<HTMLAnchorElement>();
@ -55,7 +55,7 @@ export class Me extends Component<DefaultPageProps, MeState> {
});
}
themeSelectRef = createRef<HTMLSelectElement>()
themeSelectRef = createRef<HTMLSelectElement>();
render(): JSX.Element {
return (
@ -111,12 +111,20 @@ export class Me extends Component<DefaultPageProps, MeState> {
</li>
<InputWithTitle title="Theme">
<select ref={this.themeSelectRef} class="uk-select uk-form-width-medium" onChange={() => {
let newTheme = this.themeSelectRef.current.value;
<select
ref={this.themeSelectRef}
class="uk-select uk-form-width-medium"
onChange={() => {
const newTheme = this.themeSelectRef.current.value;
this.props.settings.theme = newTheme;
}}>
}}
>
<option label="Dark" value="dark" selected={this.props.settings.theme == "dark"} />
<option label="Light" value="light" selected={this.props.settings.theme == "light"} />
<option
label="Light"
value="light"
selected={this.props.settings.theme == "light"}
/>
</select>
</InputWithTitle>
</ul>

View file

@ -6,7 +6,7 @@ import { DoesNotExistError } from "../../../../types/internalErrors";
import { Grid, GridSizes } from "../../../elements/Grid";
import { MarginInline } from "../../../elements/MarginInline";
import { SecretTitleElement } from "../SecretTitleElement";
import { delSecretsEngineURL, totpNewURL } from "../../pageLinks";
import { delSecretsEngineURL, totpNewGeneratedURL, totpNewURL } from "../../pageLinks";
import { removeDoubleSlash } from "../../../../utils";
import { route } from "preact-router";
import { setErrorText } from "../../../../pageUtils";
@ -138,6 +138,16 @@ export class TOTPList extends Component<DefaultPageProps, TOTPListState> {
{i18next.t("common_new")}
</button>
)}
{totpCaps.includes("create") && (
<button
class="uk-button uk-button-primary"
onClick={async () => {
route(totpNewGeneratedURL(baseMount));
}}
>
{i18next.t("totp_new_generated")}
</button>
)}
{mountCaps.includes("delete") && (
<button
class="uk-button uk-button-danger"

View file

@ -0,0 +1,170 @@
import { Component, JSX, createRef } from "preact";
import { CopyableInputBox } from "../../../elements/CopyableInputBox";
import { DefaultPageProps } from "../../../../types/DefaultPageProps";
import { Form } from "../../../elements/Form";
import { InputWithTitle } from "../../../elements/InputWithTitle";
import { Margin } from "../../../elements/Margin";
import { MarginInline } from "../../../elements/MarginInline";
import { NewTOTPResp } from "../../../../api/types/totp";
import { SecretTitleElement } from "../SecretTitleElement";
import { route } from "preact-router";
import { setErrorText } from "../../../../pageUtils";
import i18next from "i18next";
export class TOTPNewGeneratedForm extends Component<
{ baseMount: string } & DefaultPageProps,
{ exportedData: NewTOTPResp }
> {
uriInputRef = createRef<HTMLInputElement>();
render(): JSX.Element {
if (!this.state.exportedData) {
return (
<Form onSubmit={(data) => this.onSubmit(data)}>
<Margin>
<InputWithTitle title={i18next.t("common_name")}>
<input
class="uk-input uk-form-width-medium"
name="name"
type="text"
placeholder={i18next.t("common_name")}
required
/>
</InputWithTitle>
</Margin>
<Margin>
<InputWithTitle title={i18next.t("totp_new_generated_issuer")}>
<input class="uk-input uk-form-width-medium" name="issuer" type="text" required />
</InputWithTitle>
</Margin>
<Margin>
<InputWithTitle title={i18next.t("totp_new_generated_account_name")}>
<input
class="uk-input uk-form-width-medium"
name="account_name"
type="text"
required
/>
</InputWithTitle>
</Margin>
<Margin>
<InputWithTitle title={i18next.t("totp_new_generated_algorithm")}>
<select class="uk-select uk-form-width-medium" name="algorithm">
{["SHA512", "SHA256", "SHA1"].map((type) => (
<option label={type} value={type}>
{type}
</option>
))}
</select>
</InputWithTitle>
</Margin>
<Margin>
<InputWithTitle title={i18next.t("totp_new_generated_key_size")}>
<input
class="uk-input uk-form-width-medium"
name="key_size"
type="number"
value="20"
/>
</InputWithTitle>
</Margin>
<Margin>
<InputWithTitle title={i18next.t("totp_new_generated_period")}>
<input class="uk-input uk-form-width-medium" name="period" type="text" value="30s" />
</InputWithTitle>
</Margin>
<Margin>
<InputWithTitle title={i18next.t("totp_new_generated_digits")}>
<input class="uk-input uk-form-width-medium" name="digits" type="number" value="6" />
</InputWithTitle>
</Margin>
<Margin>
<InputWithTitle title={i18next.t("totp_new_generated_export")}>
<input class="uk-checkbox" name="exported" type="checkbox" value="yes" checked />
</InputWithTitle>
</Margin>
<p id="errorText" class="uk-text-danger" />
<MarginInline>
<button class="uk-button uk-button-primary" type="submit">
{i18next.t("common_create")}
</button>
</MarginInline>
</Form>
);
} else {
return (
<>
<p>{i18next.t("totp_new_generated_warning")}</p>
<img src={"data:image/png;base64," + this.state.exportedData.barcode} />
<CopyableInputBox copyable text={this.state.exportedData.url} />
<button
class="uk-button uk-button-primary"
onClick={async () => {
route("/secrets/totp/list/" + this.props.baseMount);
}}
>
{i18next.t("common_back")}
</button>
</>
);
}
}
async onSubmit(data: FormData): Promise<void> {
const isExported = data.get("exported") == "yes" ? true : false;
const parms = {
generate: true,
name: data.get("name") as string,
issuer: data.get("issuer") as string,
account_name: data.get("account_name") as string,
exported: isExported,
key_size: parseInt(data.get("key_size") as string),
period: data.get("period") as string,
algorithm: data.get("algorithm") as string,
digits: parseInt(data.get("digits") as string),
};
console.log(parms);
try {
const ret = await this.props.api.addNewTOTP(this.props.baseMount, parms);
if (!isExported) {
route("/secrets/totp/list/" + this.props.baseMount);
} else {
this.setState({ exportedData: ret });
}
} catch (e: unknown) {
const error = e as Error;
setErrorText(`API Error: ${error.message}`);
}
}
}
export class TOTPNewGenerated extends Component<DefaultPageProps> {
render() {
const baseMount = this.props.matches["baseMount"];
return (
<>
<SecretTitleElement
type="totp"
baseMount={baseMount}
suffix={i18next.t("totp_new_generated_suffix")}
/>
<TOTPNewGeneratedForm
settings={this.props.settings}
api={this.props.api}
baseMount={baseMount}
/>
</>
);
}
}

View file

@ -33,6 +33,10 @@ export function totpNewURL(baseMount: string): string {
return `/secrets/totp/new/${baseMount}`;
}
export function totpNewGeneratedURL(baseMount: string): string {
return `/secrets/totp/new_generated/${baseMount}`;
}
export function totpListURL(baseMount: string): string {
return `/secrets/totp/list/${baseMount}`;
}