Compare commits
No commits in common. "main" and "from-mirror" have entirely different histories.
main
...
from-mirro
17
README.md
@ -1,22 +1,17 @@
|
|||||||
# TOTPFIT
|
# TOTPFIT
|
||||||
|
|
||||||
### Another 2FAuthenticator based on TOTP for Zepp OS with Google Authenticator and Proton Authenticator support
|
### Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4 with Google Authenticator migration support
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Features:
|
### Features:
|
||||||
|
|
||||||
- Support of `otpauth://` links with parameters "client", "issuer", "algorithm", "digits", "period", "offset"
|
- Supports of `otpauth://` links with parameters "client", "issuer", "algorithm", "digits", "period", "offset"
|
||||||
- Support of Google Authenticator migration links formated: `otpauth-migration://offline?data=...`
|
- Addition/Edition/Deletion of TOTPs from mobile app
|
||||||
- Support of **Proton Authenticator** export with parameters "client", "issuer", "algorithm", "digits", "period", "offset" **(BETA)**
|
- Support of Google Authenticator migration links formated: `otpauth-migration://offline?data=...` (At this stage with only 6 digits and only 30 seconds period)
|
||||||
- Addition/Sort/Edition/Deletion of TOTPs from mobile app
|
|
||||||
|
|
||||||
### Guides:
|
### Guides:
|
||||||
|
|
||||||
[How to add 2FA TOTP records (keys) on app](/docs/guides/how-to-add-totps/README.md)
|
[How to add 2FA TOTP records (keys) on app](/docs/guides/how-to-add-totps/README.md)
|
||||||
|
|
||||||
#### This repo has mirror for public issues on [GitHub](https://github.com/Lisoveliy/totpfit)
|
#### This repo has mirror for issues on [GitHub](https://github.com/Lisoveliy/totpfit)
|
||||||
|
|
||||||
#### To contribute on this repo you must use my [Gitea](https://git.lisoveliy.su/Lisoveliy/totpfit)
|
|
||||||
|
|
||||||
Don't be shy to contact me for access!
|
|
||||||
|
7
app.json
@ -6,7 +6,7 @@
|
|||||||
"appType": "app",
|
"appType": "app",
|
||||||
"version": {
|
"version": {
|
||||||
"code": 1,
|
"code": 1,
|
||||||
"name": "1.4.0"
|
"name": "1.3.1"
|
||||||
},
|
},
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"vender": "zepp",
|
"vender": "zepp",
|
||||||
@ -37,11 +37,6 @@
|
|||||||
{
|
{
|
||||||
"st": "s",
|
"st": "s",
|
||||||
"dw": 390
|
"dw": 390
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"st": "r",
|
|
||||||
"dw": 390
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 76 KiB |
Before Width: | Height: | Size: 115 KiB |
BIN
docs/assets/image.png
Normal file
After Width: | Height: | Size: 27 KiB |
@ -14,9 +14,11 @@ Then press OK, record will appear on page
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
You can edit your otpauth:// records using button "Change TOTP link". Your previous record will be replaced with a new otpauth:// link from text field, and previous link will not be shown on field.
|
||||||
|
|
||||||
### If you use google migrations (otpauth-migration:// links)
|
### If you use google migrations (otpauth-migration:// links)
|
||||||
|
|
||||||
To add 2FA TOTP records using migration from Google Authenticator app, you must go to menu, select "Transfer accounts" -> "Export accounts"
|
To add 2FA TOTP recods using migration from Google Authenticator app, you must go to menu, select "Transfer accounts" -> "Export accounts"
|
||||||
|
|
||||||
Select codes then screenshot QR code and scan (decode) it to a URI. Use any app providing scan from image, ex: "Search screen" function (Google Lens) on Google Assistant.
|
Select codes then screenshot QR code and scan (decode) it to a URI. Use any app providing scan from image, ex: "Search screen" function (Google Lens) on Google Assistant.
|
||||||
|
|
||||||
@ -32,23 +34,4 @@ Then press OK, all selected records from Google Authenticator will appear on pag
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### If you use Proton Authenticator
|
You can edit your records using button "Change TOTP link". Your previous record will be replaced with a new otpauth:// link from text field (otpauth-migration:// will not work), and previous link will not be shown on field.
|
||||||
|
|
||||||
To add 2FA TOTP records from Proton Authenticator you must go to settings and press "Export":
|
|
||||||

|
|
||||||
|
|
||||||
After this save file and open it on text editor:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Copy all stuff from file at clipboard and import in application:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Then press OK, records will appear on page:
|
|
||||||
|
|
||||||

|
|
||||||
|
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 87 KiB |
BIN
new_icon_for_appstore.png
Normal file
After Width: | Height: | Size: 21 KiB |
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "totpfit",
|
"name": "totpfit",
|
||||||
"version": "1.4.0",
|
"version": "1.3.1",
|
||||||
"description": "Another 2FAuthenticator based on TOTP for Zepp OS with Google Authenticator and Proton Authenticator support",
|
"description": "Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"author": "Lisoveliy",
|
"author": "Lisoveliy",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -10,9 +10,7 @@ export const colors = {
|
|||||||
|
|
||||||
export const content = {
|
export const content = {
|
||||||
addTotpsHint:
|
addTotpsHint:
|
||||||
"For add a 2FA TOTP record you must have otpauth:// link otpauth-migration://" +
|
"For add a 2FA TOTP record you must have otpauth:// link or otpauth-migration:// link from Google Authenticator Migration QR-Code",
|
||||||
"link from Google Authenticator Migration QR-Code or Proton authenticator Export JSON string." +
|
|
||||||
"If you have a questions - check instruction below!",
|
|
||||||
totpRecordsHint: "TOTP records:",
|
totpRecordsHint: "TOTP records:",
|
||||||
createButton: {
|
createButton: {
|
||||||
placeHolder: "otpauth(-migration)://",
|
placeHolder: "otpauth(-migration)://",
|
||||||
@ -26,14 +24,6 @@ export const content = {
|
|||||||
label: "Change TOTP link",
|
label: "Change TOTP link",
|
||||||
placeHolder: "otpauth(-migration)://",
|
placeHolder: "otpauth(-migration)://",
|
||||||
},
|
},
|
||||||
renameButtons: {
|
|
||||||
rename: "Rename",
|
|
||||||
renameIssuer: "Rename Issuer",
|
|
||||||
renameClient: "Rename Client",
|
|
||||||
},
|
|
||||||
saveButton: {
|
|
||||||
label: "Save",
|
|
||||||
},
|
|
||||||
deleteButton: {
|
deleteButton: {
|
||||||
label: "Delete",
|
label: "Delete",
|
||||||
},
|
},
|
||||||
|
@ -31,7 +31,7 @@ function GetTOTPList(storage) {
|
|||||||
editingIndex = index;
|
editingIndex = index;
|
||||||
tempIssuer = element.issuer;
|
tempIssuer = element.issuer;
|
||||||
tempClient = element.client;
|
tempClient = element.client;
|
||||||
_props.settingsStorage.setItem("requestUpdate", Math.random());
|
updateStorage(storage);
|
||||||
},
|
},
|
||||||
onSave: () => {
|
onSave: () => {
|
||||||
storage[index].issuer = tempIssuer;
|
storage[index].issuer = tempIssuer;
|
||||||
@ -61,7 +61,6 @@ function GetTOTPList(storage) {
|
|||||||
updateStorage(storage);
|
updateStorage(storage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isEditInProgress: editingIndex !== -1,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -98,6 +97,11 @@ AppSettingsPage({
|
|||||||
try {
|
try {
|
||||||
errorMessage = "";
|
errorMessage = "";
|
||||||
let link = getTOTPByLink(changes);
|
let link = getTOTPByLink(changes);
|
||||||
|
if (link == null) {
|
||||||
|
throw new Error(
|
||||||
|
"Unsupported link type. Please use an otpauth:// or otpauth-migration:// link",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (Array.isArray(link)) {
|
if (Array.isArray(link)) {
|
||||||
storage.push(...link);
|
storage.push(...link);
|
||||||
@ -115,9 +119,7 @@ AppSettingsPage({
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
marginBottom: "10px",
|
margin: "10px",
|
||||||
marginLeft: "10px",
|
|
||||||
marginRight: "10px",
|
|
||||||
fontSize: "20px",
|
fontSize: "20px",
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
@ -135,7 +137,7 @@ AppSettingsPage({
|
|||||||
},
|
},
|
||||||
errorMessage,
|
errorMessage,
|
||||||
)
|
)
|
||||||
: null;
|
: null; //TODO: Check for work
|
||||||
|
|
||||||
const bottomContainer = View(
|
const bottomContainer = View(
|
||||||
{
|
{
|
||||||
@ -149,8 +151,8 @@ AppSettingsPage({
|
|||||||
style: {
|
style: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
marginTop: "10px",
|
marginTop: "20px",
|
||||||
marginBottom: "10px",
|
marginBottom: "20px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Link(
|
Link(
|
||||||
|
@ -14,7 +14,6 @@ export function createTOTPCard({
|
|||||||
onMoveDown,
|
onMoveDown,
|
||||||
onIssuerChange,
|
onIssuerChange,
|
||||||
onClientChange,
|
onClientChange,
|
||||||
isEditInProgress,
|
|
||||||
}) {
|
}) {
|
||||||
const infoView = View(
|
const infoView = View(
|
||||||
{
|
{
|
||||||
@ -27,7 +26,7 @@ export function createTOTPCard({
|
|||||||
isEditing
|
isEditing
|
||||||
? [
|
? [
|
||||||
TextInput({
|
TextInput({
|
||||||
label: content.renameButtons.renameIssuer,
|
label: "Rename Issuer",
|
||||||
value: tempIssuer,
|
value: tempIssuer,
|
||||||
onChange: onIssuerChange,
|
onChange: onIssuerChange,
|
||||||
labelStyle: {
|
labelStyle: {
|
||||||
@ -40,14 +39,14 @@ export function createTOTPCard({
|
|||||||
color: colors.text,
|
color: colors.text,
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
height: "40px",
|
height: "40px",
|
||||||
width: "200px",
|
width: "200px"
|
||||||
},
|
},
|
||||||
subStyle: {
|
subStyle: {
|
||||||
display: "none",
|
display: "none",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
TextInput({
|
TextInput({
|
||||||
label: content.renameButtons.renameClient,
|
label: "Rename client",
|
||||||
value: tempClient,
|
value: tempClient,
|
||||||
onChange: onClientChange,
|
onChange: onClientChange,
|
||||||
labelStyle: {
|
labelStyle: {
|
||||||
@ -60,7 +59,7 @@ export function createTOTPCard({
|
|||||||
color: colors.text,
|
color: colors.text,
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
height: "40px",
|
height: "40px",
|
||||||
width: "200px",
|
width: "200px"
|
||||||
},
|
},
|
||||||
subStyle: {
|
subStyle: {
|
||||||
display: "none",
|
display: "none",
|
||||||
@ -90,7 +89,7 @@ export function createTOTPCard({
|
|||||||
isEditing
|
isEditing
|
||||||
? [
|
? [
|
||||||
Button({
|
Button({
|
||||||
label: content.saveButton.label,
|
label: "Save",
|
||||||
style: {
|
style: {
|
||||||
margin: "5px",
|
margin: "5px",
|
||||||
backgroundColor: "#28a745",
|
backgroundColor: "#28a745",
|
||||||
@ -101,7 +100,7 @@ export function createTOTPCard({
|
|||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
Button({
|
Button({
|
||||||
label: content.renameButtons.rename,
|
label: "Rename",
|
||||||
style: {
|
style: {
|
||||||
margin: "5px",
|
margin: "5px",
|
||||||
backgroundColor: colors.notify,
|
backgroundColor: colors.notify,
|
||||||
@ -109,17 +108,15 @@ export function createTOTPCard({
|
|||||||
},
|
},
|
||||||
onClick: onRename,
|
onClick: onRename,
|
||||||
}),
|
}),
|
||||||
!isEditInProgress
|
Button({
|
||||||
? Button({
|
label: "Delete",
|
||||||
label: content.deleteButton.label,
|
|
||||||
style: {
|
style: {
|
||||||
margin: "5px",
|
margin: "5px",
|
||||||
backgroundColor: colors.alert,
|
backgroundColor: colors.alert,
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
},
|
},
|
||||||
onClick: onDelete,
|
onClick: onDelete,
|
||||||
})
|
}),
|
||||||
: null,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -130,6 +127,7 @@ export function createTOTPCard({
|
|||||||
label: "⬆",
|
label: "⬆",
|
||||||
disabled: index === 0,
|
disabled: index === 0,
|
||||||
style: {
|
style: {
|
||||||
|
width: "50px",
|
||||||
margin: "2px",
|
margin: "2px",
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
backgroundColor: colors.notify,
|
backgroundColor: colors.notify,
|
||||||
@ -140,6 +138,7 @@ export function createTOTPCard({
|
|||||||
label: "⬇",
|
label: "⬇",
|
||||||
disabled: index === storage.length - 1,
|
disabled: index === storage.length - 1,
|
||||||
style: {
|
style: {
|
||||||
|
width: "50px",
|
||||||
margin: "2px",
|
margin: "2px",
|
||||||
color: colors.text,
|
color: colors.text,
|
||||||
backgroundColor: colors.notify,
|
backgroundColor: colors.notify,
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
export class ProtonBackupExport {
|
|
||||||
version;
|
|
||||||
entries = Array.of(ProtonTotpRecord);
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ProtonTotpRecord {
|
|
||||||
content = {
|
|
||||||
uri,
|
|
||||||
entry_type,
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,139 +1,11 @@
|
|||||||
import { decodeProto, TYPES } from "../../lib/protobuf-decoder/protobufDecoder";
|
import { decodeProto, TYPES } from "../../lib/protobuf-decoder/protobufDecoder";
|
||||||
import { TOTP } from "../../lib/totp-quickjs";
|
import { TOTP } from "../../lib/totp-quickjs";
|
||||||
import { base64decode, encode } from "../../lib/totp-quickjs/base32decoder";
|
import { base64decode, encode } from "../../lib/totp-quickjs/base32decoder";
|
||||||
import { ProtonBackupExport } from "./protonBackupExport";
|
|
||||||
|
|
||||||
const otpauthScheme = "otpauth://";
|
const otpauthScheme = "otpauth://";
|
||||||
const googleMigrationScheme = "otpauth-migration://";
|
const googleMigrationScheme = "otpauth-migration://";
|
||||||
export function getTOTPByLink(link) {
|
|
||||||
try {
|
|
||||||
//proton export
|
|
||||||
const json = JSON.parse(link);
|
|
||||||
console.log(json);
|
|
||||||
return getByProtonBackup(json);
|
|
||||||
} catch (e) {
|
|
||||||
if (link.startsWith(googleMigrationScheme)) {
|
|
||||||
//google migration export
|
|
||||||
return getByGoogleMigrationScheme(link);
|
|
||||||
}
|
|
||||||
if (link.startsWith(otpauthScheme)) {
|
|
||||||
//otpauth export
|
|
||||||
return getByOtpauthScheme(link);
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
`Unsupported link type. Please use an otpauth:// or otpauth-migration:// link\n ERR: ${e}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHashType(algorithm) {
|
function _parseSingleMigrationEntry(part) {
|
||||||
if (algorithm == "SHA1") return "SHA-1";
|
|
||||||
if (algorithm == "SHA256") return "SHA-256";
|
|
||||||
if (algorithm == "SHA512") return "SHA-512";
|
|
||||||
else return "SHA-1";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getByProtonBackup(protonjson) {
|
|
||||||
try {
|
|
||||||
if ("entries" in protonjson && protonjson.version == 1) {
|
|
||||||
//Is proton export?
|
|
||||||
console.log(1);
|
|
||||||
const protonBE = Object.assign(
|
|
||||||
new ProtonBackupExport(),
|
|
||||||
protonjson,
|
|
||||||
);
|
|
||||||
const res = protonBE.entries.map((x) => {
|
|
||||||
return getByOtpauthScheme(x.content.uri);
|
|
||||||
});
|
|
||||||
console.log(res);
|
|
||||||
return res;
|
|
||||||
} else throw new Error("use proton export backup with version: 1");
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
throw new Error(`Unsupported JSON type: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getByOtpauthScheme(link) {
|
|
||||||
try {
|
|
||||||
let args = link.split("?");
|
|
||||||
let path = args[0];
|
|
||||||
let params = args[1];
|
|
||||||
|
|
||||||
let pathParts = path.split("/");
|
|
||||||
let type = pathParts[2]; //hotp or totp
|
|
||||||
let label = decodeURIComponent(pathParts[3]);
|
|
||||||
|
|
||||||
let issuerFromLabel = label.includes(":") ? label.split(":")[0] : null;
|
|
||||||
let client = label.includes(":") ? label.split(":")[1].trim() : label;
|
|
||||||
client = decodeURIComponent(client);
|
|
||||||
|
|
||||||
let secret = params.match(/secret=([^&]*)/)?.[1];
|
|
||||||
let issuerFromParams = params.match(/issuer=([^&]*)/)?.[1];
|
|
||||||
|
|
||||||
let issuer = issuerFromParams
|
|
||||||
? decodeURIComponent(issuerFromParams)
|
|
||||||
: decodeURIComponent(issuerFromLabel);
|
|
||||||
if (!issuer) issuer = client;
|
|
||||||
|
|
||||||
let period = params.match(/period=([^&]*)/)?.[1];
|
|
||||||
let digits = params.match(/digits=([^&]*)/)?.[1];
|
|
||||||
let algorithm = params.match(/algorithm=([^&]*)/)?.[1];
|
|
||||||
let offset = params.match(/offset=([^&]*)/)?.[1] ?? 0;
|
|
||||||
|
|
||||||
if (type.toLowerCase() != "totp")
|
|
||||||
throw new Error("Type is not valid, requires 'TOTP'");
|
|
||||||
|
|
||||||
if (!secret) throw new Error("Secret not defined");
|
|
||||||
|
|
||||||
return new TOTP(
|
|
||||||
secret,
|
|
||||||
issuer,
|
|
||||||
client,
|
|
||||||
Number(digits) || 6,
|
|
||||||
Number(period) || 30,
|
|
||||||
Number(offset),
|
|
||||||
getHashType(algorithm),
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("Failed to parse otpauth scheme:", err);
|
|
||||||
throw new Error(
|
|
||||||
`Invalid otpauth:// link. Please check the link and try again. ERR: ${err}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getByGoogleMigrationScheme(link) {
|
|
||||||
try {
|
|
||||||
const data = link.split("data=")[1];
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
const decodedData = decodeURIComponent(data);
|
|
||||||
const buffer = base64decode(decodedData);
|
|
||||||
const proto = decodeProto(buffer);
|
|
||||||
|
|
||||||
const totps = [];
|
|
||||||
const otpParameters = proto.parts.filter(
|
|
||||||
(p) => p.index === 1 && p.type === TYPES.LENDELIM,
|
|
||||||
);
|
|
||||||
|
|
||||||
otpParameters.forEach((part) => {
|
|
||||||
const totp = parseSingleMigrationEntry(part);
|
|
||||||
if (totp) {
|
|
||||||
totps.push(totp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return totps.length > 0 ? totps : null;
|
|
||||||
} catch (err) {
|
|
||||||
console.log("Failed to parse Google Migration scheme:", err);
|
|
||||||
throw new Error(
|
|
||||||
"Invalid otpauth-migration:// link. Failed to parse migration data.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseSingleMigrationEntry(part) {
|
|
||||||
const totpProto = decodeProto(part.value);
|
const totpProto = decodeProto(part.value);
|
||||||
const otpData = {};
|
const otpData = {};
|
||||||
|
|
||||||
@ -192,6 +64,102 @@ function parseSingleMigrationEntry(part) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getByGoogleMigrationScheme(link) {
|
||||||
|
try {
|
||||||
|
const data = link.split("data=")[1];
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
const decodedData = decodeURIComponent(data);
|
||||||
|
const buffer = base64decode(decodedData);
|
||||||
|
const proto = decodeProto(buffer);
|
||||||
|
|
||||||
|
const totps = [];
|
||||||
|
const otpParameters = proto.parts.filter(
|
||||||
|
(p) => p.index === 1 && p.type === TYPES.LENDELIM,
|
||||||
|
);
|
||||||
|
|
||||||
|
otpParameters.forEach((part) => {
|
||||||
|
const totp = _parseSingleMigrationEntry(part);
|
||||||
|
if (totp) {
|
||||||
|
totps.push(totp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return totps.length > 0 ? totps : null;
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Failed to parse Google Migration scheme:", err);
|
||||||
|
throw new Error(
|
||||||
|
"Invalid otpauth-migration:// link. Failed to parse migration data.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTOTPByLink(link) {
|
||||||
|
if (link.startsWith(googleMigrationScheme)) {
|
||||||
|
return getByGoogleMigrationScheme(link);
|
||||||
|
}
|
||||||
|
if (link.startsWith(otpauthScheme)) {
|
||||||
|
return getByOtpauthScheme(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHashType(algorithm) {
|
||||||
|
if (algorithm == "SHA1") return "SHA-1";
|
||||||
|
if (algorithm == "SHA256") return "SHA-256";
|
||||||
|
if (algorithm == "SHA512") return "SHA-512";
|
||||||
|
else return "SHA-1";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getByOtpauthScheme(link) {
|
||||||
|
try {
|
||||||
|
let args = link.split("?");
|
||||||
|
let path = args[0];
|
||||||
|
let params = args[1];
|
||||||
|
|
||||||
|
let pathParts = path.split("/");
|
||||||
|
let type = pathParts[2]; //hotp or totp
|
||||||
|
let label = decodeURIComponent(pathParts[3]);
|
||||||
|
|
||||||
|
let issuerFromLabel = label.includes(":") ? label.split(":")[0] : null;
|
||||||
|
let client = label.includes(":") ? label.split(":")[1].trim() : label;
|
||||||
|
|
||||||
|
let secret = params.match(/secret=([^&]*)/)?.[1];
|
||||||
|
let issuerFromParams = params.match(/issuer=([^&]*)/)?.[1];
|
||||||
|
|
||||||
|
let issuer = issuerFromParams
|
||||||
|
? decodeURIComponent(issuerFromParams)
|
||||||
|
: issuerFromLabel;
|
||||||
|
if (!issuer) issuer = client;
|
||||||
|
|
||||||
|
let period = params.match(/period=([^&]*)/)?.[1];
|
||||||
|
let digits = params.match(/digits=([^&]*)/)?.[1];
|
||||||
|
let algorithm = params.match(/algorithm=([^&]*)/)?.[1];
|
||||||
|
let offset = params.match(/offset=([^&]*)/)?.[1] ?? 0;
|
||||||
|
|
||||||
|
if (type.toLowerCase() != "totp")
|
||||||
|
throw new Error("Type is not valid, requires 'TOTP'");
|
||||||
|
|
||||||
|
if (!secret) throw new Error("Secret not defined");
|
||||||
|
|
||||||
|
return new TOTP(
|
||||||
|
secret,
|
||||||
|
issuer,
|
||||||
|
client,
|
||||||
|
Number(digits) || 6,
|
||||||
|
Number(period) || 30,
|
||||||
|
Number(offset),
|
||||||
|
getHashType(algorithm),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.log("Failed to parse otpauth scheme:", err);
|
||||||
|
throw new Error(
|
||||||
|
`Invalid otpauth:// link. Please check the link and try again. ERR: ${err}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function bytesToString(bytes) {
|
function bytesToString(bytes) {
|
||||||
let str = "";
|
let str = "";
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|