fix, chore, feat: refactor of settings app, fix of Google Auth migration parse, added rename button(testing), added sort feature, added timeout to screen turning off, formated codes with space between 6-digits codes
Co-authored-by: DemiarUA <demiar97@gmail.com> https://github.com/Lisoveliy/totpfit/pull/1
This commit is contained in:
336
setting/index.js
336
setting/index.js
@@ -1,6 +1,11 @@
|
||||
import { getTOTPByLink } from "./utils/queryParser.js";
|
||||
import { createTOTPCard } from "./ui/card.js";
|
||||
|
||||
let _props = null;
|
||||
let editingIndex = -1;
|
||||
let tempIssuer = "";
|
||||
let tempClient = "";
|
||||
let errorMessage = "";
|
||||
|
||||
const colors = {
|
||||
bg: "#101010",
|
||||
@@ -9,45 +14,114 @@ const colors = {
|
||||
text: "#fafafa",
|
||||
alert: "#ad3c23",
|
||||
notify: "#555555",
|
||||
bigText: "#fafafa"
|
||||
bigText: "#fafafa",
|
||||
};
|
||||
|
||||
function updateStorage(storage) {
|
||||
_props.settingsStorage.setItem("TOTPs", JSON.stringify(storage));
|
||||
}
|
||||
|
||||
function GetTOTPList(storage) {
|
||||
return storage.map((element, index) => {
|
||||
return createTOTPCard({
|
||||
element,
|
||||
index,
|
||||
storage,
|
||||
isEditing: editingIndex === index,
|
||||
tempIssuer,
|
||||
tempClient,
|
||||
onIssuerChange: (val) => {
|
||||
tempIssuer = val;
|
||||
},
|
||||
onClientChange: (val) => {
|
||||
tempClient = val;
|
||||
},
|
||||
onRename: () => {
|
||||
editingIndex = index;
|
||||
tempIssuer = element.issuer;
|
||||
tempClient = element.client;
|
||||
updateStorage(storage);
|
||||
},
|
||||
onSave: () => {
|
||||
storage[index].issuer = tempIssuer;
|
||||
storage[index].client = tempClient;
|
||||
editingIndex = -1;
|
||||
updateStorage(storage);
|
||||
},
|
||||
onDelete: () => {
|
||||
storage.splice(index, 1);
|
||||
updateStorage(storage);
|
||||
},
|
||||
onMoveUp: () => {
|
||||
if (index > 0) {
|
||||
[storage[index], storage[index - 1]] = [
|
||||
storage[index - 1],
|
||||
storage[index],
|
||||
];
|
||||
updateStorage(storage);
|
||||
}
|
||||
},
|
||||
onMoveDown: () => {
|
||||
if (index < storage.length - 1) {
|
||||
[storage[index], storage[index + 1]] = [
|
||||
storage[index + 1],
|
||||
storage[index],
|
||||
];
|
||||
updateStorage(storage);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
AppSettingsPage({
|
||||
build(props) {
|
||||
_props = props;
|
||||
const storage = JSON.parse(
|
||||
props.settingsStorage.getItem("TOTPs") ?? "[]"
|
||||
props.settingsStorage.getItem("TOTPs") ?? "[]",
|
||||
);
|
||||
const totpEntrys = GetTOTPList(storage);
|
||||
const addTOTPsHint = storage.length < 1 ?
|
||||
Text({
|
||||
paragraph: true,
|
||||
align: "center",
|
||||
style: {
|
||||
paddingTop: "10px",
|
||||
marginBottom: "10px",
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
verticalAlign: "middle",
|
||||
},
|
||||
},
|
||||
"For add a 2FA TOTP record you must have otpauth:// link or otpauth-migration:// link from Google Authenticator Migration QR-Code"
|
||||
) : null;
|
||||
const addTOTPsHint =
|
||||
storage.length < 1
|
||||
? Text(
|
||||
{
|
||||
paragraph: true,
|
||||
align: "center",
|
||||
style: {
|
||||
paddingTop: "10px",
|
||||
marginBottom: "10px",
|
||||
color: colors.text,
|
||||
fontSize: 16,
|
||||
verticalAlign: "middle",
|
||||
},
|
||||
},
|
||||
"For add a 2FA TOTP record you must have otpauth:// link or otpauth-migration:// link from Google Authenticator Migration QR-Code",
|
||||
)
|
||||
: null;
|
||||
|
||||
const createButton = TextInput({
|
||||
placeholder: "otpauth(-migration)://",
|
||||
label: "Add new TOTP record",
|
||||
onChange: (changes) => {
|
||||
let link = getTOTPByLink(changes);
|
||||
if (link == null) {
|
||||
console.log("link is invalid");
|
||||
return;
|
||||
try {
|
||||
errorMessage = "";
|
||||
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)) {
|
||||
storage.push(...link);
|
||||
} else {
|
||||
storage.push(link);
|
||||
}
|
||||
updateStorage(storage);
|
||||
} catch (e) {
|
||||
errorMessage = e.message;
|
||||
updateStorage(storage);
|
||||
}
|
||||
|
||||
if (Array.isArray(link))
|
||||
storage.push(...link);
|
||||
else storage.push(link);
|
||||
|
||||
updateStorage(storage);
|
||||
},
|
||||
labelStyle: {
|
||||
backgroundColor: colors.notify,
|
||||
@@ -58,18 +132,42 @@ AppSettingsPage({
|
||||
fontSize: "20px",
|
||||
color: colors.text,
|
||||
borderRadius: "5px",
|
||||
position: storage.length < 1 ? "absolute" : null, //TODO: Сделать что-то с этим кошмаром
|
||||
bottom: storage.length < 1 ? "0px" : null,
|
||||
left: storage.length < 1 ? "0px" : null,
|
||||
right: storage.length < 1 ? "0px" : null
|
||||
width: "100%",
|
||||
height: "45px",
|
||||
},
|
||||
});
|
||||
|
||||
var body = Section(
|
||||
const errorText = errorMessage
|
||||
? Text(
|
||||
{
|
||||
style: {
|
||||
color: colors.alert,
|
||||
textAlign: "center",
|
||||
margin: "5px",
|
||||
},
|
||||
},
|
||||
errorMessage,
|
||||
)
|
||||
: null; //TODO: Check for work
|
||||
|
||||
const bottomContainer = View(
|
||||
{
|
||||
style: {
|
||||
padding: "5px 0px",
|
||||
backgroundColor: colors.bg,
|
||||
},
|
||||
},
|
||||
[errorText, createButton].filter(Boolean),
|
||||
);
|
||||
|
||||
const pageContainer = View(
|
||||
{
|
||||
style: {
|
||||
backgroundColor: colors.bg,
|
||||
minHeight: "100vh",
|
||||
height: "100vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
},
|
||||
[
|
||||
@@ -79,142 +177,58 @@ AppSettingsPage({
|
||||
textAlign: "center",
|
||||
},
|
||||
},
|
||||
storage.length < 1 ? addTOTPsHint : Text(
|
||||
{
|
||||
align: "center",
|
||||
paragraph: true,
|
||||
style: {
|
||||
marginBottom: "10px",
|
||||
color: colors.bigText,
|
||||
fontSize: 23,
|
||||
fontWeight: "500",
|
||||
verticalAlign: "middle",
|
||||
},
|
||||
},
|
||||
"TOTP records:"
|
||||
)
|
||||
[
|
||||
storage.length < 1
|
||||
? addTOTPsHint
|
||||
: Text(
|
||||
{
|
||||
align: "center",
|
||||
paragraph: true,
|
||||
style: {
|
||||
marginBottom: "10px",
|
||||
color: colors.bigText,
|
||||
fontSize: 23,
|
||||
fontWeight: "500",
|
||||
verticalAlign: "middle",
|
||||
},
|
||||
},
|
||||
"TOTP records:",
|
||||
),
|
||||
],
|
||||
),
|
||||
...totpEntrys,
|
||||
createButton,
|
||||
View({
|
||||
style: {
|
||||
display: "flex",
|
||||
justifyContent: "center"
|
||||
}
|
||||
},
|
||||
Link({
|
||||
source: "https://github.com/Lisoveliy/totpfit/blob/main/docs/guides/how-to-add-totps/README.md"
|
||||
},
|
||||
"Instruction | Report issue (GitHub)")
|
||||
),
|
||||
]
|
||||
);
|
||||
return body;
|
||||
},
|
||||
});
|
||||
|
||||
function GetTOTPList(storage) {
|
||||
let totpEntrys = [];
|
||||
let counter = 0;
|
||||
storage.forEach((element) => {
|
||||
const elementId = counter;
|
||||
const textInput = TextInput({
|
||||
placeholder: "otpauth(-migration)://",
|
||||
label: "Change TOTP link",
|
||||
onChange: (changes) => {
|
||||
try {
|
||||
let link = getTOTPByLink(changes);
|
||||
if (Array.isArray(link))
|
||||
return;
|
||||
|
||||
storage[elementId] = link;
|
||||
updateStorage(storage);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
},
|
||||
labelStyle: {
|
||||
backgroundColor: colors.notify,
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
margin: "10px",
|
||||
flexGrow: 1,
|
||||
fontSize: "20px",
|
||||
color: colors.text,
|
||||
borderRadius: "5px",
|
||||
},
|
||||
});
|
||||
const textBig = Text(
|
||||
{
|
||||
align: "center",
|
||||
style: {
|
||||
color: colors.text,
|
||||
fontSize: "18px",
|
||||
fontWeight: "500"
|
||||
},
|
||||
paragraph: true,
|
||||
},
|
||||
`${element.issuer}: ${element.client}`
|
||||
);
|
||||
const delButton = Button({
|
||||
onClick: () => {
|
||||
storage = storage.filter(
|
||||
(x) => storage.indexOf(x) != elementId
|
||||
);
|
||||
updateStorage(storage);
|
||||
},
|
||||
style: {
|
||||
backgroundColor: colors.alert,
|
||||
fontSize: "18px",
|
||||
color: colors.text,
|
||||
height: "fit-content",
|
||||
margin: "10px",
|
||||
},
|
||||
label: "Delete",
|
||||
});
|
||||
const text = Text(
|
||||
{
|
||||
style: {
|
||||
color: colors.text,
|
||||
fontSize: "14px",
|
||||
},
|
||||
align: "center",
|
||||
},
|
||||
`${element.hashType} | ${element.digits} digits | ${element.fetchTime} seconds | ${element.timeOffset} sec offset`
|
||||
);
|
||||
const view = View(
|
||||
{
|
||||
style: {
|
||||
textAlign: "center",
|
||||
backgroundColor: colors.secondaryBg,
|
||||
//border: "2px solid white",
|
||||
borderRadius: "5px",
|
||||
margin: "10px",
|
||||
},
|
||||
},
|
||||
[
|
||||
textBig,
|
||||
text,
|
||||
View(
|
||||
{
|
||||
style: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 100px",
|
||||
flexGrow: 1,
|
||||
overflow: "scroll",
|
||||
},
|
||||
},
|
||||
[textInput, delButton]
|
||||
[
|
||||
...totpEntrys,
|
||||
View(
|
||||
{
|
||||
style: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
marginTop: "20px",
|
||||
marginBottom: "20px",
|
||||
},
|
||||
},
|
||||
Link(
|
||||
{
|
||||
source: "https://github.com/Lisoveliy/totpfit/blob/main/docs/guides/how-to-add-totps/README.md",
|
||||
},
|
||||
"Instruction | Report issue (GitHub)",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
bottomContainer,
|
||||
],
|
||||
);
|
||||
totpEntrys.push({ text: text, view: view });
|
||||
counter++;
|
||||
});
|
||||
|
||||
return totpEntrys.map((x) => x.view);
|
||||
}
|
||||
|
||||
function updateStorage(storage) {
|
||||
_props.settingsStorage.setItem("TOTPs", JSON.stringify(storage));
|
||||
}
|
||||
return pageContainer;
|
||||
},
|
||||
});
|
||||
|
||||
140
setting/ui/card.js
Normal file
140
setting/ui/card.js
Normal file
@@ -0,0 +1,140 @@
|
||||
export function createTOTPCard({
|
||||
element,
|
||||
index,
|
||||
storage,
|
||||
isEditing,
|
||||
tempIssuer,
|
||||
tempClient,
|
||||
onRename,
|
||||
onSave,
|
||||
onDelete,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
onIssuerChange,
|
||||
onClientChange,
|
||||
}) {
|
||||
const colors = {
|
||||
secondaryBg: "#282828",
|
||||
text: "#fafafa",
|
||||
alert: "#ad3c23",
|
||||
notify: "#555555",
|
||||
};
|
||||
|
||||
const infoView = View(
|
||||
{
|
||||
style: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
},
|
||||
isEditing
|
||||
? [
|
||||
TextInput({
|
||||
label: "Issuer",
|
||||
value: tempIssuer,
|
||||
onChange: onIssuerChange,
|
||||
}),
|
||||
TextInput({
|
||||
label: "Client",
|
||||
value: tempClient,
|
||||
onChange: onClientChange,
|
||||
}),
|
||||
]
|
||||
: [
|
||||
Text(
|
||||
{ style: { color: colors.text, marginBottom: "2px" } },
|
||||
`Issuer: ${element.issuer}`,
|
||||
),
|
||||
Text(
|
||||
{ style: { color: colors.text } },
|
||||
`Client: ${element.client}`,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
const buttonsView = View(
|
||||
{ style: { display: "flex", flexDirection: "row" } },
|
||||
isEditing
|
||||
? [
|
||||
Button({
|
||||
label: "Save",
|
||||
style: {
|
||||
margin: "5px",
|
||||
backgroundColor: "#28a745",
|
||||
color: colors.text,
|
||||
},
|
||||
onClick: onSave,
|
||||
}),
|
||||
]
|
||||
: [
|
||||
Button({
|
||||
label: "Rename",
|
||||
style: {
|
||||
margin: "5px",
|
||||
backgroundColor: colors.notify,
|
||||
color: colors.text,
|
||||
},
|
||||
onClick: onRename,
|
||||
}),
|
||||
Button({
|
||||
label: "Delete",
|
||||
style: {
|
||||
margin: "5px",
|
||||
backgroundColor: colors.alert,
|
||||
color: colors.text,
|
||||
},
|
||||
onClick: onDelete,
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
const reorderView = View(
|
||||
{ style: { display: "flex", flexDirection: "column" } },
|
||||
[
|
||||
Button({
|
||||
label: "↑",
|
||||
disabled: index === 0,
|
||||
style: { width: "50px", margin: "2px" },
|
||||
onClick: onMoveUp,
|
||||
}),
|
||||
Button({
|
||||
label: "↓",
|
||||
disabled: index === storage.length - 1,
|
||||
style: { width: "50px", margin: "2px" },
|
||||
onClick: onMoveDown,
|
||||
}),
|
||||
],
|
||||
);
|
||||
|
||||
const mainContent = View({ style: { flexGrow: 1, padding: "5px" } }, [
|
||||
infoView,
|
||||
Text(
|
||||
{
|
||||
style: {
|
||||
color: colors.text,
|
||||
fontSize: "14px",
|
||||
marginTop: "5px",
|
||||
},
|
||||
},
|
||||
`${element.hashType} | ${element.digits} digits | ${element.fetchTime} seconds | ${element.timeOffset} sec offset`,
|
||||
),
|
||||
buttonsView,
|
||||
]);
|
||||
|
||||
return View(
|
||||
{
|
||||
style: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: colors.secondaryBg,
|
||||
borderRadius: "5px",
|
||||
margin: "10px",
|
||||
padding: "5px",
|
||||
},
|
||||
},
|
||||
[mainContent, reorderView],
|
||||
);
|
||||
}
|
||||
@@ -2,113 +2,168 @@ import { decodeProto, TYPES } from "../../lib/protobuf-decoder/protobufDecoder";
|
||||
import { TOTP } from "../../lib/totp-quickjs";
|
||||
import { base64decode, encode } from "../../lib/totp-quickjs/base32decoder";
|
||||
|
||||
const otpauthScheme = "otpauth:/";
|
||||
const googleMigrationScheme = "otpauth-migration:/";
|
||||
const otpauthScheme = "otpauth://";
|
||||
const googleMigrationScheme = "otpauth-migration://";
|
||||
|
||||
export function getTOTPByLink(link) {
|
||||
if (link.includes(otpauthScheme))
|
||||
return getByOtpauthScheme(link)
|
||||
if (link.includes(googleMigrationScheme))
|
||||
return getByGoogleMigrationScheme(link)
|
||||
function _parseSingleMigrationEntry(part) {
|
||||
const totpProto = decodeProto(part.value);
|
||||
const otpData = {};
|
||||
|
||||
return null;
|
||||
}
|
||||
const protoPartHandlers = {
|
||||
1: (p) => {
|
||||
otpData.secret = encode(p.value);
|
||||
},
|
||||
2: (p) => {
|
||||
otpData.name = bytesToString(p.value);
|
||||
},
|
||||
3: (p) => {
|
||||
otpData.issuer = bytesToString(p.value);
|
||||
},
|
||||
4: (p) => {
|
||||
otpData.algorithm = p.value;
|
||||
},
|
||||
5: (p) => {
|
||||
otpData.digits = p.value;
|
||||
},
|
||||
6: (p) => {
|
||||
otpData.type = p.value;
|
||||
},
|
||||
};
|
||||
|
||||
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";
|
||||
}
|
||||
totpProto.parts.forEach((p) => {
|
||||
const handler = protoPartHandlers[p.index];
|
||||
if (handler) {
|
||||
handler(p);
|
||||
}
|
||||
});
|
||||
|
||||
function getByOtpauthScheme(link) {
|
||||
try {
|
||||
let args = link.split("/", otpauthScheme.length);
|
||||
let type = args[2]; //Returns 'hotp' or 'totp'
|
||||
let issuer = args[3].split(":")[0]?.split("?")[0]; //Returns issuer
|
||||
let client =
|
||||
args[3].split(":")[1]?.split("?")[0] ??
|
||||
args[3].split(":")[0]?.split("?")[0]; //Returns client
|
||||
let secret = args[3].split("secret=")[1]?.split("&")[0]; //Returns secret
|
||||
let period = args[3].split("period=")[1]?.split("&")[0]; //Returns period
|
||||
let digits = args[3].split("digit=")[1]?.split("&")[0]; //Returns digits
|
||||
let algorithm = args[3].split("algorithm=")[1]?.split("&")[0]; //Returns algorithm
|
||||
let offset = args[3].split("offset=")[1]?.split("&")[0] ?? 0; //Returns offset
|
||||
if (otpData.type !== "2") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type.toLowerCase() != "totp")
|
||||
throw new Error("Type is not valid, requires 'TOTP'");
|
||||
const digitsMap = { 1: 6, 2: 8 };
|
||||
const algoMap = { 1: "SHA-1", 2: "SHA-256", 3: "SHA-512" };
|
||||
|
||||
if (secret === undefined) throw new Error("Secret not defined");
|
||||
const finalDigits = digitsMap[otpData.digits] || 6;
|
||||
const finalAlgo = algoMap[otpData.algorithm] || "SHA-1";
|
||||
const finalIssuer = otpData.issuer || otpData.name;
|
||||
const finalName = otpData.name;
|
||||
|
||||
if (issuer == client) {
|
||||
issuer = args[3].split("issuer=")[1]?.split("&")[0];
|
||||
}
|
||||
if (!otpData.secret || !finalName) {
|
||||
throw new Error("Skipping record with missing secret or name.");
|
||||
}
|
||||
|
||||
issuer = decodeURIComponent(issuer);
|
||||
client = decodeURIComponent(client);
|
||||
|
||||
return new TOTP(
|
||||
secret,
|
||||
issuer,
|
||||
client,
|
||||
Number(digits),
|
||||
Number(period),
|
||||
Number(offset),
|
||||
getHashType(algorithm)
|
||||
);
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return null;
|
||||
}
|
||||
return new TOTP(
|
||||
otpData.secret,
|
||||
finalIssuer,
|
||||
finalName,
|
||||
finalDigits,
|
||||
30,
|
||||
0,
|
||||
finalAlgo,
|
||||
);
|
||||
}
|
||||
|
||||
function getByGoogleMigrationScheme(link) {
|
||||
try {
|
||||
const data = link.split("data=")[1];
|
||||
if (!data) return null;
|
||||
|
||||
let data = link.split("data=")[1]; //Returns base64 encoded data
|
||||
data = decodeURIComponent(data);
|
||||
let decode = base64decode(data);
|
||||
let proto = decodeProto(decode);
|
||||
const decodedData = decodeURIComponent(data);
|
||||
const buffer = base64decode(decodedData);
|
||||
const proto = decodeProto(buffer);
|
||||
|
||||
let protoTotps = [];
|
||||
const totps = [];
|
||||
const otpParameters = proto.parts.filter(
|
||||
(p) => p.index === 1 && p.type === TYPES.LENDELIM,
|
||||
);
|
||||
|
||||
proto.parts.forEach(part => {
|
||||
if (part.type == TYPES.LENDELIM) {
|
||||
protoTotps.push(decodeProto(part.value));
|
||||
}
|
||||
});
|
||||
otpParameters.forEach((part) => {
|
||||
const totp = _parseSingleMigrationEntry(part);
|
||||
if (totp) {
|
||||
totps.push(totp);
|
||||
}
|
||||
});
|
||||
|
||||
let totps = [];
|
||||
protoTotps.forEach(x => {
|
||||
let type = x.parts.filter(x => x.index == 6)[0]; //find type of OTP
|
||||
if (type.value !== '2') {
|
||||
console.log("ERR: it's a not TOTP record")
|
||||
return;
|
||||
}
|
||||
let secret = x.parts.filter(x => x.index == 1)[0].value;
|
||||
secret = encode(secret);
|
||||
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.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let name = bytesToString(x.parts.filter(x => x.index == 2)[0].value);
|
||||
let issuer = bytesToString(x.parts.filter(x => x.index == 3)[0].value);
|
||||
export function getTOTPByLink(link) {
|
||||
if (link.startsWith(googleMigrationScheme)) {
|
||||
return getByGoogleMigrationScheme(link);
|
||||
}
|
||||
if (link.startsWith(otpauthScheme)) {
|
||||
return getByOtpauthScheme(link);
|
||||
}
|
||||
|
||||
totps.push(new TOTP(
|
||||
secret,
|
||||
issuer,
|
||||
name,
|
||||
6,
|
||||
30,
|
||||
0,
|
||||
"SHA-1"
|
||||
));
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return totps;
|
||||
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) {
|
||||
let str = '';
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
str += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
let str = "";
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
str += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user