diff --git a/docs/guides/how-to-add-totps/README.md b/docs/guides/how-to-add-totps/README.md index f78b736..b1717cd 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,20 @@ 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>) \ No newline at end of file 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/setting/consts.js b/setting/consts.js index 6e73477..7946653 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 fa2406c..6d6e749 100644 --- a/setting/index.js +++ b/setting/index.js @@ -98,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); @@ -121,6 +116,8 @@ AppSettingsPage({ alignItems: "center", justifyContent: "center", marginBottom: "10px", + marginLeft: "10px", + marginRight: "10px", fontSize: "20px", color: colors.text, borderRadius: "5px", @@ -138,7 +135,7 @@ AppSettingsPage({ }, errorMessage, ) - : null; //TODO: Check for work + : null; const bottomContainer = View( { diff --git a/setting/ui/card.js b/setting/ui/card.js index be9f041..bb9c39e 100644 --- a/setting/ui/card.js +++ b/setting/ui/card.js @@ -27,7 +27,7 @@ export function createTOTPCard({ isEditing ? [ TextInput({ - label: "Rename Issuer", + label: content.renameButtons.renameIssuer, value: tempIssuer, onChange: onIssuerChange, labelStyle: { @@ -47,7 +47,7 @@ export function createTOTPCard({ }, }), TextInput({ - label: "Rename client", + label: content.renameButtons.renameClient, value: tempClient, onChange: onClientChange, labelStyle: { @@ -90,7 +90,7 @@ export function createTOTPCard({ isEditing ? [ Button({ - label: "Save", + label: content.saveButton.label, style: { margin: "5px", backgroundColor: "#28a745", @@ -101,7 +101,7 @@ export function createTOTPCard({ ] : [ Button({ - label: "Rename", + label: content.renameButtons.rename, style: { margin: "5px", backgroundColor: colors.notify, @@ -111,7 +111,7 @@ export function createTOTPCard({ }), !isEditInProgress ? Button({ - label: "Delete", + label: content.deleteButton.label, style: { margin: "5px", backgroundColor: colors.alert, @@ -129,7 +129,6 @@ export function createTOTPCard({ label: "⬆", disabled: index === 0, style: { - width: "50px", margin: "2px", color: colors.text, backgroundColor: colors.notify, @@ -140,7 +139,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..a887750 --- /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, + } +} \ No newline at end of file diff --git a/setting/utils/queryParser.js b/setting/utils/queryParser.js index 95486fe..e5b2799 100644 --- a/setting/utils/queryParser.js +++ b/setting/utils/queryParser.js @@ -1,11 +1,132 @@ 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); -function _parseSingleMigrationEntry(part) { + } 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) { + 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 +185,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++) {