Skip to content

Commit

Permalink
Merge pull request #14 from akopachov/ghi-13
Browse files Browse the repository at this point in the history
Implemented #13
  • Loading branch information
akopachov committed Aug 31, 2023
2 parents 57d33a8 + d7cbfd0 commit c9b14cc
Show file tree
Hide file tree
Showing 6 changed files with 265 additions and 101 deletions.
185 changes: 92 additions & 93 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,93 +1,92 @@
{
"name": "flipper-authenticator-companion",
"version": "1.1.1",
"private": true,
"description": "Companion application for Flipper Authenticator software-based TOTP authenticator for Flipper Zero device",
"main": "src/electron.cjs",
"type": "module",
"author": "Alexander Kopachov (@akopachov)",
"scripts": {
"dev": "cross-env NODE_ENV=dev pnpm run dev:all",
"dev:all": "concurrently -n=svelte,electron -c='#ff3e00',blue \"pnpm run dev:svelte\" \"pnpm run dev:electron\"",
"dev:svelte": "vite dev",
"dev:electron": "electron src/electron.cjs",
"build": "cross-env NODE_ENV=production pnpm run build:svelte && pnpm build:electron",
"build:svelte": "vite build",
"build:electron": "electron-builder --config build.config.json",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"update:icons": "node update-aegis-icons.cjs"
},
"browserslist": [
"Chrome 89"
],
"engines": {
"node": ">=18",
"pnpm": ">=8",
"npm": "please-use-pnpm",
"yarn": "please-use-pnpm"
},
"devDependencies": {
"@floating-ui/dom": "^1.5.1",
"@skeletonlabs/skeleton": "^2.0.0",
"@skeletonlabs/tw-plugin": "^0.1.0",
"@sveltejs/adapter-static": "2.0.3",
"@sveltejs/kit": "1.24.0",
"@tailwindcss/forms": "^0.5.6",
"@tailwindcss/typography": "^0.5.9",
"@types/node": "^20.5.7",
"@types/papaparse": "^5.3.8",
"@types/uuid": "^9.0.3",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"async-sema": "^3.1.1",
"autoprefixer": "^10.4.15",
"concurrently": "^8.2.1",
"cross-env": "^7.0.3",
"delay": "^6.0.0",
"electron": "^26.1.0",
"electron-builder": "^24.6.3",
"electron-connect": "^0.6.3",
"electron-packager": "^17.1.2",
"electron-reloader": "^1.2.3",
"escape-string-regexp": "^5.0.0",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-svelte": "^2.33.0",
"papaparse": "^5.4.1",
"postcss": "^8.4.29",
"postcss-load-config": "^4.0.1",
"prettier": "^3.0.3",
"prettier-plugin-svelte": "^3.0.3",
"qr-scanner": "^1.4.2",
"sass": "^1.66.1",
"smart-buffer": "^4.2.0",
"svelte": "^4.2.0",
"svelte-check": "^3.5.1",
"svelte-dnd-action": "^0.9.26",
"svelte-eslint-parser": "^0.33.0",
"svelte-preprocess": "^5.0.4",
"tailwindcss": "^3.3.3",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"url-otpauth-ng": "^3.1.0",
"utc-offsets": "^1.0.0",
"uuid": "^9.0.0",
"vite": "^4.4.9",
"vite-plugin-electron-renderer": "^0.14.5",
"vite-plugin-tailwind-purgecss": "^0.1.3"
},
"dependencies": {
"electron-log": "^4.4.8",
"electron-serve": "^1.1.0",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.1",
"electron-window-state": "^5.0.3",
"node-screenshots": "^0.1.6",
"serialport": "^12.0.0"
},
"optionalDependencies": {
"adm-zip": "^0.5.10"
}
}
{
"name": "flipper-authenticator-companion",
"version": "1.1.1",
"private": true,
"description": "Companion application for Flipper Authenticator software-based TOTP authenticator for Flipper Zero device",
"main": "src/electron.cjs",
"type": "module",
"author": "Alexander Kopachov (@akopachov)",
"scripts": {
"dev": "cross-env NODE_ENV=dev pnpm run dev:all",
"dev:all": "concurrently -n=svelte,electron -c='#ff3e00',blue \"pnpm run dev:svelte\" \"pnpm run dev:electron\"",
"dev:svelte": "vite dev",
"dev:electron": "electron src/electron.cjs",
"build": "cross-env NODE_ENV=production pnpm run build:svelte && pnpm build:electron",
"build:svelte": "vite build",
"build:electron": "electron-builder --config build.config.json",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"update:icons": "node update-aegis-icons.cjs"
},
"browserslist": [
"Chrome 89"
],
"engines": {
"node": ">=18",
"pnpm": ">=8",
"npm": "please-use-pnpm",
"yarn": "please-use-pnpm"
},
"devDependencies": {
"@floating-ui/dom": "^1.5.1",
"@skeletonlabs/skeleton": "^2.0.0",
"@skeletonlabs/tw-plugin": "^0.1.0",
"@sveltejs/adapter-static": "2.0.3",
"@sveltejs/kit": "1.24.0",
"@tailwindcss/forms": "^0.5.6",
"@tailwindcss/typography": "^0.5.9",
"@types/node": "^20.5.7",
"@types/papaparse": "^5.3.8",
"@types/uuid": "^9.0.3",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
"async-sema": "^3.1.1",
"autoprefixer": "^10.4.15",
"concurrently": "^8.2.1",
"cross-env": "^7.0.3",
"delay": "^6.0.0",
"electron": "^26.1.0",
"electron-builder": "^24.6.3",
"electron-connect": "^0.6.3",
"electron-packager": "^17.1.2",
"electron-reloader": "^1.2.3",
"escape-string-regexp": "^5.0.0",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-svelte": "^2.33.0",
"papaparse": "^5.4.1",
"postcss": "^8.4.29",
"postcss-load-config": "^4.0.1",
"prettier": "^3.0.3",
"prettier-plugin-svelte": "^3.0.3",
"qr-scanner": "^1.4.2",
"sass": "^1.66.1",
"smart-buffer": "^4.2.0",
"svelte": "^4.2.0",
"svelte-check": "^3.5.1",
"svelte-dnd-action": "^0.9.26",
"svelte-eslint-parser": "^0.33.0",
"svelte-preprocess": "^5.0.4",
"tailwindcss": "^3.3.3",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"utc-offsets": "^1.0.0",
"uuid": "^9.0.0",
"vite": "^4.4.9",
"vite-plugin-electron-renderer": "^0.14.5",
"vite-plugin-tailwind-purgecss": "^0.1.3"
},
"dependencies": {
"electron-log": "^4.4.8",
"electron-serve": "^1.1.0",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.1",
"electron-window-state": "^5.0.3",
"node-screenshots": "^0.1.6",
"serialport": "^12.0.0"
},
"optionalDependencies": {
"adm-zip": "^0.5.10"
}
}
7 changes: 0 additions & 7 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/lib/url-otpauth-ts/error-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export enum ErrorType {
InvalidIssuer = 0,
InvalidLabel = 1,
InvalidProtocol = 2,
MissingAccountName = 3,
MissingCounter = 4,
MissingIssuer = 5,
MissingSecretKey = 6,
UnknownOtp = 7,
InvalidDigits = 8,
UnknownAlgorithm = 9,
}
9 changes: 9 additions & 0 deletions src/lib/url-otpauth-ts/otp-auth-invalid-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ErrorType } from './error-type';

