diff --git a/app.json b/app.json index 3d015e4..8df975d 100644 --- a/app.json +++ b/app.json @@ -37,6 +37,11 @@ { "st": "s", "dw": 390 + }, + + { + "st": "r", + "dw": 390 } ] } diff --git a/assets/default.b/icon.png b/assets/default.b/icon.png index c757258..e615d58 100644 Binary files a/assets/default.b/icon.png and b/assets/default.b/icon.png differ diff --git a/assets/default.r/icon.png b/assets/default.r/icon.png index c757258..e615d58 100644 Binary files a/assets/default.r/icon.png and b/assets/default.r/icon.png differ diff --git a/assets/default.s/icon.png b/assets/default.s/icon.png index c757258..e615d58 100644 Binary files a/assets/default.s/icon.png and b/assets/default.s/icon.png differ diff --git a/docs/assets/image.jpg b/docs/assets/image.jpg new file mode 100644 index 0000000..e9a95e2 Binary files /dev/null and b/docs/assets/image.jpg differ diff --git a/docs/assets/image.png b/docs/assets/image.png deleted file mode 100644 index 69a46ed..0000000 Binary files a/docs/assets/image.png and /dev/null differ diff --git a/docs/guides/how-to-add-totps/README.md b/docs/guides/how-to-add-totps/README.md index f78b736..d03fef0 100644 --- a/docs/guides/how-to-add-totps/README.md +++ b/docs/guides/how-to-add-totps/README.md @@ -14,11 +14,9 @@ Then press OK, record will appear on page ![Added record](image-4.png) -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) -To add 2FA TOTP recods using migration from Google Authenticator app, you must go to menu, select "Transfer accounts" -> "Export accounts" +To add 2FA TOTP records 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. @@ -34,4 +32,23 @@ Then press OK, all selected records from Google Authenticator will appear on pag ![Added records from otpauth-migration](image-7.png) -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. +### If you use Proton Authenticator + +To add 2FA TOTP records from Proton Authenticator you must go to settings and press "Export": +![alt text](photo_2025-08-08_17-10-27.jpg) + +After this save file and open it on text editor: + +![alt text](photo_2025-08-08_17-10-35.jpg) + +![alt text](photo_2025-08-08_17-10-41.jpg) + +Copy all stuff from file at clipboard and import in application: + +![alt text](photo_2025-08-08_17-10-38.jpg) + +![alt text](<Снимок экрана_20250808_170854.png>) + +Then press OK, records will appear on page: + +![alt text](<Снимок экрана_20250808_170912.png>) diff --git a/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-27.jpg b/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-27.jpg new file mode 100644 index 0000000..22c87a8 Binary files /dev/null and b/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-27.jpg differ diff --git a/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-35.jpg b/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-35.jpg new file mode 100644 index 0000000..7411203 Binary files /dev/null and b/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-35.jpg differ diff --git a/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-38.jpg b/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-38.jpg new file mode 100644 index 0000000..0dda29e Binary files /dev/null and b/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-38.jpg differ diff --git a/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-41.jpg b/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-41.jpg new file mode 100644 index 0000000..bf2aa03 Binary files /dev/null and b/docs/guides/how-to-add-totps/photo_2025-08-08_17-10-41.jpg differ diff --git a/docs/guides/how-to-add-totps/Снимок экрана_20250808_170854.png b/docs/guides/how-to-add-totps/Снимок экрана_20250808_170854.png new file mode 100644 index 0000000..bc04d1c Binary files /dev/null and b/docs/guides/how-to-add-totps/Снимок экрана_20250808_170854.png differ diff --git a/docs/guides/how-to-add-totps/Снимок экрана_20250808_170912.png b/docs/guides/how-to-add-totps/Снимок экрана_20250808_170912.png new file mode 100644 index 0000000..390ced9 Binary files /dev/null and b/docs/guides/how-to-add-totps/Снимок экрана_20250808_170912.png differ diff --git a/icon_for_appstore.png b/icon_for_appstore.png index 1a4146a..e615d58 100644 Binary files a/icon_for_appstore.png and b/icon_for_appstore.png differ diff --git a/new_icon_for_appstore.png b/new_icon_for_appstore.png deleted file mode 100644 index e615d58..0000000 Binary files a/new_icon_for_appstore.png and /dev/null differ diff --git a/setting/consts.js b/setting/consts.js index 6e73477..e7a9b5b 100644 --- a/setting/consts.js +++ b/setting/consts.js @@ -10,7 +10,9 @@ export const colors = { export const content = { addTotpsHint: - "For add a 2FA TOTP record you must have otpauth:// link or otpauth-migration:// link from Google Authenticator Migration QR-Code", + "For add a 2FA TOTP record you must have otpauth:// link otpauth-migration://" + + "link from Google Authenticator Migration QR-Code or Proton authenticator Export JSON string." + + "If you have a questions - check instruction below!", totpRecordsHint: "TOTP records:", createButton: { placeHolder: "otpauth(-migration)://", @@ -24,6 +26,14 @@ export const content = { label: "Change TOTP link", placeHolder: "otpauth(-migration)://", }, + renameButtons: { + rename: "Rename", + renameIssuer: "Rename Issuer", + renameClient: "Rename Client", + }, + saveButton: { + label: "Save", + }, deleteButton: { label: "Delete", }, diff --git a/setting/index.js b/setting/index.js index 413acc6..6d6e749 100644 --- a/setting/index.js +++ b/setting/index.js @@ -31,7 +31,7 @@ function GetTOTPList(storage) { editingIndex = index; tempIssuer = element.issuer; tempClient = element.client; - updateStorage(storage); + _props.settingsStorage.setItem("requestUpdate", Math.random()); }, onSave: () => { storage[index].issuer = tempIssuer; @@ -61,6 +61,7 @@ function GetTOTPList(storage) { updateStorage(storage); } }, + isEditInProgress: editingIndex !== -1, }); }); } @@ -97,11 +98,6 @@ AppSettingsPage({ 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); @@ -119,7 +115,9 @@ AppSettingsPage({ display: "flex", alignItems: "center", justifyContent: "center", - margin: "10px", + marginBottom: "10px", + marginLeft: "10px", + marginRight: "10px", fontSize: "20px", color: colors.text, borderRadius: "5px", @@ -137,7 +135,7 @@ AppSettingsPage({ }, errorMessage, ) - : null; //TODO: Check for work + : null; const bottomContainer = View( { @@ -151,8 +149,8 @@ AppSettingsPage({ style: { display: "flex", justifyContent: "center", - marginTop: "20px", - marginBottom: "20px", + marginTop: "10px", + marginBottom: "10px", }, }, Link( diff --git a/setting/ui/card.js b/setting/ui/card.js index f9f058f..baeb51a 100644 --- a/setting/ui/card.js +++ b/setting/ui/card.js @@ -14,6 +14,7 @@ export function createTOTPCard({ onMoveDown, onIssuerChange, onClientChange, + isEditInProgress, }) { const infoView = View( { @@ -26,7 +27,7 @@ export function createTOTPCard({ isEditing ? [ TextInput({ - label: "Rename Issuer", + label: content.renameButtons.renameIssuer, value: tempIssuer, onChange: onIssuerChange, labelStyle: { @@ -39,14 +40,14 @@ export function createTOTPCard({ color: colors.text, borderRadius: "5px", height: "40px", - width: "200px" + width: "200px", }, subStyle: { display: "none", }, }), TextInput({ - label: "Rename client", + label: content.renameButtons.renameClient, value: tempClient, onChange: onClientChange, labelStyle: { @@ -59,7 +60,7 @@ export function createTOTPCard({ color: colors.text, borderRadius: "5px", height: "40px", - width: "200px" + width: "200px", }, subStyle: { display: "none", @@ -89,7 +90,7 @@ export function createTOTPCard({ isEditing ? [ Button({ - label: "Save", + label: content.saveButton.label, style: { margin: "5px", backgroundColor: "#28a745", @@ -100,7 +101,7 @@ export function createTOTPCard({ ] : [ Button({ - label: "Rename", + label: content.renameButtons.rename, style: { margin: "5px", backgroundColor: colors.notify, @@ -108,15 +109,17 @@ export function createTOTPCard({ }, onClick: onRename, }), - Button({ - label: "Delete", - style: { - margin: "5px", - backgroundColor: colors.alert, - color: colors.text, - }, - onClick: onDelete, - }), + !isEditInProgress + ? Button({ + label: content.deleteButton.label, + style: { + margin: "5px", + backgroundColor: colors.alert, + color: colors.text, + }, + onClick: onDelete, + }) + : null, ], ); @@ -127,7 +130,6 @@ export function createTOTPCard({ label: "⬆", disabled: index === 0, style: { - width: "50px", margin: "2px", color: colors.text, backgroundColor: colors.notify, @@ -138,7 +140,6 @@ export function createTOTPCard({ label: "⬇", disabled: index === storage.length - 1, style: { - width: "50px", margin: "2px", color: colors.text, backgroundColor: colors.notify, diff --git a/setting/utils/protonBackupExport.js b/setting/utils/protonBackupExport.js new file mode 100644 index 0000000..34d9cfe --- /dev/null +++ b/setting/utils/protonBackupExport.js @@ -0,0 +1,11 @@ +export class ProtonBackupExport { + version; + entries = Array.of(ProtonTotpRecord); +} + +export class ProtonTotpRecord { + content = { + uri, + entry_type, + }; +} diff --git a/setting/utils/queryParser.js b/setting/utils/queryParser.js index 95486fe..e26910a 100644 --- a/setting/utils/queryParser.js +++ b/setting/utils/queryParser.js @@ -1,11 +1,139 @@ import { decodeProto, TYPES } from "../../lib/protobuf-decoder/protobufDecoder"; import { TOTP } from "../../lib/totp-quickjs"; import { base64decode, encode } from "../../lib/totp-quickjs/base32decoder"; +import { ProtonBackupExport } from "./protonBackupExport"; const otpauthScheme = "otpauth://"; 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 _parseSingleMigrationEntry(part) { +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 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 otpData = {}; @@ -64,102 +192,6 @@ 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) { let str = ""; for (let i = 0; i < bytes.length; i++) {