Skip to content

Commit c9b14cc

Browse files
authored
Merge pull request #14 from akopachov/ghi-13
Implemented #13
2 parents 57d33a8 + d7cbfd0 commit c9b14cc

File tree

6 files changed

+265
-101
lines changed

6 files changed

+265
-101
lines changed

package.json

Lines changed: 92 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,92 @@
1-
{
2-
"name": "flipper-authenticator-companion",
3-
"version": "1.1.1",
4-
"private": true,
5-
"description": "Companion application for Flipper Authenticator software-based TOTP authenticator for Flipper Zero device",
6-
"main": "src/electron.cjs",
7-
"type": "module",
8-
"author": "Alexander Kopachov (@akopachov)",
9-
"scripts": {
10-
"dev": "cross-env NODE_ENV=dev pnpm run dev:all",
11-
"dev:all": "concurrently -n=svelte,electron -c='#ff3e00',blue \"pnpm run dev:svelte\" \"pnpm run dev:electron\"",
12-
"dev:svelte": "vite dev",
13-
"dev:electron": "electron src/electron.cjs",
14-
"build": "cross-env NODE_ENV=production pnpm run build:svelte && pnpm build:electron",
15-
"build:svelte": "vite build",
16-
"build:electron": "electron-builder --config build.config.json",
17-
"lint": "eslint src/",
18-
"lint:fix": "eslint src/ --fix",
19-
"update:icons": "node update-aegis-icons.cjs"
20-
},
21-
"browserslist": [
22-
"Chrome 89"
23-
],
24-
"engines": {
25-
"node": ">=18",
26-
"pnpm": ">=8",
27-
"npm": "please-use-pnpm",
28-
"yarn": "please-use-pnpm"
29-
},
30-
"devDependencies": {
31-
"@floating-ui/dom": "^1.5.1",
32-
"@skeletonlabs/skeleton": "^2.0.0",
33-
"@skeletonlabs/tw-plugin": "^0.1.0",
34-
"@sveltejs/adapter-static": "2.0.3",
35-
"@sveltejs/kit": "1.24.0",
36-
"@tailwindcss/forms": "^0.5.6",
37-
"@tailwindcss/typography": "^0.5.9",
38-
"@types/node": "^20.5.7",
39-
"@types/papaparse": "^5.3.8",
40-
"@types/uuid": "^9.0.3",
41-
"@typescript-eslint/eslint-plugin": "^6.5.0",
42-
"@typescript-eslint/parser": "^6.5.0",
43-
"async-sema": "^3.1.1",
44-
"autoprefixer": "^10.4.15",
45-
"concurrently": "^8.2.1",
46-
"cross-env": "^7.0.3",
47-
"delay": "^6.0.0",
48-
"electron": "^26.1.0",
49-
"electron-builder": "^24.6.3",
50-
"electron-connect": "^0.6.3",
51-
"electron-packager": "^17.1.2",
52-
"electron-reloader": "^1.2.3",
53-
"escape-string-regexp": "^5.0.0",
54-
"eslint": "^8.48.0",
55-
"eslint-config-prettier": "^9.0.0",
56-
"eslint-plugin-prettier": "^5.0.0",
57-
"eslint-plugin-svelte": "^2.33.0",
58-
"papaparse": "^5.4.1",
59-
"postcss": "^8.4.29",
60-
"postcss-load-config": "^4.0.1",
61-
"prettier": "^3.0.3",
62-
"prettier-plugin-svelte": "^3.0.3",
63-
"qr-scanner": "^1.4.2",
64-
"sass": "^1.66.1",
65-
"smart-buffer": "^4.2.0",
66-
"svelte": "^4.2.0",
67-
"svelte-check": "^3.5.1",
68-
"svelte-dnd-action": "^0.9.26",
69-
"svelte-eslint-parser": "^0.33.0",
70-
"svelte-preprocess": "^5.0.4",
71-
"tailwindcss": "^3.3.3",
72-
"tslib": "^2.6.2",
73-
"typescript": "^5.2.2",
74-
"url-otpauth-ng": "^3.1.0",
75-
"utc-offsets": "^1.0.0",
76-
"uuid": "^9.0.0",
77-
"vite": "^4.4.9",
78-
"vite-plugin-electron-renderer": "^0.14.5",
79-
"vite-plugin-tailwind-purgecss": "^0.1.3"
80-
},
81-
"dependencies": {
82-
"electron-log": "^4.4.8",
83-
"electron-serve": "^1.1.0",
84-
"electron-store": "^8.1.0",
85-
"electron-updater": "^6.1.1",
86-
"electron-window-state": "^5.0.3",
87-
"node-screenshots": "^0.1.6",
88-
"serialport": "^12.0.0"
89-
},
90-
"optionalDependencies": {
91-
"adm-zip": "^0.5.10"
92-
}
93-
}
1+
{
2+
"name": "flipper-authenticator-companion",
3+
"version": "1.1.1",
4+
"private": true,
5+
"description": "Companion application for Flipper Authenticator software-based TOTP authenticator for Flipper Zero device",
6+
"main": "src/electron.cjs",
7+
"type": "module",
8+
"author": "Alexander Kopachov (@akopachov)",
9+
"scripts": {
10+
"dev": "cross-env NODE_ENV=dev pnpm run dev:all",
11+
"dev:all": "concurrently -n=svelte,electron -c='#ff3e00',blue \"pnpm run dev:svelte\" \"pnpm run dev:electron\"",
12+
"dev:svelte": "vite dev",
13+
"dev:electron": "electron src/electron.cjs",
14+
"build": "cross-env NODE_ENV=production pnpm run build:svelte && pnpm build:electron",
15+
"build:svelte": "vite build",
16+
"build:electron": "electron-builder --config build.config.json",
17+
"lint": "eslint src/",
18+
"lint:fix": "eslint src/ --fix",
19+
"update:icons": "node update-aegis-icons.cjs"
20+
},
21+
"browserslist": [
22+
"Chrome 89"
23+
],
24+
"engines": {
25+
"node": ">=18",
26+
"pnpm": ">=8",
27+
"npm": "please-use-pnpm",
28+
"yarn": "please-use-pnpm"
29+
},
30+
"devDependencies": {
31+
"@floating-ui/dom": "^1.5.1",
32+
"@skeletonlabs/skeleton": "^2.0.0",
33+
"@skeletonlabs/tw-plugin": "^0.1.0",
34+
"@sveltejs/adapter-static": "2.0.3",
35+
"@sveltejs/kit": "1.24.0",
36+
"@tailwindcss/forms": "^0.5.6",
37+
"@tailwindcss/typography": "^0.5.9",
38+
"@types/node": "^20.5.7",
39+
"@types/papaparse": "^5.3.8",
40+
"@types/uuid": "^9.0.3",
41+
"@typescript-eslint/eslint-plugin": "^6.5.0",
42+
"@typescript-eslint/parser": "^6.5.0",
43+
"async-sema": "^3.1.1",
44+
"autoprefixer": "^10.4.15",
45+
"concurrently": "^8.2.1",
46+
"cross-env": "^7.0.3",
47+
"delay": "^6.0.0",
48+
"electron": "^26.1.0",
49+
"electron-builder": "^24.6.3",
50+
"electron-connect": "^0.6.3",
51+
"electron-packager": "^17.1.2",
52+
"electron-reloader": "^1.2.3",
53+
"escape-string-regexp": "^5.0.0",
54+
"eslint": "^8.48.0",
55+
"eslint-config-prettier": "^9.0.0",
56+
"eslint-plugin-prettier": "^5.0.0",
57+
"eslint-plugin-svelte": "^2.33.0",
58+
"papaparse": "^5.4.1",
59+
"postcss": "^8.4.29",
60+
"postcss-load-config": "^4.0.1",
61+
"prettier": "^3.0.3",
62+
"prettier-plugin-svelte": "^3.0.3",
63+
"qr-scanner": "^1.4.2",
64+
"sass": "^1.66.1",
65+
"smart-buffer": "^4.2.0",
66+
"svelte": "^4.2.0",
67+
"svelte-check": "^3.5.1",
68+
"svelte-dnd-action": "^0.9.26",
69+
"svelte-eslint-parser": "^0.33.0",
70+
"svelte-preprocess": "^5.0.4",
71+
"tailwindcss": "^3.3.3",
72+
"tslib": "^2.6.2",
73+
"typescript": "^5.2.2",
74+
"utc-offsets": "^1.0.0",
75+
"uuid": "^9.0.0",
76+
"vite": "^4.4.9",
77+
"vite-plugin-electron-renderer": "^0.14.5",
78+
"vite-plugin-tailwind-purgecss": "^0.1.3"
79+
},
80+
"dependencies": {
81+
"electron-log": "^4.4.8",
82+
"electron-serve": "^1.1.0",
83+
"electron-store": "^8.1.0",
84+
"electron-updater": "^6.1.1",
85+
"electron-window-state": "^5.0.3",
86+
"node-screenshots": "^0.1.6",
87+
"serialport": "^12.0.0"
88+
},
89+
"optionalDependencies": {
90+
"adm-zip": "^0.5.10"
91+
}
92+
}

