├── .gitignore
├── resources
├── icon.png
├── loading.png
├── GitHub-Mark-64px.png
├── widgets.gui
├── styles.css
├── styles~348x250.css
└── index.gui
├── Authenticator-screenshot1.png
├── Authenticator-screenshot2.png
├── package.json
├── common
├── totp.js
├── hmac-sha1.js
├── globals.js
├── util.js
└── js-sha1.js
├── LICENSE
├── companion
├── settings.js
├── tokens.js
└── index.js
├── CHANGELOG.md
├── README.md
├── app
├── tokens.js
├── index.js
└── interface.js
└── settings
└── index.jsx
/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /node_modules
3 | package-lock.json
4 | .vscode
5 | .eslintrc.json
6 |
--------------------------------------------------------------------------------
/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lixxia/fitbit-authenticator/HEAD/resources/icon.png
--------------------------------------------------------------------------------
/resources/loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lixxia/fitbit-authenticator/HEAD/resources/loading.png
--------------------------------------------------------------------------------
/Authenticator-screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lixxia/fitbit-authenticator/HEAD/Authenticator-screenshot1.png
--------------------------------------------------------------------------------
/Authenticator-screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lixxia/fitbit-authenticator/HEAD/Authenticator-screenshot2.png
--------------------------------------------------------------------------------
/resources/GitHub-Mark-64px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lixxia/fitbit-authenticator/HEAD/resources/GitHub-Mark-64px.png
--------------------------------------------------------------------------------
/resources/widgets.gui:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "fitbit": {
3 | "appUUID": "ff58cce2-1f9d-4a2f-917d-3cb70c11b542",
4 | "appType": "app",
5 | "appDisplayName": "Authenticator",
6 | "iconFile": "resources/icon.png",
7 | "wipeColor": "#607d8b",
8 | "requestedPermissions": [],
9 | "buildTargets": [
10 | "meson",
11 | "higgs",
12 | "gemini"
13 | ],
14 | "i18n": {
15 | "en": {
16 | "name": "Authenticator"
17 | }
18 | }
19 | },
20 | "scripts": {
21 | "build": "fitbit-build"
22 | },
23 | "devDependencies": {
24 | "@fitbit/sdk": "~3.1.0",
25 | "@fitbit/sdk-cli": "^1.0.1"
26 | },
27 | "dependencies": {}
28 | }
29 |
--------------------------------------------------------------------------------
/common/totp.js:
--------------------------------------------------------------------------------
1 | import {hmac_sha1} from "./hmac-sha1.js";
2 |
3 | // Convert a TOTP counter to a Uint8Array
4 | var countertouint8array = function(counter) {
5 | let array = new Uint8Array(8);
6 | array[0] = (counter & 0xff00000000000000) >>> 56;
7 | array[1] = (counter & 0x00ff000000000000) >>> 48;
8 | array[2] = (counter & 0x0000ff0000000000) >>> 40;
9 | array[3] = (counter & 0x000000ff00000000) >>> 32;
10 | array[4] = (counter & 0x00000000ff000000) >>> 24
11 | array[5] = (counter & 0x0000000000ff0000) >>> 16;
12 | array[6] = (counter & 0x000000000000ff00) >>> 8;
13 | array[7] = (counter & 0x00000000000000ff);
14 | return array;
15 | };
16 |
17 | export function TOTP() {
18 | this.getOTP = function(secret, epoch) {
19 | let time = countertouint8array(Math.floor(epoch / 30));
20 | let hmac = hmac_sha1(secret, time);
21 |
22 | let offset = hmac[hmac.length - 1] & 0x0f;
23 | let otp = ((hmac[offset] << 24 | hmac[offset+1] << 16 | hmac[offset+2] << 8 | hmac[offset+3]) & 0x7fffffff) + "";
24 | return otp.substr(otp.length - 6, 6);
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Laura Barber
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/common/hmac-sha1.js:
--------------------------------------------------------------------------------
1 | import {sha1} from "./js-sha1.js";
2 |
3 | // Compute the HMAC-SHA1 of a message with a key. Key, message, and return value are all Uint8Arrays.
4 | export function hmac_sha1(keyArray, messageArray) {
5 | if (keyArray.length > 64) {
6 | keyArray = sha1(keyArray);
7 | }
8 | if (keyArray.length < 64) {
9 | let oldkeyArray = keyArray;
10 | keyArray = new Uint8Array(64);
11 | keyArray.set(oldkeyArray); // Copies data from the old Uint8Array into the new one
12 | }
13 | let o_key_pad = keyArray.map(v => v ^ 0x5c);
14 | let i_key_pad = keyArray.map(v => v ^ 0x36);
15 | return sha1_uint8array(concat(o_key_pad, sha1_uint8array(concat(i_key_pad, messageArray))));
16 | }
17 |
18 | // Compute the SHA1 hash of a Uint8Array, returned as a Uint8Array
19 | function sha1_uint8array(messageArray) {
20 | // sha1.arrayBuffer uses DataView, which doesn't exist in Fitbit's JS engine. sha1.array is still pretty fast, and compatible with Uint8Array.
21 | return new Uint8Array(sha1.array(messageArray));
22 | }
23 |
24 | // Concatenate two Uint8Arrays
25 | function concat(arr1, arr2) {
26 | let out = new Uint8Array(arr1.length + arr2.length);
27 | out.set(arr1);
28 | out.set(arr2, arr1.length);
29 | return out;
30 | }
--------------------------------------------------------------------------------
/common/globals.js:
--------------------------------------------------------------------------------
1 | export const TOKEN_LIST = "token_list"; //displayed in settings
2 | export const FILE_NAME = "tokens.json"; //internal token storage
3 | export const TOKEN_SECRETS = "token_secrets"; //stored internally (old)
4 | export const TOKEN_NUM = 10;
5 |
6 | export const DEFAULT_SETTINGS = {
7 | color: "0",
8 | font: {"selected":[0],"values":[{"name":"System-Regular"}]},
9 | text_toggle: false,
10 | groups: {"selected":[1],"values":[{"name":"Two (123 456)","value":"1"}]},
11 | display_always: false
12 | };
13 |
14 | export const COLORS = [
15 | {color: "#001F3F", value: "0"}, //navy
16 | {color: "#0074D9", value: "1"}, //blue
17 | {color: "#39CCCC", value: "2"}, //teal
18 | {color: "#327d5b", value: "3"}, //olive
19 | {color: "#2ECC40", value: "4"}, //green
20 | {color: "#EBCB00", value: "5"}, //yellow
21 | {color: "#FF851B", value: "6"}, //orange
22 | {color: "#FF4136", value: "7"}, //red
23 | {color: "#F012BE", value: "8"}, //fuchsia
24 | {color: "#B10DC9", value: "9"}, //purple
25 | {color: "#85144B", value: "10"}, //maroon
26 | {color: "#969696", value: "11"}, //gray
27 | {color: "#111111", value: "12"} //black
28 | ]
29 |
30 | export const FONTS = [
31 | {name: "System-Regular"},
32 | {name: "Tungsten-Medium"},
33 | {name: "Colfax-Regular"},
34 | {name: "Fabrikat-Bold"},
35 | {name: "Seville-Condensed"}
36 | ]
--------------------------------------------------------------------------------
/companion/settings.js:
--------------------------------------------------------------------------------
1 | import {settingsStorage} from "settings";
2 | import {TOKEN_LIST} from "../common/globals.js";
3 |
4 | export function singleSetting(key, setting) {
5 | let arr = {};
6 | arr[key] = JSON.parse(setting);
7 | settingsStorage.setItem(key, setting);
8 | return arr;
9 | }
10 |
11 | export function reorderItems(setting) {
12 | let reorder = {"reorder": JSON.parse(setting)};
13 |
14 | settingsStorage.setItem(TOKEN_LIST, setting);
15 | return reorder;
16 | }
17 |
18 | export function deleteItem(oldVal,newVal) {
19 | let deleteArr = [];
20 | if (newVal.length === 0) {
21 | //Delete only item
22 | deleteArr.push(oldVal[oldVal.length-1]["name"]);
23 | } else {
24 | let newNames = [];
25 | let oldNames = [];
26 |
27 | for (let o of oldVal) { oldNames.push(o["name"]); }
28 | for (let n of newVal) { newNames.push(n["name"]); }
29 |
30 | deleteArr = oldNames.filter(function(i) {
31 | return newNames.indexOf(i) < 0;
32 | });
33 | }
34 | return {"delete": deleteArr};
35 | }
36 |
37 | export function checkUniqueNames(newArray) {
38 | let testArray = {};
39 | let duplicates = [];
40 |
41 | newArray.map(function(item) {
42 | let itemName = item["name"].split(":")[0];
43 | if (itemName in testArray) {
44 | testArray[itemName].duplicate = true;
45 | item.duplicate = true;
46 | duplicates.push(itemName);
47 | }
48 | else {
49 | testArray[itemName] = item;
50 | delete item.duplicate;
51 | }
52 | });
53 |
54 | return duplicates;
55 | }
56 |
57 | export function revokeLast(item, array) {
58 | array.pop();
59 | settingsStorage.setItem(item, JSON.stringify(array));
60 | }
61 |
62 | export function stripTokens() {
63 | // After storing the secrets, don't want any tokens visible in settings
64 | let tokens = JSON.parse(settingsStorage.getItem(TOKEN_LIST));
65 |
66 | for (let i=0; i 0 ) {
41 | console.error("Item already exists, removing latest user submission.");
42 | settings.revokeLast(TOKEN_LIST, this.tokensList);
43 | return;
44 | } else if ( ! this.validateToken(newVal[newVal.length-1]["name"].split(":")[1])) {
45 | console.error("Invalid token, removing latest user submission.");
46 | settings.revokeLast(TOKEN_LIST, this.tokensList);
47 | return;
48 | }
49 |
50 | // Build token
51 | for (let i=0; i {
14 | console.warn("Companion Socket Open");
15 | restoreSettings();
16 | };
17 |
18 | // Message socket closes
19 | messaging.peerSocket.onclose = () => {
20 | console.warn("Companion Socket Closed");
21 | };
22 |
23 | function sendSettings() {
24 | outbox.enqueue('settings.cbor', cbor.encode(settingsCache))
25 | .then(ft => console.warn('Settings sent.'))
26 | .catch(error => console.error("Error sending settings: " + error));
27 | }
28 |
29 | settingsStorage.onchange = evt => {
30 | if (evt.key === "color" || evt.key === "progress_toggle" || evt.key === "text_toggle" || evt.key === "font" || evt.key === "display_always" || evt.key === "groups") { //simple setting
31 | settingsCache[evt.key] = JSON.parse(evt.newValue);
32 | sendSettings();
33 | //sendVal(settings.singleSetting(evt.key, evt.newValue));
34 | } else if (evt.oldValue !== null && evt.oldValue.length === evt.newValue.length) { //reorder
35 | sendVal(settings.reorderItems(evt.newValue));
36 | } else if (evt.oldValue !== null && evt.newValue.length < evt.oldValue.length) { //delete
37 | sendVal(settings.deleteItem(JSON.parse(evt.oldValue),JSON.parse(evt.newValue)));
38 | } else { // new token sent
39 | sendVal(token.newToken(JSON.parse(evt.newValue)));
40 | settings.stripTokens();
41 | }
42 | };
43 |
44 |
45 | // Restore any previously saved settings and send to the device
46 | function restoreSettings() {
47 | for (let index = 0; index < settingsStorage.length; index++) {
48 | let key = settingsStorage.key(index);
49 | // If users have any data from old version, send over & delete
50 | if (key && key == "token_secrets") {
51 | sendVal(JSON.parse(settingsStorage.getItem(TOKEN_SECRETS)));
52 | settingsStorage.removeItem(TOKEN_SECRETS);
53 | }
54 | // Skip token_list is only names
55 | else if (key && key !== "token_list") {
56 | let value = settingsStorage.getItem(key);
57 | try {
58 | settingsCache[key] = JSON.parse(value);
59 | }
60 | catch(ex) {
61 | settingsCache[key] = value;
62 | }
63 | }
64 | }
65 | }
66 |
67 | // Send data to device using Messaging API
68 | function sendVal(data) {
69 | if (messaging.peerSocket.readyState === messaging.peerSocket.OPEN) {
70 | messaging.peerSocket.send(data);
71 | } else {
72 | console.error("Unable to send data.");
73 | }
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/resources/index.gui:
--------------------------------------------------------------------------------
1 |
81 |
82 |
--------------------------------------------------------------------------------
/common/util.js:
--------------------------------------------------------------------------------
1 | // Convert a number to a special monospace number
2 | export function monoDigits(num, pad = true) {
3 | let monoNum = '';
4 | if (typeof num === 'number') {
5 | num |= 0;
6 | if (pad && num < 10) {
7 | monoNum = c0 + monoDigit(num);
8 | } else {
9 | while (num > 0) {
10 | monoNum = monoDigit(num % 10) + monoNum;
11 | num = (num / 10) | 0;
12 | }
13 | }
14 | } else {
15 | let text = num.toString();
16 | let textLen = text.length;
17 | for (let i = 0; i < textLen; i++) {
18 | monoNum += monoDigit(text.charAt(i));
19 | }
20 | }
21 | return monoNum;
22 | }
23 |
24 | const c0 = String.fromCharCode(0x10);
25 | const c1 = String.fromCharCode(0x11);
26 | const c2 = String.fromCharCode(0x12);
27 | const c3 = String.fromCharCode(0x13);
28 | const c4 = String.fromCharCode(0x14);
29 | const c5 = String.fromCharCode(0x15);
30 | const c6 = String.fromCharCode(0x16);
31 | const c7 = String.fromCharCode(0x17);
32 | const c8 = String.fromCharCode(0x18);
33 | const c9 = String.fromCharCode(0x19);
34 |
35 | function monoDigit(digit) {
36 | switch (digit) {
37 | case 0: return c0;
38 | case 1: return c1;
39 | case 2: return c2;
40 | case 3: return c3;
41 | case 4: return c4;
42 | case 5: return c5;
43 | case 6: return c6;
44 | case 7: return c7;
45 | case 8: return c8;
46 | case 9: return c9;
47 | case '0': return c0;
48 | case '1': return c1;
49 | case '2': return c2;
50 | case '3': return c3;
51 | case '4': return c4;
52 | case '5': return c5;
53 | case '6': return c6;
54 | case '7': return c7;
55 | case '8': return c8;
56 | case '9': return c9;
57 | default: return digit;
58 | }
59 | }
60 |
61 | let base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
62 |
63 | // Convert a base32-encoded string to a Uint8Array
64 | export function base32ToUint8Array(base32) {
65 | base32 = base32.replace(/ +/g, ""); // Strip whitespace
66 |
67 | let array = new Uint8Array(Math.floor(base32.length * 5 / 8));
68 | let arrayIndex = 0;
69 | let bits = 0;
70 | let value = 0;
71 |
72 | for (let i=0; i= 0 && val < 32 ) {
83 | value = (value << 5) | val;
84 | bits += 5;
85 |
86 | // Transfer a byte into the Uint8Array if there is enough data
87 | if (bits >= 8) {
88 | array[arrayIndex++] = (value >>> (bits - 8)) & 0xFF;
89 | value = value & 0xFF;
90 | bits -= 8;
91 | }
92 | } else {
93 | throw Error("Character out of range: " + char);
94 | }
95 | }
96 |
97 | return array;
98 | }
99 |
--------------------------------------------------------------------------------
/app/tokens.js:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import {FILE_NAME} from "../common/globals.js";
3 | import {base32ToUint8Array} from "../common/util.js";
4 |
5 | export function AuthToken() {
6 | try {
7 | this.file = fs.readFileSync(FILE_NAME, "cbor");
8 | } catch (err) {
9 | console.error("File not found, initializing JSON.");
10 | this.file = {"data":[]};
11 | }
12 | if (this.convertTokensToUint8Array()) {
13 | fs.writeFileSync(FILE_NAME, this.file, "cbor");
14 | }
15 | }
16 |
17 | // Convert any existing string/base32-encoded tokens to a Uint8Array, and wrap raw ArrayBuffer tokens
18 | AuthToken.prototype.convertTokensToUint8Array = function() {
19 | let changed = false;
20 | for (let i in this.file.data) {
21 | if (typeof this.file.data[i]["token"] == "string") {
22 | this.file.data[i]["token"] = base32ToUint8Array(this.file.data[i]["token"]);
23 | changed = true;
24 | } else if (this.file.data[i]["token"] instanceof ArrayBuffer) {
25 | // Data read from a CBOR file comes back as a base ArrayBuffer, which needs to be wrapped in a Uint8Array to use.
26 | // This is not a conversion that can be written to the filesystem, so we don't set `changed`.
27 | this.file.data[i]["token"] = new Uint8Array(this.file.data[i]["token"]);
28 | } else if (!(this.file.data[i]["token"] instanceof Uint8Array)) {
29 | console.error("Unknown object found in token field: " + this.file.data[i]["token"]);
30 | }
31 | }
32 | return changed;
33 | }
34 |
35 | AuthToken.prototype.reorderTokens = function(tokens) {
36 | let newOrder = [];
37 | let fileOrder = [];
38 | let newTokens = {"data":[]};
39 |
40 | for (let token of tokens) {
41 | newOrder.push(token.name)
42 | }
43 |
44 | for (let t in this.file.data) {
45 | fileOrder.push(this.file.data[t].name);
46 | }
47 |
48 | for (let name of newOrder) {
49 | newTokens.data.push(this.file.data[fileOrder.indexOf(name)]);
50 | }
51 | this.file = newTokens;
52 | fs.writeFileSync(FILE_NAME, this.file, "cbor");
53 | return this.file;
54 | }
55 |
56 | AuthToken.prototype.writeToken = function(tokens) {
57 | tokens = JSON.parse(JSON.stringify(tokens)); //hasOwnProperty does not function correctly without this re-parse
58 |
59 | for (let j in tokens) {
60 | if (tokens[j].hasOwnProperty('token')) {
61 | this.file.data.push({"name":tokens[j]["name"],"token":base32ToUint8Array(tokens[j]["token"])});
62 | }
63 | }
64 | fs.writeFileSync(FILE_NAME, this.file, "cbor");
65 | return this.file;
66 | }
67 |
68 | AuthToken.prototype.deleteToken = function(tokenNames) {
69 | for (let i in this.file.data) {
70 | if(tokenNames.indexOf(this.file.data[i]["name"]) != -1) {
71 | this.file.data.splice(i, 1);
72 | }
73 | }
74 | fs.writeFileSync(FILE_NAME, this.file, "cbor");
75 | return this.file;
76 | }
77 |
78 | AuthToken.prototype.reloadTokens = function() {
79 | return this.file;
80 | }
--------------------------------------------------------------------------------
/settings/index.jsx:
--------------------------------------------------------------------------------
1 | import { COLORS,FONTS,DEFAULT_SETTINGS } from "../common/globals.js";
2 |
3 | function setDefaults(props) {
4 | for (let key in DEFAULT_SETTINGS) {
5 | if (!props.settings[key]) {
6 | props.settingsStorage.setItem(key, JSON.stringify(DEFAULT_SETTINGS[key]));
7 | }
8 | }
9 | };
10 |
11 | function mySettings(props) {
12 | setDefaults(props);
13 |
14 | return (
15 |
16 | Tokens}
17 | description={
18 |
19 | Entry Format
20 | Names of tokens must be unique. All tokens must be entered in the format name:base32token, the colon delimeter must exist.
21 | Invalid formatting or tokens will result in the item being rejected.
22 | Items can be reordered in the settings.
23 | Security Considerations
24 | Any secret tokens entered are sent and stored on the watch. Once stored they are stripped from the displayed settings so that they are no longer viewable. If the token is still visible in the settings, reload the watch app and it should update.
25 | If the application is uninstalled from the watch, all associated data is permanently deleted. Please consider this before using this as your only means of accessing your tokens.
26 |
27 | }>
28 |
38 | }
39 | />
40 |
41 |
42 | Appearance}>
44 |
48 |
52 |
57 |
66 |
71 |
72 | Support}>
73 | In some cases the companion may be unable to communicate with the watch. It's best to reopen the app/companion if this happens.
74 | If you experience any problems please contact me or create an issue on github!
75 |
76 |
81 |
82 |
83 |
84 |
85 |
86 | );
87 | }
88 |
89 | registerSettingsPage(mySettings);
--------------------------------------------------------------------------------
/app/index.js:
--------------------------------------------------------------------------------
1 | import * as messaging from "messaging";
2 | import { AuthUI } from "./interface.js";
3 | import { display } from "display";
4 | import { AuthToken } from "./tokens.js";
5 | import { inbox } from "file-transfer"
6 | import { readFileSync } from "fs";
7 | import { DEFAULT_SETTINGS } from "../common/globals.js"
8 |
9 | let ui = new AuthUI();
10 | let token = new AuthToken();
11 | let ids = [];
12 | let groups = 1;
13 | let file;
14 | let settings = DEFAULT_SETTINGS;
15 |
16 | inbox.onnewfile = processInbox;
17 |
18 | function loadSettings()
19 | {
20 | try {
21 | settings = readFileSync('settings.cbor', "cbor");
22 | setDefaults();
23 | } catch (e) {
24 | settings = DEFAULT_SETTINGS;
25 | }
26 | applySettings();
27 | }
28 |
29 | function setDefaults() {
30 | for (let key in DEFAULT_SETTINGS) {
31 | if (!settings.hasOwnProperty(key)) {
32 | settings[key] = DEFAULT_SETTINGS[key];
33 | }
34 | }
35 | }
36 |
37 | function applySettings() {
38 | ui.updateColors(settings.color);
39 | ui.updateFont(settings.font.selected);
40 | ui.updateCounter(settings.text_toggle);
41 |
42 | if (settings.display_always) {
43 | display.autoOff = false;
44 | } else {
45 | display.autoOff = true;
46 | }
47 |
48 | groups = settings.groups.selected;
49 | ui.updateUI("rerender", null, groups);
50 | }
51 |
52 | function processInbox() {
53 | let fileName;
54 | while (fileName = inbox.nextFile()) {
55 | if (fileName === 'settings.cbor') {
56 | loadSettings();
57 | }
58 | }
59 | }
60 |
61 | // STARTUP TASKS
62 | loadSettings();
63 |
64 | try {
65 | file = token.reloadTokens();
66 | ui.updateUI("loaded", file, groups);
67 | manageTimer("start");
68 | } catch (e) {
69 | ui.updateUI("none");
70 | }
71 |
72 | display.addEventListener("change", function() {
73 | if (display.on) {
74 | wake();
75 | } else {
76 | sleep();
77 | }
78 | });
79 |
80 | // Listen for the onopen event
81 | messaging.peerSocket.onopen = function() {
82 | //ui.updateUI("loading");
83 | messaging.peerSocket.send("Open");
84 | }
85 |
86 | // Listen for the onmessage event
87 | messaging.peerSocket.onmessage = function(evt) {
88 | if (evt.data.hasOwnProperty('reorder')) {
89 | ui.updateUI("loaded", token.reorderTokens(evt.data.reorder), groups);
90 | } else if (evt.data.hasOwnProperty('delete')) {
91 | ui.updateUI("loaded", token.deleteToken(evt.data.delete), groups);
92 | } else {
93 | if (file.data.length === 0) {
94 | manageTimer("start");
95 | //ui.resumeTimer(); //First token, begin animation
96 | }
97 | ui.updateUI("loaded", token.writeToken(evt.data), groups);
98 | }
99 | }
100 |
101 | messaging.peerSocket.onerror = function(err) {
102 | console.error("Connection error: " + err.code + " - " + err.message);
103 | ui.updateUI("error");
104 | }
105 |
106 | function wake() {
107 | ui.updateUI("loading");
108 | ui.updateUI("loaded", token.reloadTokens(), groups);
109 | manageTimer("start");
110 | }
111 |
112 | function sleep() {
113 | // Stop progress
114 | ui.stopAnimation();
115 | manageTimer("stop");
116 | }
117 |
118 | function manageTimer(arg) {
119 | if (arg === "stop") {
120 | ui.stopAnimation();
121 | for (let i of ids) {
122 | clearInterval(i);
123 | }
124 | ids = []; //empty array
125 | } else if (arg === "start") {
126 | if (ids.length === 0) { //don't double start animation
127 | ui.resumeTimer();
128 | let id = setInterval(timer, 1000);
129 | ids.push(id);
130 | }
131 | } else {
132 | console.error("Invalid timer management argument.")
133 | }
134 | }
135 |
136 | function timer() {
137 | // Update tokens every 30s
138 | let epoch = Math.round(new Date().getTime() / 1000.0);
139 | let countDown = 30 - (epoch % 30);
140 | if (epoch % 30 == 0) {
141 | ui.updateTextTimer("loading");
142 | manageTimer("stop");
143 | ui.updateUI("loaded", token.reloadTokens(), groups);
144 | manageTimer("start");
145 | } else {
146 | ui.updateTextTimer(countDown);
147 | }
148 | }
149 |
150 |
151 | //Test Codes
152 | //ZVZG5UZU4D7MY4DH
153 | //test:ZVZG 5UZU 4D7M Y4DH ZVZG ZVZG AB
154 | //JBSWY3DPEHPK3PXP
--------------------------------------------------------------------------------
/common/js-sha1.js:
--------------------------------------------------------------------------------
1 | /*
2 | Modified to add a module export and remove the unused 'nodeWrap' function ('p' in minified js) which caused build warnings
3 | */
4 |
5 | // A variable 'window' is required to get access to the sha1 object
6 | var window = {
7 | // These 2 settings are required to make the companion app work in the simulator
8 | JS_SHA1_NO_NODE_JS: true,
9 | JS_SHA1_NO_COMMON_JS: true,
10 | };
11 |
12 | /*
13 | * [js-sha1]{@link https://github.com/emn178/js-sha1}
14 | *
15 | * @version 0.6.0
16 | * @author Chen, Yi-Cyuan [emn178@gmail.com]
17 | * @copyright Chen, Yi-Cyuan 2014-2017
18 | * @license MIT
19 | */
20 | !function(){"use strict";function t(t){t?(f[0]=f[16]=f[1]=f[2]=f[3]=f[4]=f[5]=f[6]=f[7]=f[8]=f[9]=f[10]=f[11]=f[12]=f[13]=f[14]=f[15]=0,this.blocks=f):this.blocks=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],this.h0=1732584193,this.h1=4023233417,this.h2=2562383102,this.h3=271733878,this.h4=3285377520,this.block=this.start=this.bytes=this.hBytes=0,this.finalized=this.hashed=!1,this.first=!0}var h="object"==typeof window?window:{},s=!h.JS_SHA1_NO_NODE_JS&&"object"==typeof process&&process.versions&&process.versions.node;s&&(h=global);var i=!h.JS_SHA1_NO_COMMON_JS&&"object"==typeof module&&module.exports,e="function"==typeof define&&define.amd,r="0123456789abcdef".split(""),o=[-2147483648,8388608,32768,128],n=[24,16,8,0],a=["hex","array","digest","arrayBuffer"],f=[],u=function(h){return function(s){return new t(!0).update(s)[h]()}},c=function(){var h=u("hex");s&&(h=p(h)),h.create=function(){return new t},h.update=function(t){return h.create().update(t)};for(var i=0;i>2]|=t[r]<>2]|=i<>2]|=(192|i>>6)<>2]|=(128|63&i)<=57344?(a[e>>2]|=(224|i>>12)<>2]|=(128|i>>6&63)<>2]|=(128|63&i)<>2]|=(240|i>>18)<>2]|=(128|i>>12&63)<>2]|=(128|i>>6&63)<>2]|=(128|63&i)<=64?(this.block=a[16],this.start=e-64,this.hash(),this.hashed=!0):this.start=e}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296<<0,this.bytes=this.bytes%4294967296),this}},t.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var t=this.blocks,h=this.lastByteIndex;t[16]=this.block,t[h>>2]|=o[3&h],this.block=t[16],h>=56&&(this.hashed||this.hash(),t[0]=this.block,t[16]=t[1]=t[2]=t[3]=t[4]=t[5]=t[6]=t[7]=t[8]=t[9]=t[10]=t[11]=t[12]=t[13]=t[14]=t[15]=0),t[14]=this.hBytes<<3|this.bytes>>>29,t[15]=this.bytes<<3,this.hash()}},t.prototype.hash=function(){var t,h,s=this.h0,i=this.h1,e=this.h2,r=this.h3,o=this.h4,n=this.blocks;for(t=16;t<80;++t)h=n[t-3]^n[t-8]^n[t-14]^n[t-16],n[t]=h<<1|h>>>31;for(t=0;t<20;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i&e|~i&r)+o+1518500249+n[t]<<0)<<5|o>>>27)+(s&(i=i<<30|i>>>2)|~s&e)+r+1518500249+n[t+1]<<0)<<5|r>>>27)+(o&(s=s<<30|s>>>2)|~o&i)+e+1518500249+n[t+2]<<0)<<5|e>>>27)+(r&(o=o<<30|o>>>2)|~r&s)+i+1518500249+n[t+3]<<0)<<5|i>>>27)+(e&(r=r<<30|r>>>2)|~e&o)+s+1518500249+n[t+4]<<0,e=e<<30|e>>>2;for(;t<40;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i^e^r)+o+1859775393+n[t]<<0)<<5|o>>>27)+(s^(i=i<<30|i>>>2)^e)+r+1859775393+n[t+1]<<0)<<5|r>>>27)+(o^(s=s<<30|s>>>2)^i)+e+1859775393+n[t+2]<<0)<<5|e>>>27)+(r^(o=o<<30|o>>>2)^s)+i+1859775393+n[t+3]<<0)<<5|i>>>27)+(e^(r=r<<30|r>>>2)^o)+s+1859775393+n[t+4]<<0,e=e<<30|e>>>2;for(;t<60;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i&e|i&r|e&r)+o-1894007588+n[t]<<0)<<5|o>>>27)+(s&(i=i<<30|i>>>2)|s&e|i&e)+r-1894007588+n[t+1]<<0)<<5|r>>>27)+(o&(s=s<<30|s>>>2)|o&i|s&i)+e-1894007588+n[t+2]<<0)<<5|e>>>27)+(r&(o=o<<30|o>>>2)|r&s|o&s)+i-1894007588+n[t+3]<<0)<<5|i>>>27)+(e&(r=r<<30|r>>>2)|e&o|r&o)+s-1894007588+n[t+4]<<0,e=e<<30|e>>>2;for(;t<80;t+=5)s=(h=(i=(h=(e=(h=(r=(h=(o=(h=s<<5|s>>>27)+(i^e^r)+o-899497514+n[t]<<0)<<5|o>>>27)+(s^(i=i<<30|i>>>2)^e)+r-899497514+n[t+1]<<0)<<5|r>>>27)+(o^(s=s<<30|s>>>2)^i)+e-899497514+n[t+2]<<0)<<5|e>>>27)+(r^(o=o<<30|o>>>2)^s)+i-899497514+n[t+3]<<0)<<5|i>>>27)+(e^(r=r<<30|r>>>2)^o)+s-899497514+n[t+4]<<0,e=e<<30|e>>>2;this.h0=this.h0+s<<0,this.h1=this.h1+i<<0,this.h2=this.h2+e<<0,this.h3=this.h3+r<<0,this.h4=this.h4+o<<0},t.prototype.hex=function(){this.finalize();var t=this.h0,h=this.h1,s=this.h2,i=this.h3,e=this.h4;return r[t>>28&15]+r[t>>24&15]+r[t>>20&15]+r[t>>16&15]+r[t>>12&15]+r[t>>8&15]+r[t>>4&15]+r[15&t]+r[h>>28&15]+r[h>>24&15]+r[h>>20&15]+r[h>>16&15]+r[h>>12&15]+r[h>>8&15]+r[h>>4&15]+r[15&h]+r[s>>28&15]+r[s>>24&15]+r[s>>20&15]+r[s>>16&15]+r[s>>12&15]+r[s>>8&15]+r[s>>4&15]+r[15&s]+r[i>>28&15]+r[i>>24&15]+r[i>>20&15]+r[i>>16&15]+r[i>>12&15]+r[i>>8&15]+r[i>>4&15]+r[15&i]+r[e>>28&15]+r[e>>24&15]+r[e>>20&15]+r[e>>16&15]+r[e>>12&15]+r[e>>8&15]+r[e>>4&15]+r[15&e]},t.prototype.toString=t.prototype.hex,t.prototype.digest=function(){this.finalize();var t=this.h0,h=this.h1,s=this.h2,i=this.h3,e=this.h4;return[t>>24&255,t>>16&255,t>>8&255,255&t,h>>24&255,h>>16&255,h>>8&255,255&h,s>>24&255,s>>16&255,s>>8&255,255&s,i>>24&255,i>>16&255,i>>8&255,255&i,e>>24&255,e>>16&255,e>>8&255,255&e]},t.prototype.array=t.prototype.digest,t.prototype.arrayBuffer=function(){this.finalize();var t=new ArrayBuffer(20),h=new DataView(t);return h.setUint32(0,this.h0),h.setUint32(4,this.h1),h.setUint32(8,this.h2),h.setUint32(12,this.h3),h.setUint32(16,this.h4),t};var y=c();i?module.exports=y:(h.sha1=y,e&&define(function(){return y}))}();
21 |
22 | export var sha1 = window.sha1;
--------------------------------------------------------------------------------
/app/interface.js:
--------------------------------------------------------------------------------
1 | import {TOKEN_NUM,COLORS,FONTS} from "../common/globals.js";
2 | import {TOTP} from "../common/totp.js";
3 | import document from "document";
4 | import { me as device } from "device";
5 |
6 | export function AuthUI() {
7 | this.tokenList = document.getElementById("tokenList");
8 | this.statusText = document.getElementById("status");
9 | this.loadingText = document.getElementById("loading-text");
10 | this.loadingAnim = document.getElementById("loading-anim");
11 | this.spinnerCenter = document.getElementById("spinner-center");
12 | this.spinnerCorner = document.getElementById("spinner-corner");
13 | this.statusBg = document.getElementById("status-bg");
14 | this.prog = ['0','1','2','3'].map(num => document.getElementById(`prog${num}`));
15 | this.prog_bg = ['0','1','2','3'].map(num => document.getElementById(`prog${num}-bg`));
16 | this.width = device.screen.width;
17 | this.height = device.screen.height;
18 | this.ids = [];
19 |
20 | this.tiles = [];
21 | for (let i=0; i= self.width) {
173 | clearInterval(id);
174 | self.prog[1].y2 = 0;
175 | self.startProgress(1);
176 | } else {
177 | bar.x2 += updateInterval;
178 | }
179 | }
180 | else if (num === 1) {
181 | if (bar.y2 >= self.height) {
182 | clearInterval(id);
183 | self.prog[2].x2 = self.width;
184 | self.startProgress(2);
185 | } else {
186 | bar.y2 += updateInterval;
187 | }
188 | } else if (num === 2) {
189 | if (bar.x2 <= 0) {
190 | clearInterval(id);
191 | self.prog[3].y2 = self.height;
192 | self.startProgress(3);
193 | } else {
194 | bar.x2 -= updateInterval;
195 | }
196 | } else if (num === 3) {
197 | if (bar.y2 <= 0) {
198 | clearInterval(id);
199 | } else {
200 | bar.y2 -= updateInterval;
201 | }
202 | }
203 | }
204 | }
205 |
206 | AuthUI.prototype.clearProgress = function() {
207 | this.stopAnimation();
208 | this.prog[0].x2 = 0;
209 | this.prog[1].y2 = 0;
210 | this.prog[2].x2 = this.width;
211 | this.prog[3].y2 = this.height;
212 | }
213 |
214 | AuthUI.prototype.resumeTimer = function() {
215 | let epoch = Math.round(new Date().getTime() / 1000.0);
216 | let catchUp = (epoch % 30) * 43;
217 | let i=0;
218 | this.clearProgress();
219 | while (catchUp > 0) {
220 | if (i === 0) {
221 | this.prog[0].x2 = Math.min(this.width,catchUp);
222 | } else if (i === 1) {
223 | this.prog[0].x2 = this.width;
224 | this.prog[1].y2 = Math.min(this.height,catchUp);
225 | } else if (i === 2) {
226 | this.prog[0].x2 = this.width;
227 | this.prog[1].y2 = this.height;
228 | this.prog[2].x2 = Math.min(this.width,this.width - catchUp);
229 | } else if (i === 3) {
230 | this.prog[0].x2 = this.width;
231 | this.prog[1].y2 = this.height;
232 | this.prog[2].x2 = 0;
233 | this.prog[3].y2 = Math.min(this.height,this.height - catchUp);
234 | }
235 | i++;
236 | catchUp -= this.width;
237 | }
238 |
239 | if (i === 0) {
240 | this.startProgress(0);
241 | } else {
242 | this.startProgress(i-1);
243 | }
244 | }
245 |
246 | AuthUI.prototype.stopAnimation = function() {
247 | for (let i of this.ids) {
248 | clearInterval(i);
249 | }
250 | }
--------------------------------------------------------------------------------