Display ente authenticator codes (#983)
This commit is contained in:
commit
a5c7c3169e
|
@ -58,6 +58,7 @@
|
|||
"ml-matrix": "^6.8.2",
|
||||
"next": "^13.1.2",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"otpauth": "^9.0.2",
|
||||
"p-queue": "^7.1.0",
|
||||
"photoswipe": "file:./thirdparty/photoswipe",
|
||||
"piexifjs": "^1.0.6",
|
||||
|
@ -77,6 +78,7 @@
|
|||
"similarity-transformation": "^0.0.1",
|
||||
"styled-components": "^5.3.5",
|
||||
"transformation-matrix": "^2.10.0",
|
||||
"vscode-uri": "^3.0.7",
|
||||
"workbox-precaching": "^6.1.5",
|
||||
"workbox-recipes": "^6.1.5",
|
||||
"workbox-routing": "^6.1.5",
|
||||
|
|
|
@ -368,6 +368,7 @@
|
|||
"UPLOAD_DIRS": "Folder",
|
||||
"UPLOAD_GOOGLE_TAKEOUT": "Google takeout",
|
||||
"DEDUPLICATE_FILES": "Deduplicate files",
|
||||
"AUTHENTICATOR_SECTION": "Authenticator",
|
||||
"NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared",
|
||||
"CLUB_BY_CAPTURE_TIME": "Club by capture time",
|
||||
"FILES": "Files",
|
||||
|
|
25
src/components/Authenicator/AuthFooder.tsx
Normal file
25
src/components/Authenicator/AuthFooder.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Button } from '@mui/material';
|
||||
|
||||
export const AuthFooter = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<p>Download our mobile app to add & manage your secrets.</p>
|
||||
<a href="https://github.com/ente-io/auth#-download" download>
|
||||
<Button
|
||||
style={{
|
||||
backgroundColor: 'green',
|
||||
padding: '12px 18px',
|
||||
color: 'white',
|
||||
}}>
|
||||
Download
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
190
src/components/Authenicator/OTPDisplay.tsx
Normal file
190
src/components/Authenicator/OTPDisplay.tsx
Normal file
|
@ -0,0 +1,190 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { TOTP, HOTP } from 'otpauth';
|
||||
import { Code } from 'types/authenticator/code';
|
||||
import TimerProgress from './TimerProgress';
|
||||
|
||||
const TOTPDisplay = ({ issuer, account, code, nextCode }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '4px 16px',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
minWidth: '320px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'rgba(40, 40, 40, 0.6)',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
minWidth: '200px',
|
||||
}}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0px',
|
||||
fontSize: '14px',
|
||||
textAlign: 'left',
|
||||
}}>
|
||||
{issuer}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
marginBottom: '8px',
|
||||
textAlign: 'left',
|
||||
fontSize: '12px',
|
||||
}}>
|
||||
{account}
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'left',
|
||||
}}>
|
||||
{code}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ flex: 1 }} />
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '32px',
|
||||
alignItems: 'flex-end',
|
||||
minWidth: '120px',
|
||||
textAlign: 'right',
|
||||
}}>
|
||||
<p
|
||||
style={{
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0px',
|
||||
fontSize: '10px',
|
||||
marginTop: 'auto',
|
||||
textAlign: 'right',
|
||||
}}>
|
||||
next
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '0px',
|
||||
marginTop: 'auto',
|
||||
textAlign: 'right',
|
||||
}}>
|
||||
{nextCode}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function BadCodeInfo({ codeInfo, codeErr }) {
|
||||
const [showRawData, setShowRawData] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="code-info">
|
||||
<div>{codeInfo.title}</div>
|
||||
<div>{codeErr}</div>
|
||||
<div>
|
||||
{showRawData ? (
|
||||
<div onClick={() => setShowRawData(false)}>
|
||||
{codeInfo.rawData ?? 'no raw data'}
|
||||
</div>
|
||||
) : (
|
||||
<div onClick={() => setShowRawData(true)}>Show rawData</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface OTPDisplayProps {
|
||||
codeInfo: Code;
|
||||
}
|
||||
|
||||
const OTPDisplay = (props: OTPDisplayProps) => {
|
||||
const { codeInfo } = props;
|
||||
const [code, setCode] = useState('');
|
||||
const [nextCode, setNextCode] = useState('');
|
||||
const [codeErr, setCodeErr] = useState('');
|
||||
|
||||
const generateCodes = () => {
|
||||
try {
|
||||
const currentTime = new Date().getTime();
|
||||
if (codeInfo.type.toLowerCase() === 'totp') {
|
||||
const totp = new TOTP({
|
||||
secret: codeInfo.secret,
|
||||
algorithm: codeInfo.algorithm ?? Code.defaultAlgo,
|
||||
period: codeInfo.period ?? Code.defaultPeriod,
|
||||
digits: codeInfo.digits ?? Code.defaultDigits,
|
||||
});
|
||||
setCode(totp.generate());
|
||||
setNextCode(
|
||||
totp.generate({
|
||||
timestamp: currentTime + codeInfo.period * 1000,
|
||||
})
|
||||
);
|
||||
} else if (codeInfo.type.toLowerCase() === 'hotp') {
|
||||
const hotp = new HOTP({
|
||||
secret: codeInfo.secret,
|
||||
counter: 0,
|
||||
algorithm: codeInfo.algorithm,
|
||||
});
|
||||
setCode(hotp.generate());
|
||||
setNextCode(hotp.generate({ counter: 1 }));
|
||||
}
|
||||
} catch (err) {
|
||||
setCodeErr(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// this is to set the initial code and nextCode on component mount
|
||||
generateCodes();
|
||||
const codeType = codeInfo.type;
|
||||
const codePeriodInMs = codeInfo.period * 1000;
|
||||
const timeToNextCode =
|
||||
codePeriodInMs - (new Date().getTime() % codePeriodInMs);
|
||||
const intervalId = null;
|
||||
// wait until we are at the start of the next code period,
|
||||
// and then start the interval loop
|
||||
setTimeout(() => {
|
||||
// we need to call generateCodes() once before the interval loop
|
||||
// to set the initial code and nextCode
|
||||
generateCodes();
|
||||
codeType.toLowerCase() === 'totp' ||
|
||||
codeType.toLowerCase() === 'hotp'
|
||||
? setInterval(() => {
|
||||
generateCodes();
|
||||
}, codePeriodInMs)
|
||||
: null;
|
||||
}, timeToNextCode);
|
||||
|
||||
return () => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
};
|
||||
}, [codeInfo]);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '8px' }}>
|
||||
<TimerProgress period={codeInfo.period ?? Code.defaultPeriod} />
|
||||
{codeErr === '' ? (
|
||||
<TOTPDisplay
|
||||
issuer={codeInfo.issuer}
|
||||
account={codeInfo.account}
|
||||
code={code}
|
||||
nextCode={nextCode}
|
||||
/>
|
||||
) : (
|
||||
<BadCodeInfo codeInfo={codeInfo} codeErr={codeErr} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OTPDisplay;
|
46
src/components/Authenicator/TimerProgress.tsx
Normal file
46
src/components/Authenicator/TimerProgress.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const TimerProgress = ({ period }) => {
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [ticker, setTicker] = useState(null);
|
||||
const microSecondsInPeriod = period * 1000000;
|
||||
|
||||
const startTicker = () => {
|
||||
const ticker = setInterval(() => {
|
||||
updateTimeRemaining();
|
||||
}, 10);
|
||||
setTicker(ticker);
|
||||
};
|
||||
|
||||
const updateTimeRemaining = () => {
|
||||
const timeRemaining =
|
||||
microSecondsInPeriod -
|
||||
((new Date().getTime() * 1000) % microSecondsInPeriod);
|
||||
setProgress(timeRemaining / microSecondsInPeriod);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
startTicker();
|
||||
return () => clearInterval(ticker);
|
||||
}, []);
|
||||
|
||||
const color = progress > 0.4 ? 'green' : 'orange';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '3px',
|
||||
backgroundColor: 'transparent',
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width: `${progress * 100}%`,
|
||||
height: '100%',
|
||||
backgroundColor: color,
|
||||
}}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimerProgress;
|
|
@ -63,6 +63,8 @@ export default function UtilitySection({ closeSidebar }) {
|
|||
|
||||
const redirectToDeduplicatePage = () => router.push(PAGES.DEDUPLICATE);
|
||||
|
||||
const redirectToAuthenticatorPage = () => router.push(PAGES.AUTH);
|
||||
|
||||
const somethingWentWrong = () =>
|
||||
setDialogMessage({
|
||||
title: t('ERROR'),
|
||||
|
@ -98,6 +100,11 @@ export default function UtilitySection({ closeSidebar }) {
|
|||
<SidebarButton onClick={redirectToDeduplicatePage}>
|
||||
{t('DEDUPLICATE_FILES')}
|
||||
</SidebarButton>
|
||||
{isInternalUser() && (
|
||||
<SidebarButton onClick={redirectToAuthenticatorPage}>
|
||||
{t('AUTHENTICATOR_SECTION')}
|
||||
</SidebarButton>
|
||||
)}
|
||||
<SidebarButton onClick={openPreferencesOptions}>
|
||||
{t('PREFERENCES')}
|
||||
</SidebarButton>
|
||||
|
|
|
@ -14,4 +14,6 @@ export enum PAGES {
|
|||
SHARED_ALBUMS = '/shared-albums',
|
||||
// ML_DEBUG = '/ml-debug',
|
||||
DEDUPLICATE = '/deduplicate',
|
||||
// AUTH page is used to show (auth)enticator codes
|
||||
AUTH = '/auth',
|
||||
}
|
||||
|
|
91
src/pages/auth/index.tsx
Normal file
91
src/pages/auth/index.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import OTPDisplay from 'components/Authenicator/OTPDisplay';
|
||||
import { getAuthCodes } from 'services/authenticator/authenticatorService';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AuthFooter } from 'components/Authenicator/AuthFooder';
|
||||
|
||||
const AuthenticatorCodesPage = () => {
|
||||
const router = useRouter();
|
||||
const [codes, setCodes] = useState([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCodes = async () => {
|
||||
try {
|
||||
const res = await getAuthCodes();
|
||||
setCodes(res);
|
||||
} catch (err) {
|
||||
if (err.message === CustomError.KEY_MISSING) {
|
||||
router.push({
|
||||
pathname: PAGES.CREDENTIALS,
|
||||
query: { redirectPage: PAGES.AUTH },
|
||||
});
|
||||
} else {
|
||||
// do not log errors
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchCodes();
|
||||
}, []);
|
||||
|
||||
const filteredCodes = codes.filter(
|
||||
(secret) =>
|
||||
(secret.issuer ?? '')
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()) ||
|
||||
(secret.account ?? '')
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
}}>
|
||||
<div style={{ marginBottom: '2rem' }} />
|
||||
<h2>ente Authenticator</h2>
|
||||
{filteredCodes.length === 0 && searchTerm.length === 0 ? (
|
||||
<></>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '1rem' }} />
|
||||
{filteredCodes.length === 0 ? (
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
textAlign: 'center',
|
||||
marginTop: '32px',
|
||||
}}>
|
||||
{searchTerm.length !== 0 ? (
|
||||
<p>No results found.</p>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
filteredCodes.map((code) => (
|
||||
<OTPDisplay codeInfo={code} key={code.id} />
|
||||
))
|
||||
)}
|
||||
<div style={{ marginBottom: '2rem' }} />
|
||||
<AuthFooter />
|
||||
<div style={{ marginBottom: '4rem' }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticatorCodesPage;
|
|
@ -31,6 +31,8 @@ import VerifyMasterPasswordForm, {
|
|||
|
||||
export default function Credentials() {
|
||||
const router = useRouter();
|
||||
const routerRedirectPage =
|
||||
router.query.redirectPage?.toString() ?? PAGES.GALLERY;
|
||||
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
|
||||
const appContext = useContext(AppContext);
|
||||
const [user, setUser] = useState<User>();
|
||||
|
@ -86,7 +88,7 @@ export default function Credentials() {
|
|||
await decryptAndStoreToken(key);
|
||||
const redirectURL = appContext.redirectURL;
|
||||
appContext.setRedirectURL(null);
|
||||
router.push(redirectURL ?? PAGES.GALLERY);
|
||||
router.push(redirectURL ?? routerRedirectPage ?? PAGES.GALLERY);
|
||||
} catch (e) {
|
||||
logError(e, 'useMasterPassword failed');
|
||||
}
|
||||
|
|
101
src/services/authenticator/authenticatorService.ts
Normal file
101
src/services/authenticator/authenticatorService.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import HTTPService from 'services/HTTPService';
|
||||
import { AuthEntity, AuthKey } from 'types/authenticator/api';
|
||||
import { Code } from 'types/authenticator/code';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import { getActualKey, getToken } from 'utils/common/key';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
export const getAuthCodes = async (): Promise<Code[]> => {
|
||||
const masterKey = await getActualKey();
|
||||
try {
|
||||
const authKeyData = await getAuthKey();
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const authenticatorKey = await cryptoWorker.decryptB64(
|
||||
authKeyData.encryptedKey,
|
||||
authKeyData.header,
|
||||
masterKey
|
||||
);
|
||||
// always fetch all data from server for now
|
||||
const authEntity: AuthEntity[] = await getDiff(0);
|
||||
const authCodes = await Promise.all(
|
||||
authEntity
|
||||
.filter((f) => !f.isDeleted)
|
||||
.map(async (entity) => {
|
||||
try {
|
||||
const decryptedCode =
|
||||
await cryptoWorker.decryptMetadata(
|
||||
entity.encryptedData,
|
||||
entity.header,
|
||||
authenticatorKey
|
||||
);
|
||||
return Code.fromRawData(entity.id, decryptedCode);
|
||||
} catch (e) {
|
||||
logError(
|
||||
Error('failed to parse code'),
|
||||
'codeId = ' + entity.id
|
||||
);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((f) => f !== null || f !== undefined)
|
||||
);
|
||||
// sort by issuer name which can be undefined also
|
||||
authCodes.sort((a, b) => {
|
||||
if (a.issuer && b.issuer) {
|
||||
return a.issuer.localeCompare(b.issuer);
|
||||
}
|
||||
if (a.issuer) {
|
||||
return -1;
|
||||
}
|
||||
if (b.issuer) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return authCodes;
|
||||
} catch (e) {
|
||||
logError(e, 'get authenticator entities failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getAuthKey = async (): Promise<AuthKey> => {
|
||||
try {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/authenticator/key`,
|
||||
{},
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
}
|
||||
);
|
||||
return resp.data;
|
||||
} catch (e) {
|
||||
logError(e, 'Get key failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
// return a promise which resolves to list of AuthEnitity
|
||||
export const getDiff = async (
|
||||
sinceTime: number,
|
||||
limit = 2500
|
||||
): Promise<AuthEntity[]> => {
|
||||
try {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/authenticator/entity/diff`,
|
||||
{
|
||||
sinceTime,
|
||||
limit,
|
||||
},
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
}
|
||||
);
|
||||
return resp.data.diff;
|
||||
} catch (e) {
|
||||
logError(e, 'Get diff failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
13
src/types/authenticator/api.ts
Normal file
13
src/types/authenticator/api.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export interface AuthEntity {
|
||||
id: string;
|
||||
encryptedData: string | null;
|
||||
header: string | null;
|
||||
isDeleted: boolean;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
export interface AuthKey {
|
||||
encryptedKey: string;
|
||||
header: string;
|
||||
}
|
182
src/types/authenticator/code.ts
Normal file
182
src/types/authenticator/code.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
import { URI } from 'vscode-uri';
|
||||
|
||||
type Type = 'totp' | 'TOTP' | 'hotp' | 'HOTP';
|
||||
|
||||
type AlgorithmType =
|
||||
| 'sha1'
|
||||
| 'SHA1'
|
||||
| 'sha256'
|
||||
| 'SHA256'
|
||||
| 'sha512'
|
||||
| 'SHA512';
|
||||
|
||||
export class Code {
|
||||
static readonly defaultDigits = 6;
|
||||
static readonly defaultAlgo = 'sha1';
|
||||
static readonly defaultPeriod = 30;
|
||||
|
||||
// id for the corresponding auth entity
|
||||
id?: String;
|
||||
account: string;
|
||||
issuer: string;
|
||||
digits?: number;
|
||||
period: number;
|
||||
secret: string;
|
||||
algorithm: AlgorithmType;
|
||||
type: Type;
|
||||
rawData?: string;
|
||||
|
||||
constructor(
|
||||
account: string,
|
||||
issuer: string,
|
||||
digits: number | undefined,
|
||||
period: number,
|
||||
secret: string,
|
||||
algorithm: AlgorithmType,
|
||||
type: Type,
|
||||
rawData?: string,
|
||||
id?: string
|
||||
) {
|
||||
this.account = account;
|
||||
this.issuer = issuer;
|
||||
this.digits = digits;
|
||||
this.period = period;
|
||||
this.secret = secret;
|
||||
this.algorithm = algorithm;
|
||||
this.type = type;
|
||||
this.rawData = rawData;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
static fromRawData(id: string, rawData: string): Code {
|
||||
let santizedRawData = rawData
|
||||
.replace(/\+/g, '%2B')
|
||||
.replace(/:/g, '%3A')
|
||||
.replaceAll('\r', '');
|
||||
if (santizedRawData.startsWith('"')) {
|
||||
santizedRawData = santizedRawData.substring(1);
|
||||
}
|
||||
if (santizedRawData.endsWith('"')) {
|
||||
santizedRawData = santizedRawData.substring(
|
||||
0,
|
||||
santizedRawData.length - 1
|
||||
);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
return new Code(
|
||||
Code._getAccount(uriPath),
|
||||
Code._getIssuer(uriPath, uriParams),
|
||||
Code._getDigits(uriParams),
|
||||
Code._getPeriod(uriParams),
|
||||
Code.getSanitizedSecret(uriParams),
|
||||
Code._getAlgorithm(uriParams),
|
||||
Code._getType(uriPath),
|
||||
rawData,
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
private static _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 '';
|
||||
}
|
||||
}
|
||||
|
||||
private static _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;
|
||||
}
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
|
||||
private static _getDigits(uriParams): number {
|
||||
try {
|
||||
return parseInt(uriParams['digits'], 10) || Code.defaultDigits;
|
||||
} catch (e) {
|
||||
return Code.defaultDigits;
|
||||
}
|
||||
}
|
||||
|
||||
private static _getPeriod(uriParams): number {
|
||||
try {
|
||||
return parseInt(uriParams['period'], 10) || Code.defaultPeriod;
|
||||
} catch (e) {
|
||||
return Code.defaultPeriod;
|
||||
}
|
||||
}
|
||||
|
||||
private static _getAlgorithm(uriParams): AlgorithmType {
|
||||
try {
|
||||
const algorithm = uriParams['algorithm'].toLowerCase();
|
||||
if (algorithm === 'sha256') {
|
||||
return algorithm;
|
||||
} else if (algorithm === 'sha512') {
|
||||
return algorithm;
|
||||
}
|
||||
} catch (e) {
|
||||
// nothing
|
||||
}
|
||||
return 'sha1';
|
||||
}
|
||||
|
||||
private static _getType(uriPath: string): Type {
|
||||
const oauthType = uriPath.split('/')[0].substring(0);
|
||||
if (oauthType === 'totp') {
|
||||
return 'totp';
|
||||
} else if (oauthType === 'hotp') {
|
||||
return 'hotp';
|
||||
}
|
||||
throw new Error(`Unsupported format with host ${oauthType}`);
|
||||
}
|
||||
|
||||
static getSanitizedSecret(uriParams): string {
|
||||
return uriParams['secret'].replace(/ /g, '').toUpperCase();
|
||||
}
|
||||
}
|
17
yarn.lock
17
yarn.lock
|
@ -3214,6 +3214,11 @@ json5@^1.0.1:
|
|||
dependencies:
|
||||
minimist "^1.2.0"
|
||||
|
||||
jssha@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/jssha/-/jssha-3.3.0.tgz#44b5531bcf55a12f4a388476c647a9a1cca92839"
|
||||
integrity sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==
|
||||
|
||||
"jsx-ast-utils@^2.4.1 || ^3.0.0":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz"
|
||||
|
@ -3736,6 +3741,13 @@ optionator@^0.9.1:
|
|||
type-check "^0.4.0"
|
||||
word-wrap "^1.2.3"
|
||||
|
||||
otpauth@^9.0.2:
|
||||
version "9.0.2"
|
||||
resolved "https://registry.yarnpkg.com/otpauth/-/otpauth-9.0.2.tgz#5f369bdeb74513fb6c0f25c5ae5ac6b3780ebc89"
|
||||
integrity sha512-0TzpkJYg24VvIK3/K91HKpTtMlwm73UoThhcGY8fZsXcwHDrqf008rfdOjj3NnQuyuT11+vHyyO//qRzi6OZ9A==
|
||||
dependencies:
|
||||
jssha "~3.3.0"
|
||||
|
||||
p-limit@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
|
||||
|
@ -4880,6 +4892,11 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
|
|||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
vscode-uri@^3.0.7:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8"
|
||||
integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==
|
||||
|
||||
void-elements@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
|
||||
|
|
Loading…
Reference in a new issue