pnpm-lock.yaml

Lines changed: 0 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/url-otpauth-ts/error-type.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export enum ErrorType {
2+
InvalidIssuer = 0,
3+
InvalidLabel = 1,
4+
InvalidProtocol = 2,
5+
MissingAccountName = 3,
6+
MissingCounter = 4,
7+
MissingIssuer = 5,
8+
MissingSecretKey = 6,
9+
UnknownOtp = 7,
10+
InvalidDigits = 8,
11+
UnknownAlgorithm = 9,
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ErrorType } from './error-type';
2+
3+
export class OtpAuthInvalidUrl extends Error {
4+
constructor(errorType: ErrorType) {
5+
super();
6+
this.name = 'OtpauthInvalidURL';
7+
this.message = `Given otpauth:// URL is invalid. (Error ${ErrorType[errorType]})`;
8+
}
9+
}

src/lib/url-otpauth-ts/parse.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Forked from https://github.com/huihuimoe/url-otpauth-ng
2+
// Updated to add Steam support
3+
4+
import { ErrorType } from './error-type';
5+
import { OtpAuthInvalidUrl } from './otp-auth-invalid-url';
6+
7+
const PossibleType = <const>['totp', 'hotp', 'yaotp'];
8+
const PossibleDigits = <const>[5, 6, 8];
9+
const PossibleAlgorithms = <const>['SHA1', 'SHA256', 'SHA512', 'Steam'];
10+
11+
const isInArray = <T, A extends T>(item: T, array: ReadonlyArray<A>): item is A => array.includes(item as A);
12+
13+
export type OtpUrlParseResult = {
14+
type: (typeof PossibleType)[number];
15+
account: string;
16+
key: string;
17+
issuer: string;
18+
digits: (typeof PossibleDigits)[number];
19+
algorithm: (typeof PossibleAlgorithms)[number];
20+
period: number;
21+
counter?: number;
22+
};
23+
24+
const DefaultOtpValue: Pick<OtpUrlParseResult, 'digits' | 'algorithm' | 'period'> = {
25+
digits: 6,
26+
algorithm: 'SHA1',
27+
period: 30,
28+
};
29+
30+
/**
31+
* Parses an OTPAuth URI.
32+
*
33+
* Parses an URL as described in Google Authenticator's "KeyUriFormat" document (see:
34+
* [https://github.com/google/google-authenticator/wiki/Key-Uri-Format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format))
35+
* and returns an object that contains the following properties:
36+
*
37+
**/
38+
export function parse(rawUrl: string | URL): OtpUrlParseResult {
39+
const decode = decodeURIComponent;
40+
41+
const parsedOtpValues: Partial<OtpUrlParseResult> = {};
42+
43+
let parsed = new URL(rawUrl);
44+
45+
if (parsed.protocol !== 'otpauth:') {
46+
throw new OtpAuthInvalidUrl(ErrorType.InvalidProtocol);
47+
}
48+
49+
// hack for Chrome
50+
parsed.protocol = 'ftp';
51+
parsed = new URL(parsed);
52+
53+
const otpAlgo = decode(parsed.host);
54+
55+
if (!isInArray(otpAlgo, PossibleType)) {
56+
throw new OtpAuthInvalidUrl(ErrorType.UnknownOtp);
57+
}
58+
59+
parsedOtpValues.type = otpAlgo;
60+
61+
//
62+
// Label (contains account name, may contain issuer)
63+
//
64+
65+
const label = parsed.pathname.substring(1);
66+
// if you want to support mutli commas in label
67+
// const labelComponents = label.split(~label.indexOf(':') ? /:(.*)/ : /%3A(.*)/, 2)
68+
const labelComponents = label.split(~label.indexOf(':') ? ':' : '%3A');
69+
let issuer = '';
70+
let account = '';
71+
72+
if (labelComponents.length === 1) {
73+
account = decode(labelComponents[0]);
74+
} else if (labelComponents.length === 2) {
75+
issuer = decode(labelComponents[0]);
76+
account = decode(labelComponents[1]);
77+
} else {
78+
throw new OtpAuthInvalidUrl(ErrorType.InvalidLabel);
79+
}
80+
81+
if (account.length < 1) {
82+
throw new OtpAuthInvalidUrl(ErrorType.MissingAccountName);
83+
}
84+
85+
if (labelComponents.length === 2 && issuer.length < 1) {
86+
throw new OtpAuthInvalidUrl(ErrorType.InvalidIssuer);
87+
}
88+
89+
parsedOtpValues.account = account;
90+
91+
const parameters = parsed.searchParams;
92+
93+
// Secret key
94+
if (!parameters.has('secret')) {
95+
throw new OtpAuthInvalidUrl(ErrorType.MissingSecretKey);
96+
}
97+
98+
parsedOtpValues.key = parameters.get('secret')!;
99+
100+
// Issuer
101+
if (parameters.has('issuer') && issuer && parameters.get('issuer') !== issuer && issuer !== 'steamctl') {
102+
// If present, it must be equal to the "issuer" specified in the label.
103+
// Exception - steamctl
104+
throw new OtpAuthInvalidUrl(ErrorType.InvalidIssuer);
105+
}
106+
107+
parsedOtpValues.issuer = parameters.get('issuer') || issuer;
108+
109+
// OTP digits
110+
if (parameters.has('digits')) {
111+
const parsedDigits = parseInt(parameters.get('digits')!) || 0;
112+
if (isInArray(parsedDigits, PossibleDigits)) {
113+
parsedOtpValues.digits = parsedDigits;
114+
} else {
115+
throw new OtpAuthInvalidUrl(ErrorType.InvalidDigits);
116+
}
117+
}
118+
119+
// Algorithm to create hash
120+
if (parameters.has('algorithm')) {
121+
const algo = parameters.get('algorithm')!;
122+
if (isInArray(algo, PossibleAlgorithms)) {
123+
// Optional 'algorithm' parameter.
124+
parsedOtpValues.algorithm = algo;
125+
} else {
126+
throw new OtpAuthInvalidUrl(ErrorType.UnknownAlgorithm);
127+
}
128+
} else if (issuer === 'steamctl') {
129+
parsedOtpValues.algorithm = 'Steam';
130+
}
131+
132+
// Period (only for TOTP)
133+
if (otpAlgo === 'totp') {
134+
// Optional 'period' parameter for TOTP.
135+
if (parameters.has('period')) {
136+
parsedOtpValues.period = parseInt(parameters.get('period')!) || 0;
137+
}
138+
}
139+
140+
// Counter (only for HOTP)
141+
if (otpAlgo === 'hotp') {
142+
if (parameters.has('counter')) {
143+
parsedOtpValues.counter = parseInt(parameters.get('counter')!) || 0;
144+
} else {
145+
// We require the 'counter' parameter for HOTP.
146+
throw new OtpAuthInvalidUrl(ErrorType.MissingCounter);
147+
}
148+
}
149+
150+
return { ...DefaultOtpValue, ...parsedOtpValues } as OtpUrlParseResult;
151+
}

src/routes/update/[[id]]/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import { TokenSecretEncoding } from '$models/token-secret-encoding';
1111
import { TokenAutomationFeature } from '$models/token-automation-feature';
1212
import { goto } from '$app/navigation';
13-
import { parse } from 'url-otpauth-ng';
13+
import { parse } from '$lib/url-otpauth-ts/parse';
1414
import { SharedTotpAppClient } from '$stores/totp-shared-client';
1515
import { page } from '$app/stores';
1616
import { blur } from 'svelte/transition';

0 commit comments

Comments
 (0)