diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 4006cc9e1..526489779 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -46,14 +46,11 @@ const AuthenticatorCodesPage = () => { appContext.showNavBar(false); }, []); + const lcSearch = searchTerm.toLowerCase(); const filteredCodes = codes.filter( - (secret) => - (secret.issuer ?? "") - .toLowerCase() - .includes(searchTerm.toLowerCase()) || - (secret.account ?? "") - .toLowerCase() - .includes(searchTerm.toLowerCase()), + (code) => + code.issuer?.toLowerCase().includes(lcSearch) || + code.account?.toLowerCase().includes(lcSearch), ); if (!hasFetched) { @@ -270,7 +267,7 @@ const OTPDisplay: React.FC = ({ code, otp, nextOTP }) => { textAlign: "left", }} > - {code.issuer} + {code.issuer ?? ""}

= ({ code, otp, nextOTP }) => { color: "grey", }} > - {code.account} + {code.account ?? ""}

{ - const santizedRawData = uriString - .replaceAll("+", "%2B") - .replaceAll(":", "%3A") - .replaceAll("\r", "") - // trim quotes - .replace(/^"|"$/g, ""); - - const uriParams = {}; - const searchParamsString = - decodeURIComponent(santizedRawData).split("?")[1]; - searchParamsString.split("&").forEach((pair) => { - const [key, value] = pair.split("="); - uriParams[key] = value; - }); - - const uri = URI.parse(santizedRawData); - let uriPath = decodeURIComponent(uri.path); - if (uriPath.startsWith("/otpauth://") || uriPath.startsWith("otpauth://")) { - uriPath = uriPath.split("otpauth://")[1]; - } else if (uriPath.startsWith("otpauth%3A//")) { - uriPath = uriPath.split("otpauth%3A//")[1]; + try { + return _codeFromURIString(id, uriString); + } catch (e) { + // We might have legacy encodings of account names that contain a "#", + // which causes the rest of the URL to be treated as a fragment, and + // ignored. See if this was potentially such a case, otherwise rethrow. + if (uriString.includes("#")) + return _codeFromURIString(id, uriString.replaceAll("#", "%23")); + throw e; } +}; + +const _codeFromURIString = (id: string, uriString: string): Code => { + const url = new URL(uriString); + + // A URL like + // + // new URL("otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0") + // + // is parsed differently by the browser and Node depending on the scheme. + // When the scheme is http(s), then both of them consider "hotp" as the + // `host`. However, when the scheme is "otpauth", as is our case, the + // browser considers the entire thing as part of the pathname. so we get. + // + // host: "" + // pathname: "//hotp/Test" + // + // Since this code run on browsers only, we parse as per that behaviour. + + const [type, path] = parsePathname(url); return { id, - type: _getType(uriPath), - account: _getAccount(uriPath), - issuer: _getIssuer(uriPath, uriParams), - digits: parseDigits(uriParams), - period: parsePeriod(uriParams), - secret: parseSecret(uriParams), - algorithm: parseAlgorithm(uriParams), + type, + account: parseAccount(path), + issuer: parseIssuer(url, path), + digits: parseDigits(url), + period: parsePeriod(url), + secret: parseSecret(url), + algorithm: parseAlgorithm(url), uriString, }; }; -const _getType = (uriPath: string): Code["type"] => { - const oauthType = uriPath.split("/")[0].substring(0); - if (oauthType.toLowerCase() === "totp") { - return "totp"; - } else if (oauthType.toLowerCase() === "hotp") { - return "hotp"; - } - throw new Error(`Unsupported format with host ${oauthType}`); +const parsePathname = (url: URL): [type: Code["type"], path: string] => { + const p = url.pathname.toLowerCase(); + if (p.startsWith("//totp")) return ["totp", url.pathname.slice(6)]; + if (p.startsWith("//hotp")) return ["hotp", url.pathname.slice(6)]; + throw new Error(`Unsupported code or unparseable path "${url.pathname}"`); }; -const _getAccount = (uriPath: string): string => { - try { - const path = decodeURIComponent(uriPath); - if (path.includes(":")) { - return path.split(":")[1]; - } else if (path.includes("/")) { - return path.split("/")[1]; - } - } catch (e) { - return ""; - } +const parseAccount = (path: string): string | undefined => { + // "/ACME:user@example.org" => "user@example.org" + let p = decodeURIComponent(path); + if (p.startsWith("/")) p = p.slice(1); + if (p.includes(":")) p = p.split(":").slice(1).join(":"); + return p; }; -const _getIssuer = (uriPath: string, uriParams: { get?: any }): string => { - try { - if (uriParams["issuer"] !== undefined) { - let issuer = uriParams["issuer"]; - // This is to handle bug in the ente auth app - if (issuer.endsWith("period")) { - issuer = issuer.substring(0, issuer.length - 6); - } - return issuer; +const parseIssuer = (url: URL, path: string): string => { + // If there is a "issuer" search param, use that. + let issuer = url.searchParams.get("issuer"); + if (issuer) { + // This is to handle bug in old versions of Ente Auth app. + if (issuer.endsWith("period")) { + issuer = issuer.substring(0, issuer.length - 6); } - let path = decodeURIComponent(uriPath); - if (path.startsWith("totp/") || path.startsWith("hotp/")) { - path = path.substring(5); - } - if (path.includes(":")) { - return path.split(":")[0]; - } else if (path.includes("-")) { - return path.split("-")[0]; - } - return path; - } catch (e) { - return ""; + return issuer; } + + // Otherwise use the `prefix:` from the account as the issuer. + // "/ACME:user@example.org" => "ACME" + let p = decodeURIComponent(path); + if (p.startsWith("/")) p = p.slice(1); + + if (p.includes(":")) p = p.split(":")[0]; + else if (p.includes("-")) p = p.split("-")[0]; + + return p; }; -const parseDigits = (uriParams): number => - parseInt(uriParams["digits"] ?? "", 10) || 6; +const parseDigits = (url: URL): number => + parseInt(url.searchParams.get("digits") ?? "", 10) || 6; -const parsePeriod = (uriParams): number => - parseInt(uriParams["period"] ?? "", 10) || 30; +const parsePeriod = (url: URL): number => + parseInt(url.searchParams.get("period") ?? "", 10) || 30; -const parseAlgorithm = (uriParams): Code["algorithm"] => { - switch (uriParams["algorithm"]?.toLowerCase()) { +const parseAlgorithm = (url: URL): Code["algorithm"] => { + switch (url.searchParams.get("algorithm")?.toLowerCase()) { case "sha256": return "sha256"; case "sha512": @@ -148,8 +147,8 @@ const parseAlgorithm = (uriParams): Code["algorithm"] => { } }; -const parseSecret = (uriParams): string => - uriParams["secret"].replaceAll(" ", "").toUpperCase(); +const parseSecret = (url: URL): string => + ensure(url.searchParams.get("secret")).replaceAll(" ", "").toUpperCase(); /** * Generate a pair of OTPs (one time passwords) from the given {@link code}. diff --git a/web/apps/auth/src/services/remote.ts b/web/apps/auth/src/services/remote.ts index 07b15d7d7..11d57aa23 100644 --- a/web/apps/auth/src/services/remote.ts +++ b/web/apps/auth/src/services/remote.ts @@ -35,7 +35,7 @@ export const getAuthCodes = async (): Promise => { ); return codeFromURIString(entity.id, decryptedCode); } catch (e) { - log.error(`failed to parse codeId = ${entity.id}`); + log.error(`Failed to parse codeID ${entity.id}`, e); return null; } }), diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index 1541878c5..0ec924b29 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -43,7 +43,6 @@ "similarity-transformation": "^0.0.1", "transformation-matrix": "^2.16", "uuid": "^9.0.1", - "vscode-uri": "^3.0.7", "xml-js": "^1.6.11", "zxcvbn": "^4.4.2" }, diff --git a/web/yarn.lock b/web/yarn.lock index 894a44dd0..aaa0d517a 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -4804,11 +4804,6 @@ void-elements@3.1.0: resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== -vscode-uri@^3.0.7: - version "3.0.8" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" - integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"