Skip to content

Commit

Permalink
Handle bad OTP URIs - fixes #466
Browse files Browse the repository at this point in the history
  • Loading branch information
perry-mitchell committed Mar 27, 2024
1 parent 697dc66 commit 36808b4
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 14 deletions.
12 changes: 10 additions & 2 deletions source/popup/components/otps/OTPItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,19 @@ export function OTPItem(props: OTPItemProps) {
onClick();
}, [onClick]);
const [codeFirst, codeSecond] = useMemo(() => {
if (otp.errored) return [otp.digits, ""];
return otp.digits.length === 8
? [otp.digits.substring(0, 4), otp.digits.substring(4)]
: [otp.digits.substring(0, 3), otp.digits.substring(3)]
}, [otp.digits]);
const spinnerLeft = otp.remaining / otp.period;
const spinnerLeft = useMemo(() => {
if (otp.errored) return 1;
return otp.remaining / otp.period;
}, [otp]);
const spinnerIntent = useMemo(() => {
if (otp.errored) return Intent.DANGER;
return spinnerLeft < 0.15 ? Intent.DANGER : spinnerLeft < 0.35 ? Intent.WARNING : Intent.SUCCESS;
}, [otp.errored, spinnerLeft]);
return (
<Container isActive={false} onClick={handleOTPClick}>
<OTPRow>
Expand All @@ -108,7 +116,7 @@ export function OTPItem(props: OTPItemProps) {
<Spinner
size={19}
value={spinnerLeft}
intent={spinnerLeft < 0.15 ? Intent.DANGER : spinnerLeft < 0.35 ? Intent.WARNING : Intent.SUCCESS}
intent={spinnerIntent}
/>
<OTPCodePart>{codeFirst}</OTPCodePart>
<OTPCodePart>{codeSecond}</OTPCodePart>
Expand Down
45 changes: 36 additions & 9 deletions source/popup/hooks/otp.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as OTPAuth from "otpauth";
import { Layerr } from "layerr";
import { useEffect, useMemo, useState } from "react";
import { OTP } from "../types.js";
import { useTimer } from "../../shared/hooks/timer.js";
import { t } from "../../shared/i18n/trans.js";
import { OTP } from "../types.js";

export interface PreparedOTP extends OTP {
digits: string;
errored: boolean;
period: number;
remaining: number;
}
Expand All @@ -14,18 +17,23 @@ function getPeriodTimeLeft(period: number): number {
}

export function usePreparedOTPs(otps: Array<OTP>): Array<PreparedOTP> {
const [parsedOTPs, setParsedOTPs] = useState<Record<string, OTPAuth.TOTP>>({});
const [parsedOTPs, setParsedOTPs] = useState<Record<string, OTPAuth.TOTP | Error>>({});
const [periods, setPeriods] = useState<Record<string, [number, number]>>({});
useEffect(() => {
const newParsed = { ...parsedOTPs };
let changed = false;
for (const otp of otps) {
if (newParsed[otp.otpURL]) continue;
const otpInst = OTPAuth.URI.parse(otp.otpURL) as OTPAuth.TOTP;
if (!otpInst.period) {
throw new Error(`OTP is invalid (no period): ${otp.otpURL}`);
try {
const otpInst = OTPAuth.URI.parse(otp.otpURL) as OTPAuth.TOTP;
if (!otpInst.period) {
throw new Error(`OTP is invalid (no period): ${otp.otpURL}`);
}
newParsed[otp.otpURL] = otpInst;
} catch (err) {
newParsed[otp.otpURL] = err;
console.error(err);
}
newParsed[otp.otpURL] = otpInst;
changed = true;
}
for (const parsed in newParsed) {
Expand All @@ -46,7 +54,13 @@ export function usePreparedOTPs(otps: Array<OTP>): Array<PreparedOTP> {
Object.keys(parsedOTPs).reduce(
(newPeriods, url) => ({
...newPeriods,
[url]: [parsedOTPs[url].period, getPeriodTimeLeft(parsedOTPs[url].period)]
[url]:
parsedOTPs[url] instanceof OTPAuth.TOTP
? [
(parsedOTPs[url] as OTPAuth.TOTP).period,
getPeriodTimeLeft((parsedOTPs[url] as OTPAuth.TOTP).period)
]
: [0, 0]
}),
{}
)
Expand All @@ -61,12 +75,25 @@ export function usePreparedOTPs(otps: Array<OTP>): Array<PreparedOTP> {
const parsed = parsedOTPs[otp.otpURL];
const periodInfo = periods[otp.otpURL];
if (!parsed || !Array.isArray(periodInfo)) return output;
let errored = false,
code: string = "";
try {
if (parsed instanceof Error) {
throw new Layerr(parsed, "OTP was not parseable");
}
code = parsed.generate();
} catch (err) {
console.error(err);
code = t("popup.entries.otp.code-error");
errored = true;
}
return [
...output,
{
...otp,
otpTitle: parsed.label,
digits: parsed.generate(),
otpTitle: parsed instanceof OTPAuth.TOTP ? parsed.label : t("popup.entries.otp.label-error"),
digits: code,
errored,
period: periodInfo[0],
remaining: periodInfo[1]
}
Expand Down
17 changes: 16 additions & 1 deletion source/popup/services/tab.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { SearchResult } from "buttercup";
import { Intent } from "@blueprintjs/core";
import { otpURIToDigits } from "../../shared/library/otp.js";
import { getToaster } from "../../shared/services/notifications.js";
import { localisedErrorMessage } from "../../shared/library/error.js";
import { t } from "../../shared/i18n/trans.js";
import { OTP, TabEvent, TabEventType } from "../types.js";

export function sendEntryResultToTabForInput(formID: string, entry: SearchResult): void {
Expand All @@ -20,10 +24,21 @@ export function sendOTPToTabForInput(formID: string, otp: OTP): void {
if (!formID) {
throw new Error("No form ID found for dialog");
}
let code: string = "";
try {
code = otpURIToDigits(otp.otpURL);
} catch (err) {
console.error(err);
getToaster().show({
intent: Intent.DANGER,
message: t("error.otp-generate", { message: localisedErrorMessage(err) }),
timeout: 10000
});
}
sendTabEvent({
formID,
inputDetails: {
otp: otpURIToDigits(otp.otpURL)
otp: code
},
type: TabEventType.InputDetails
});
Expand Down
5 changes: 5 additions & 0 deletions source/shared/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
},
"fatal-boundary": "A fatal error has occurred - we're sorry this has happened. Please check out the details below in case they help diagnose the issue:",
"generic": "Requested action failed: {{message}}",
"otp-generate": "Failed generating an OTP code for an OTP URI: {{message}}",
"reset": "Failed resetting application settings: {{message}}"
},
"form": {
Expand Down Expand Up @@ -123,6 +124,10 @@
"info": {
"tooltip": "Show entry properties"
},
"otp": {
"code-error": "ERROR",
"label-error": "Bad OTP Item"
},
"search": {
"button": "Search entries",
"placeholder": "Search..."
Expand Down
9 changes: 7 additions & 2 deletions source/shared/library/otp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SearchResult } from "buttercup";
import { Layerr } from "layerr";
import * as OTPAuth from "otpauth";

function extractFirstOTPURI(entry: SearchResult): string | null {
Expand All @@ -14,8 +15,12 @@ function extractFirstOTPURI(entry: SearchResult): string | null {
}

export function otpURIToDigits(uri: string): string {
const otp = OTPAuth.URI.parse(uri);
return otp.generate();
try {
const otp = OTPAuth.URI.parse(uri);
return otp.generate();
} catch (err) {
throw new Layerr(err, "Failed generating OTP code for URI");
}
}

export function searchResultToOTP(entry: SearchResult): string | null {
Expand Down

0 comments on commit 36808b4

Please sign in to comment.