diff --git a/README.md b/README.md index e958a1d..b00c512 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ # TOTPFIT -### Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4 +### Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4 with Google Authenticator migration Support ![alt text](docs/assets/image2.png) ### Features: - Supports of otpauth links with parameters "issuer", "algorithm", "digits", "period" -- Addition/Edition/Deletion of TOTPs from mobile settings app +- Addition/Edition/Deletion of TOTPs from mobile app +- Support of google migration links formated: ```otpauth-migration://offline?data=...``` (At this stage with only 6 digits and only 30 seconds period) -### Google Migration Support: -- Support of google migration links formated: ```otpauth-migration://offline?data=...``` (BETA) +### Guides: -### Screenshots: +[How to add 2FA TOTP records (keys) on app](/docs/guides/how-to-add-totps/README.md) -![alt text](docs/assets/image2.png) - -![alt text](docs/assets/image.png) \ No newline at end of file +#### This repo has mirror for issues on [GitHub](https://github.com/Lisoveliy/totpfit) \ No newline at end of file diff --git a/app.json b/app.json index 2578602..bdcd7c5 100644 --- a/app.json +++ b/app.json @@ -6,7 +6,7 @@ "appType": "app", "version": { "code": 1, - "name": "1.2.2" + "name": "1.3" }, "icon": "icon.png", "vender": "zepp", diff --git a/docs/guides/how-to-add-totps/README.md b/docs/guides/how-to-add-totps/README.md new file mode 100644 index 0000000..6c9b437 --- /dev/null +++ b/docs/guides/how-to-add-totps/README.md @@ -0,0 +1,37 @@ +# How to add 2FA TOTP records (keys) on app + +### If you use default 2FA otpauth:// links + +To add 2FA TOTP records using 2FA TOTP QR-Codes, you must scan QR-Code of service providing 2FA and scan (decode) it to a URI. If you have screenshot of QR-Code -- scan it on any app providing scan from image, ex: Search screen on Google Assistant. For example, this QR-Code will represent next URI string: + +![QR Code with URI](image.png) + +Copy this URI string and paste it to app using button *"Add new TOTP record"*: + +![Add new TOTP record popup](image-2.png) + +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 entered on 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" + +Select codes then screenshot QR code and and scan (decode) it to a URI. Use any app providing scan from image, ex: "Search screen" function (Google Lens) on Google Assistant. + +For example, this QR-Code will represent next URI string: + +![Google lens scan from Google Authenticator](image-5.png) + +After scaning copy this URI string and paste it to app using button *"Add new TOTP record"*: + +![Add new TOTP record using otpauth-migration](image-6.png) + +Then press OK, all selected records on Google Authenticator will appear on page + +![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 entered on text field, and previous link will not be shown on field. \ No newline at end of file diff --git a/docs/guides/how-to-add-totps/image-2.png b/docs/guides/how-to-add-totps/image-2.png new file mode 100644 index 0000000..ae8d038 Binary files /dev/null and b/docs/guides/how-to-add-totps/image-2.png differ diff --git a/docs/guides/how-to-add-totps/image-4.png b/docs/guides/how-to-add-totps/image-4.png new file mode 100644 index 0000000..66dd003 Binary files /dev/null and b/docs/guides/how-to-add-totps/image-4.png differ diff --git a/docs/guides/how-to-add-totps/image-5.png b/docs/guides/how-to-add-totps/image-5.png new file mode 100644 index 0000000..9b6ca4d Binary files /dev/null and b/docs/guides/how-to-add-totps/image-5.png differ diff --git a/docs/guides/how-to-add-totps/image-6.png b/docs/guides/how-to-add-totps/image-6.png new file mode 100644 index 0000000..57fdc9b Binary files /dev/null and b/docs/guides/how-to-add-totps/image-6.png differ diff --git a/docs/guides/how-to-add-totps/image-7.png b/docs/guides/how-to-add-totps/image-7.png new file mode 100644 index 0000000..4afb0f4 Binary files /dev/null and b/docs/guides/how-to-add-totps/image-7.png differ diff --git a/docs/guides/how-to-add-totps/image.png b/docs/guides/how-to-add-totps/image.png new file mode 100644 index 0000000..1efd5e4 Binary files /dev/null and b/docs/guides/how-to-add-totps/image.png differ diff --git a/lib/protobuf-decoder/varintUtils.js b/lib/protobuf-decoder/varintUtils.js index 6b9b6b4..b3a8a98 100644 --- a/lib/protobuf-decoder/varintUtils.js +++ b/lib/protobuf-decoder/varintUtils.js @@ -1,23 +1,23 @@ export function decodeVarint(buffer, offset) { - let res = this.BigInt(0); - let shift = 0; - let byte = 0; + let res = 0; + let shift = 0; + let byte = 0; - do { - if (offset >= buffer.length) { - throw new RangeError("Index out of bound decoding varint"); - } + do { + if (offset >= buffer.length) { + throw new RangeError("Index out of bound decoding varint"); + } - byte = buffer[offset++]; + byte = buffer[offset++]; - const multiplier = this.BigInt(2) ** this.BigInt(shift); - const thisByteValue = this.BigInt(byte & 0x7f) * multiplier; - shift += 7; - res = res + thisByteValue; - } while (byte >= 0x80); + const multiplier = 2 ** shift; + const thisByteValue = (byte & 0x7f) * multiplier; + shift += 7; + res = res + thisByteValue; + } while (byte >= 0x80); - return { - value: res, - length: shift / 7 - }; + return { + value: res, + length: shift / 7 + }; } diff --git a/package.json b/package.json index f87dd91..2c88185 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "totpfit", - "version": "1.2.2", + "version": "1.3", "description": "Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4", "main": "app.js", "author": "Lisoveliy", diff --git a/setting/index.js b/setting/index.js index 03e27a1..fb0641b 100644 --- a/setting/index.js +++ b/setting/index.js @@ -2,6 +2,16 @@ import { getTOTPByLink } from "./utils/queryParser.js"; let _props = null; +const colors = { + bg: "#101010", + linkBg: "#ffffffc0", + secondaryBg: "#282828", + text: "#fafafa", + alert: "#ad3c23", + notify: "#555555", + bigText: "#fafafa" +}; + AppSettingsPage({ build(props) { _props = props; @@ -9,9 +19,23 @@ AppSettingsPage({ 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 createButton = TextInput({ - placeholder: "otpauth://", - label: "Add new OTP Link", + placeholder: "otpauth(-migration)://", + label: "Add new TOTP record", onChange: (changes) => { let link = getTOTPByLink(changes); if (link == null) { @@ -19,29 +43,32 @@ AppSettingsPage({ return; } - if(Array.isArray(link)) + if (Array.isArray(link)) storage.push(...link); else storage.push(link); updateStorage(storage); }, labelStyle: { - backgroundColor: "#14213D", + backgroundColor: colors.notify, display: "flex", alignItems: "center", justifyContent: "center", margin: "10px", - flexGrow: 1, fontSize: "20px", - color: "#FFFFFF", + 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 }, }); var body = Section( { style: { - backgroundColor: "black", + backgroundColor: colors.bg, minHeight: "100vh", }, }, @@ -52,22 +79,34 @@ AppSettingsPage({ textAlign: "center", }, }, - Text( + storage.length < 1 ? addTOTPsHint : Text( { align: "center", paragraph: true, style: { marginBottom: "10px", - color: "#fff", + color: colors.bigText, fontSize: 23, + fontWeight: "500", verticalAlign: "middle", }, }, - "TOTPS:" + "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; @@ -80,14 +119,14 @@ function GetTOTPList(storage) { storage.forEach((element) => { const elementId = counter; const textInput = TextInput({ - placeholder: "otpauth://", - label: "Change OTP link", + placeholder: "otpauth(-migration)://", + label: "Change TOTP link", onChange: (changes) => { try { let link = getTOTPByLink(changes); - if(Array.isArray(link)) + if (Array.isArray(link)) return; - + storage[elementId] = link; updateStorage(storage); } catch (err) { @@ -95,7 +134,7 @@ function GetTOTPList(storage) { } }, labelStyle: { - backgroundColor: "#14213D", + backgroundColor: colors.notify, textAlign: "center", display: "flex", alignItems: "center", @@ -103,7 +142,7 @@ function GetTOTPList(storage) { margin: "10px", flexGrow: 1, fontSize: "20px", - color: "#E5E5E5", + color: colors.text, borderRadius: "5px", }, }); @@ -111,8 +150,9 @@ function GetTOTPList(storage) { { align: "center", style: { - color: "#ffffff", - fontSize: "16px", + color: colors.text, + fontSize: "18px", + fontWeight: "500" }, paragraph: true, }, @@ -126,29 +166,30 @@ function GetTOTPList(storage) { updateStorage(storage); }, style: { - backgroundColor: "#ba181b", + backgroundColor: colors.alert, fontSize: "18px", - color: "#ffffff", + color: colors.text, height: "fit-content", margin: "10px", }, - label: "DEL", + label: "Delete", }); const text = Text( { style: { - color: "#ffffff", + color: colors.text, fontSize: "14px", }, align: "center", }, - `${element.hashType} | ${element.digits} digits | ${element.fetchTime} seconds | offset ${element.timeOffset} seconds` + `${element.hashType} | ${element.digits} digits | ${element.fetchTime} seconds | ${element.timeOffset} sec offset` ); const view = View( { style: { textAlign: "center", - border: "2px solid white", + backgroundColor: colors.secondaryBg, + //border: "2px solid white", borderRadius: "5px", margin: "10px", }, diff --git a/setting/utils/queryParser.js b/setting/utils/queryParser.js index aca7871..ae94456 100644 --- a/setting/utils/queryParser.js +++ b/setting/utils/queryParser.js @@ -6,9 +6,9 @@ const otpauthScheme = "otpauth:/"; const googleMigrationScheme = "otpauth-migration:/"; export function getTOTPByLink(link) { - if(link.includes(otpauthScheme)) + if (link.includes(otpauthScheme)) return getByOtpauthScheme(link) - if(link.includes(googleMigrationScheme)) + if (link.includes(googleMigrationScheme)) return getByGoogleMigrationScheme(link) return null; @@ -21,7 +21,7 @@ function getHashType(algorithm) { else return "SHA-1"; } -function getByOtpauthScheme(link){ +function getByOtpauthScheme(link) { try { let args = link.split("/", otpauthScheme.length); let type = args[2]; //Returns 'hotp' or 'totp' @@ -31,7 +31,7 @@ function getByOtpauthScheme(link){ 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("digits=")[1]?.split("&")[0]; //Returns digits + 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 @@ -40,9 +40,9 @@ function getByOtpauthScheme(link){ if (secret === undefined) throw new Error("Secret not defined"); - if(issuer == client){ - issuer = args[3].split("issuer=")[1]?.split("&")[0]; - } + if (issuer == client) { + issuer = args[3].split("issuer=")[1]?.split("&")[0]; + } issuer = decodeURIComponent(issuer); client = decodeURIComponent(client); @@ -51,64 +51,64 @@ function getByOtpauthScheme(link){ secret, issuer, client, - digits, - period, + Number(digits), + Number(period), Number(offset), getHashType(algorithm) ); } catch (err) { - console.log(err) + console.log(err) return null; } } -function getByGoogleMigrationScheme(link){ +function getByGoogleMigrationScheme(link) { - let data = link.split("data=")[1]; //Returns base64 encoded data - data = decodeURIComponent(data); - let decode = base64decode(data); - let proto = decodeProto(decode); + let data = link.split("data=")[1]; //Returns base64 encoded data + data = decodeURIComponent(data); + let decode = base64decode(data); + let proto = decodeProto(decode); - let protoTotps = []; + let protoTotps = []; - proto.parts.forEach(part => { - if(part.type == TYPES.LENDELIM){ - protoTotps.push(decodeProto(part.value)); - } - }); + proto.parts.forEach(part => { + if (part.type == TYPES.LENDELIM) { + protoTotps.push(decodeProto(part.value)); + } + }); - 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); + 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); - let name = bytesToString(x.parts.filter(x => x.index == 2)[0].value); - let issuer = bytesToString(x.parts.filter(x => x.index == 3)[0].value); - - totps.push(new TOTP( + let name = bytesToString(x.parts.filter(x => x.index == 2)[0].value); + let issuer = bytesToString(x.parts.filter(x => x.index == 3)[0].value); + + totps.push(new TOTP( secret, issuer, name, 6, 30, 0, - "SHA-1" + "SHA-1" )); - }); + }); + + return totps; - return totps; - } 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; } \ No newline at end of file