export class OtpAuthInvalidUrl extends Error {
constructor(errorType: ErrorType) {
super();
this.name = 'OtpauthInvalidURL';
this.message = `Given otpauth:// URL is invalid. (Error ${ErrorType[errorType]})`;
}
}
151 changes: 151 additions & 0 deletions src/lib/url-otpauth-ts/parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// Forked from https://github.com/huihuimoe/url-otpauth-ng
// Updated to add Steam support

import { ErrorType } from './error-type';
import { OtpAuthInvalidUrl } from './otp-auth-invalid-url';

const PossibleType = <const>['totp', 'hotp', 'yaotp'];
const PossibleDigits = <const>[5, 6, 8];
const PossibleAlgorithms = <const>['SHA1', 'SHA256', 'SHA512', 'Steam'];

const isInArray = <T, A extends T>(item: T, array: ReadonlyArray<A>): item is A => array.includes(item as A);

export type OtpUrlParseResult = {
type: (typeof PossibleType)[number];
account: string;
key: string;
issuer: string;
digits: (typeof PossibleDigits)[number];
algorithm: (typeof PossibleAlgorithms)[number];
period: number;
counter?: number;
};

const DefaultOtpValue: Pick<OtpUrlParseResult, 'digits' | 'algorithm' | 'period'> = {
digits: 6,
algorithm: 'SHA1',
period: 30,
};

/**
* Parses an OTPAuth URI.
*
* Parses an URL as described in Google Authenticator's "KeyUriFormat" document (see:
* [https://github.com/google/google-authenticator/wiki/Key-Uri-Format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format))
* and returns an object that contains the following properties:
*
**/
export function parse(rawUrl: string | URL): OtpUrlParseResult {
const decode = decodeURIComponent;

const parsedOtpValues: Partial<OtpUrlParseResult> = {};

let parsed = new URL(rawUrl);

if (parsed.protocol !== 'otpauth:') {
throw new OtpAuthInvalidUrl(ErrorType.InvalidProtocol);
}

// hack for Chrome
parsed.protocol = 'ftp';
parsed = new URL(parsed);

const otpAlgo = decode(parsed.host);

if (!isInArray(otpAlgo, PossibleType)) {
throw new OtpAuthInvalidUrl(ErrorType.UnknownOtp);
}

parsedOtpValues.type = otpAlgo;

//
// Label (contains account name, may contain issuer)
//

const label = parsed.pathname.substring(1);
// if you want to support mutli commas in label
// const labelComponents = label.split(~label.indexOf(':') ? /:(.*)/ : /%3A(.*)/, 2)
const labelComponents = label.split(~label.indexOf(':') ? ':' : '%3A');
let issuer = '';
let account = '';

if (labelComponents.length === 1) {
account = decode(labelComponents[0]);
} else if (labelComponents.length === 2) {
issuer = decode(labelComponents[0]);
account = decode(labelComponents[1]);
} else {
throw new OtpAuthInvalidUrl(ErrorType.InvalidLabel);
}

if (account.length < 1) {
throw new OtpAuthInvalidUrl(ErrorType.MissingAccountName);
}

if (labelComponents.length === 2 && issuer.length < 1) {
throw new OtpAuthInvalidUrl(ErrorType.InvalidIssuer);
}

parsedOtpValues.account = account;

const parameters = parsed.searchParams;

// Secret key
if (!parameters.has('secret')) {
throw new OtpAuthInvalidUrl(ErrorType.MissingSecretKey);
}

parsedOtpValues.key = parameters.get('secret')!;

// Issuer
if (parameters.has('issuer') && issuer && parameters.get('issuer') !== issuer && issuer !== 'steamctl') {
// If present, it must be equal to the "issuer" specified in the label.
// Exception - steamctl
throw new OtpAuthInvalidUrl(ErrorType.InvalidIssuer);
}

parsedOtpValues.issuer = parameters.get('issuer') || issuer;

// OTP digits
if (parameters.has('digits')) {
const parsedDigits = parseInt(parameters.get('digits')!) || 0;
if (isInArray(parsedDigits, PossibleDigits)) {
parsedOtpValues.digits = parsedDigits;
} else {
throw new OtpAuthInvalidUrl(ErrorType.InvalidDigits);
}
}

// Algorithm to create hash
if (parameters.has('algorithm')) {
const algo = parameters.get('algorithm')!;
if (isInArray(algo, PossibleAlgorithms)) {
// Optional 'algorithm' parameter.
parsedOtpValues.algorithm = algo;
} else {
throw new OtpAuthInvalidUrl(ErrorType.UnknownAlgorithm);
}
} else if (issuer === 'steamctl') {
parsedOtpValues.algorithm = 'Steam';
}

// Period (only for TOTP)
if (otpAlgo === 'totp') {
// Optional 'period' parameter for TOTP.
if (parameters.has('period')) {
parsedOtpValues.period = parseInt(parameters.get('period')!) || 0;
}
}

// Counter (only for HOTP)
if (otpAlgo === 'hotp') {
if (parameters.has('counter')) {
parsedOtpValues.counter = parseInt(parameters.get('counter')!) || 0;
} else {
// We require the 'counter' parameter for HOTP.
throw new OtpAuthInvalidUrl(ErrorType.MissingCounter);
}
}

return { ...DefaultOtpValue, ...parsedOtpValues } as OtpUrlParseResult;
}
2 changes: 1 addition & 1 deletion src/routes/update/[[id]]/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { TokenSecretEncoding } from '$models/token-secret-encoding';
import { TokenAutomationFeature } from '$models/token-automation-feature';
import { goto } from '$app/navigation';
import { parse } from 'url-otpauth-ng';
import { parse } from '$lib/url-otpauth-ts/parse';
import { SharedTotpAppClient } from '$stores/totp-shared-client';
import { page } from '$app/stores';
import { blur } from 'svelte/transition';
Expand Down

0 comments on commit c9b14cc

Please sign in to comment.