diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..ff2dfbc
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,28 @@
+{
+ "env": {
+ "webextensions": true,
+ "browser": true,
+ "es2021": true
+ },
+ "extends": [
+ "eslint:recommended"
+ ],
+ "globals": {
+ "process": "readonly"
+ },
+ "ignorePatterns": ["node_modules"],
+ "parserOptions": {
+ "ecmaVersion": "latest",
+ "ecmaFeatures": {
+ "jsx": true
+ },
+ "sourceType": "module"
+ },
+ "plugins": ["@stylistic/js"],
+ "rules": {
+ "no-console": ["error", {"allow": ["warn", "error"]}],
+ "no-unused-vars": ["error", {"argsIgnorePattern": "^_"}],
+ "@stylistic/js/indent": ["error", "tab"],
+ "@stylistic/js/semi": ["error", "always"]
+ }
+}
diff --git a/.gitignore b/.gitignore
index 8234428..645de9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,6 @@
*.zip
*.png
-.web-extension-id
\ No newline at end of file
+.web-extension-id
+
+node_modules
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..7715576
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+package-lock = false
\ No newline at end of file
diff --git a/background.js b/background.js
index fb93789..8515be2 100644
--- a/background.js
+++ b/background.js
@@ -4,74 +4,74 @@ let state = {
salt: window.crypto.getRandomValues(new Uint8Array(16)),
key: null,
entries: []
-}
+};
-let popup_port = null // communication channel with popup
+let popup_port = null; // communication channel with popup
function send_state(state)
{
- popup_port.postMessage({type: 'state', data: state})
+ popup_port.postMessage({type: 'state', data: state});
}
function show_error(error)
{
- console.error(error)
- popup_port.postMessage({type: 'notification', data: error})
+ console.error(error);
+ popup_port.postMessage({type: 'notification', data: error});
}
function b32decode(text)
{
- const up_text = text.toUpperCase()
- let result = []
- let lshift = 3
- let carry = 0
+ const up_text = text.toUpperCase();
+ let result = [];
+ let lshift = 3;
+ let carry = 0;
for(const index in up_text)
{
- const char = up_text[index]
+ const char = up_text[index];
if(char === '=')
- break
- let value = char.charCodeAt()
- if(value >= 65 && value <= 90) // char in [A- Z]
- value -= 65
- else if(value >= 50 && value <= 55) // char in [2-7]
- value -= 24
- else
- throw `Incorrect Base32 char '${text[index]}' at index ${index}`
+ break;
+ let value = char.charCodeAt();
+ if(value >= 65 && value <= 90) // char in [A- Z]
+ value -= 65;
+ else if(value >= 50 && value <= 55) // char in [2-7]
+ value -= 24;
+ else
+ throw `Incorrect Base32 char '${text[index]}' at index ${index}`;
if(lshift > 0) // next value is needed to complete the octet
{
- carry += value << lshift
- lshift -= 5
+ carry += value << lshift;
+ lshift -= 5;
if(lshift === 0) // last value aligned with octet
- carry = 0
+ carry = 0;
}
else // getting the last octet' bits
{
- result.push(carry + (value >>> (-lshift)))
- carry = (value << (lshift + 8)) & 248 // if lshit is 0 this will always be 0
- lshift += 3
+ result.push(carry + (value >>> (-lshift)));
+ carry = (value << (lshift + 8)) & 248; // if lshit is 0 this will always be 0
+ lshift += 3;
}
}
if(carry !== 0)
- result.push(carry)
- return result
+ result.push(carry);
+ return result;
}
// Create encryption key from password
async function setKey(password)
{
- const storage = await browser.storage.local.get()
+ const storage = await browser.storage.local.get();
if(storage.entries !== undefined && storage.iv !== undefined && storage.salt !== undefined)
{
try
{
- state.iv = new Uint8Array(b32decode(storage.iv))
- state.salt = new Uint8Array(b32decode(storage.salt))
+ state.iv = new Uint8Array(b32decode(storage.iv));
+ state.salt = new Uint8Array(b32decode(storage.salt));
}
catch(error)
{
- show_error(error)
- return
+ show_error(error);
+ return;
}
}
@@ -81,7 +81,7 @@ async function setKey(password)
encoder.encode(password),
'PBKDF2',
false,
- ['deriveKey'])
+ ['deriveKey']);
state.key = await window.crypto.subtle.deriveKey(
{
name: "PBKDF2",
@@ -92,26 +92,26 @@ async function setKey(password)
key_material,
{ "name": "AES-GCM", "length": 256},
true,
- ["encrypt", "decrypt"])
+ ["encrypt", "decrypt"]);
// If entries are set in the state we automatically restore them
if(storage.entries !== undefined)
- restore(storage)
+ restore(storage);
else
- send_state(state)
+ send_state(state);
}
// Decrypt entries saved in local storage
async function restore(storage)
{
- let decoded_secret = null
+ let decoded_secret = null;
try
{
- decoded_secret = new Uint8Array(b32decode(storage.entries))
+ decoded_secret = new Uint8Array(b32decode(storage.entries));
}
catch(error)
{
- show_error(error)
- return
+ show_error(error);
+ return;
}
try
@@ -125,36 +125,36 @@ async function restore(storage)
);
let decoder = new TextDecoder();
- state.entries = JSON.parse(decoder.decode(decrypted))
- send_state(state)
+ state.entries = JSON.parse(decoder.decode(decrypted));
+ send_state(state);
}
catch(_error)
{
- state.key = null
- show_error('Cannot decrypt entries: wrong password')
+ state.key = null;
+ show_error('Cannot decrypt entries: wrong password');
}
}
function connected(port) {
- popup_port = port
+ popup_port = port;
popup_port.onMessage.addListener((message) => {
- if(message.command === 'init')
+ if(message.command === 'init')
{
if(state.key === null)
- send_state(state)
+ send_state(state);
else
- browser.storage.local.get().then((storage) => { restore(storage) })
+ browser.storage.local.get().then((storage) => { restore(storage); });
}
else if(message.command === 'key')
- setKey(message.password)
+ setKey(message.password);
else if(message.command === 'logout')
{
- state.key = null
- state.entries = []
+ state.key = null;
+ state.entries = [];
}
else
- show_error(`Wrong message: ${message}`)
- })
+ show_error(`Wrong message: ${message}`);
+ });
}
browser.runtime.onConnect.addListener(connected);
\ No newline at end of file
diff --git a/icons/export.svg b/icons/export.svg
index 56951b1..0e1c8dc 100644
--- a/icons/export.svg
+++ b/icons/export.svg
@@ -1,3 +1,3 @@
\ No newline at end of file
diff --git a/icons/import.svg b/icons/import.svg
index 0e1c8dc..56951b1 100644
--- a/icons/import.svg
+++ b/icons/import.svg
@@ -1,3 +1,3 @@
\ No newline at end of file
diff --git a/manifest.json b/manifest.json
index 7985937..dd45e78 100644
--- a/manifest.json
+++ b/manifest.json
@@ -20,5 +20,5 @@
"storage",
"clipboardWrite"
],
- "version": "1.3.2"
+ "version": "1.4.0"
}
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..451ad3d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,6 @@
+{
+ "devDependencies": {
+ "@stylistic/eslint-plugin-js": "^1.6.3",
+ "eslint": "^8.57.0"
+ }
+}
diff --git a/popup.css b/popup.css
index 7022918..eeaa0c7 100644
--- a/popup.css
+++ b/popup.css
@@ -152,6 +152,21 @@ h2 {
right: 10px;
}
+#action {
+ display: flex;
+ justify-content: center;
+}
+
+#action > div {
+ display: flex;
+ justify-content: center;
+}
+
+#action img {
+ height: 2em;
+ cursor: pointer;
+}
+
#entry-add-container {
position: relative;
padding-bottom: 4px;
@@ -189,7 +204,6 @@ h2 {
flex-direction: row;
justify-content: space-between;
background-color: var(--error-bg-color);
- font-size: 1.2em;
padding: 4px;
transform: translateY(-100%);
transition: transform 0.3s;
@@ -204,6 +218,10 @@ h2 {
cursor: pointer;
}
+#notification-text {
+ white-space: pre-wrap;
+}
+
#password-container {
padding: 1em;
}
@@ -217,16 +235,6 @@ h2 {
margin-right: 8px;
}
-#store-action {
- display: flex;
- justify-content: center;
-}
-
-#store-action img {
- height: 2em;
- cursor: pointer;
-}
-
@keyframes timer0 {
from {
stroke-dashoffset: 0;
diff --git a/popup.html b/popup.html
index 8ca0023..d44df0d 100644
--- a/popup.html
+++ b/popup.html
@@ -7,7 +7,7 @@
-
diff --git a/popup.js b/popup.js
index c8e033f..7a30f79 100644
--- a/popup.js
+++ b/popup.js
@@ -1,140 +1,142 @@
-const password_container_elt = document.querySelector('#password-container')
-const password_form_elt = document.querySelector('#password-form')
-const master_password_elt = document.querySelector('#master-password')
-const master_visibility_elt = document.querySelector('#master-visibility')
-const entry_add_container_elt = document.querySelector('#entry-add-container')
-const logout_elt = document.querySelector('#logout')
-const store_action_elt = document.querySelector('#store-action')
-const export_elt = document.querySelector('#export')
-const entry_form_elt = document.querySelector('#entry-form')
-const entry_name_elt = document.querySelector('#entry-name')
-const entry_secret_elt = document.querySelector('#entry-secret')
-const entry_visibility_elt = document.querySelector('#entry-visibility')
-const entry_list_elt = document.querySelector('#entry-list')
-const notification_elt = document.querySelector('#notification')
-const notification_text_elt = document.querySelector('#notification-text')
+const password_container_elt = document.querySelector('#password-container');
+const password_form_elt = document.querySelector('#password-form');
+const master_password_elt = document.querySelector('#master-password');
+const master_visibility_elt = document.querySelector('#master-visibility');
+const entry_add_container_elt = document.querySelector('#entry-add-container');
+const logout_elt = document.querySelector('#logout');
+const store_action_elt = document.querySelector('#store-action');
+const export_elt = document.querySelector('#export');
+const import_action_elt = document.querySelector('#import-action');
+const import_elt = document.querySelector('#import');
+const entry_form_elt = document.querySelector('#entry-form');
+const entry_name_elt = document.querySelector('#entry-name');
+const entry_secret_elt = document.querySelector('#entry-secret');
+const entry_visibility_elt = document.querySelector('#entry-visibility');
+const entry_list_elt = document.querySelector('#entry-list');
+const notification_elt = document.querySelector('#notification');
+const notification_text_elt = document.querySelector('#notification-text');
-const b32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'
-const notification_delay = 5000
-const notification_repeat = 500
+const b32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
+const notification_delay = 5000;
+const notification_repeat = 500;
-let notifications = []
-let notification_timeout = null
-let refresh_timeout = null
+let notifications = [];
+let notification_timeout = null;
+let refresh_timeout = null;
function togglePassword(elt)
{
if(elt.getAttribute('type') === 'password')
- elt.setAttribute('type', 'text')
+ elt.setAttribute('type', 'text');
else
- elt.setAttribute('type', 'password')
+ elt.setAttribute('type', 'password');
}
function b32encode(bytes)
{
- let result = []
- let index = 0
- let lshift = 0
- let carry = 0
- let byte = bytes[index]
+ let result = [];
+ let index = 0;
+ let lshift = 0;
+ let carry = 0;
+ let byte = bytes[index];
while(index < bytes.length)
{
if(lshift < 0) // need to right shift (should have a carry)
{
- result.push(b32chars[carry + (byte >>> (-lshift + 3))])
- carry = 0
- lshift += 5
+ result.push(b32chars[carry + (byte >>> (-lshift + 3))]);
+ carry = 0;
+ lshift += 5;
}
else if(lshift > 3) // not enough bits to convert (save carry, next will be right shift)
{
- carry = ((byte << lshift) & 248) >> 3
- lshift -= 8
- index += 1
- byte = bytes[index]
+ carry = ((byte << lshift) & 248) >> 3;
+ lshift -= 8;
+ index += 1;
+ byte = bytes[index];
}
else // enough bits to convert
{
- result.push(b32chars[((byte << lshift) & 248) >> 3])
- lshift += 5
+ result.push(b32chars[((byte << lshift) & 248) >> 3]);
+ lshift += 5;
}
}
if(carry !== 0) // if carry left, add has last char
- result.push(b32chars[carry])
- return result.join('')
+ result.push(b32chars[carry]);
+ return result.join('');
}
function b32decode(text)
{
- const up_text = text.toUpperCase()
- let result = []
- let lshift = 3
- let carry = 0
+ const up_text = text.toUpperCase();
+ let result = [];
+ let lshift = 3;
+ let carry = 0;
for(const index in up_text)
{
- const char = up_text[index]
+ const char = up_text[index];
if(char === '=')
- break
- let value = char.charCodeAt()
+ break;
+ let value = char.charCodeAt();
if(value >= 65 && value <= 90) // char in [A- Z]
- value -= 65
+ value -= 65;
else if(value >= 50 && value <= 55) // char in [2-7]
- value -= 24
+ value -= 24;
else
- throw `Incorrect Base32 char '${text[index]}' at index ${index}`
+ throw `Incorrect Base32 char '${text[index]}' at index ${index}`;
if(lshift > 0) // next value is needed to complete the octet
{
- carry += value << lshift
- lshift -= 5
+ carry += value << lshift;
+ lshift -= 5;
if(lshift === 0) // last value aligned with octet
- carry = 0
+ carry = 0;
}
else // getting the last octet' bits
{
- result.push(carry + (value >>> (-lshift)))
- carry = (value << (lshift + 8)) & 248 // if lshit is 0 this will always be 0
- lshift += 3
+ result.push(carry + (value >>> (-lshift)));
+ carry = (value << (lshift + 8)) & 248; // if lshit is 0 this will always be 0
+ lshift += 3;
}
}
if(carry !== 0)
- result.push(carry)
- return result
+ result.push(carry);
+ return result;
}
function show_notification(text)
{
- notifications.push(text)
- _notification()
+ notifications.push(text);
+ _notification();
}
function show_error(error)
{
- console.error(error)
- show_notification(error)
+ console.error(error);
+ show_notification(error);
}
function close_notification()
{
- clearTimeout(notification_timeout)
- _next_notification()
+ clearTimeout(notification_timeout);
+ _next_notification();
}
function _next_notification()
{
- notification_timeout = null
- notification_elt.classList.remove('show')
- notifications.shift()
+ notification_timeout = null;
+ notification_elt.classList.remove('show');
+ notifications.shift();
if(notifications.length !== 0)
- setTimeout(_notification, notification_repeat)
+ setTimeout(_notification, notification_repeat);
}
function _notification()
{
if(notifications.length !== 0 && notification_timeout === null)
{
- notification_text_elt.firstChild.data = notifications[0]
- notification_elt.classList.add('show')
- notification_timeout = setTimeout(_next_notification, notification_delay)
+ notification_text_elt.firstChild.data = notifications[0];
+ notification_elt.classList.add('show');
+ notification_timeout = setTimeout(_next_notification, notification_delay);
}
}
@@ -142,160 +144,208 @@ class Store
{
constructor()
{
- this.entries = []
- this.iv = null
- this.salt = null
- this.key = null
+ this.entries = [];
+ this.iv = null;
+ this.salt = null;
+ this.key = null;
}
async loadState(state)
{
- this.iv = state.iv
- this.salt = state.salt
- this.key = state.key
+ this.iv = state.iv;
+ this.salt = state.salt;
+ this.key = state.key;
for(const entry of state.entries)
{
- await store.addEntry(entry.name, entry.secret, false)
+ await store.addEntry(entry.name, entry.secret, false);
}
- store.save()
+ store.save();
}
async addEntry(name, secret, save=true)
{
- const entry = await Entry.create(name, secret)
+ const entry = await Entry.create(name, secret);
if(entry === null)
- return
- entry.insert()
- this.entries.push(entry)
+ return;
+ entry.insert();
+ this.entries.push(entry);
if(save)
- this.save()
+ this.save();
}
async removeEntry(entry)
{
- entry.remove()
- this.entries = this.entries.filter(test_entry => test_entry !== entry)
- this.save()
+ entry.remove();
+ this.entries = this.entries.filter(test_entry => test_entry !== entry);
+ this.save();
}
async save()
{
if(this.key === null)
{
- show_error(`Cannot save store without key`)
- return
+ show_error(`Cannot save store without key`);
+ return;
}
let encoder = new TextEncoder();
const encrypted = await window.crypto.subtle.encrypt(
{name: 'AES-GCM', iv: this.iv},
this.key,
encoder.encode(JSON.stringify(
- this.entries.map(entry => {return {name: entry.name, secret: entry.secret}}))))
+ this.entries.map(entry => {return {name: entry.name, secret: entry.secret};}))));
await browser.storage.local.set({
iv: b32encode(this.iv),
salt: b32encode(this.salt),
entries: b32encode(new Uint8Array(encrypted))
- })
+ });
}
refresh()
{
- const now = Date.now()
- const count_time = now / 30000
- const counter = Math.floor(count_time)
- const animation_delay = (count_time - counter) * 30
+ const now = Date.now();
+ const count_time = now / 30000;
+ const counter = Math.floor(count_time);
+ const animation_delay = (count_time - counter) * 30;
if(refresh_timeout !== null)
- clearTimeout(refresh_timeout)
+ clearTimeout(refresh_timeout);
refresh_timeout = setTimeout(
- () => { store.refresh() },
- ((counter + 1) * 30000) - now + 1)
+ () => { store.refresh(); },
+ ((counter + 1) * 30000) - now + 1);
for(const entry of this.entries)
{
- entry.refresh(counter)
- entry.refreshTimer(animation_delay)
+ entry.refresh(counter);
+ entry.refreshTimer(animation_delay);
}
}
close()
{
- this.entries.forEach(entry => { entry.remove() })
- this.entries = []
- this.key = null
+ this.entries.forEach(entry => { entry.remove(); });
+ this.entries = [];
+ this.key = null;
}
export()
{
- let export_elt = document.createElement('a')
+ const export_elt = document.createElement('a');
export_elt.setAttribute('href', 'data:application/json,' + JSON.stringify(
- this.entries.map(entry => {return {name: entry.name, secret: entry.secret}})))
- export_elt.setAttribute('download', 'uotp_export.json')
- export_elt.style.display = 'none'
- document.body.appendChild(export_elt)
- export_elt.click()
- document.body.removeChild(export_elt)
+ this.entries.map(entry => {return {name: entry.name, secret: entry.secret};})));
+ export_elt.setAttribute('download', 'uotp_export.json');
+ export_elt.style.display = 'none';
+ document.body.appendChild(export_elt);
+ export_elt.click();
+ document.body.removeChild(export_elt);
+ }
+
+ import()
+ {
+ const import_elt = document.createElement('input');
+ import_elt.setAttribute('type', 'file');
+ import_elt.onchange = event => {
+ if(event.target.files.length > 0)
+ {
+ let reader = new FileReader();
+ reader.onload = () => {
+ let import_json = null;
+ try
+ {
+ import_json = JSON.parse(reader.result);
+ }
+ catch(error)
+ {
+ show_error('Error reading import file : not a valid json.');
+ }
+ if(!Array.isArray(import_json))
+ {
+ show_error('Invalid imported file : expecting a list of entries, imported JSON is not a list');
+ return;
+ }
+ const errors = [];
+ import_json.forEach((entry, index) => {
+ if(!entry.name)
+ {
+ errors.push(`Invalid entry #${index} in imported file : "name" not found`);
+ return;
+ }
+ if(!entry.secret)
+ {
+ errors.push(`Invalid entry #${index} in imported file : "secret" not found`);
+ return;
+ }
+ this.addEntry(entry.name, entry.secret);
+ });
+ if(errors.length > 0)
+ show_error(`${errors.length} errors in imported file :\n${errors.join('\n')}`);
+ };
+ reader.readAsText(event.target.files[0]);
+ }
+ };
+ document.body.appendChild(import_elt);
+ import_elt.click();
+ document.body.removeChild(import_elt);
}
}
-let store = new Store()
+let store = new Store();
class Entry
{
constructor(name, secret, key)
{
- this.name = name
- this.secret = secret
- this.key = key
- this.timer_count = 0
+ this.name = name;
+ this.secret = secret;
+ this.key = key;
+ this.timer_count = 0;
- let new_elt = document.createElement('div')
- new_elt.className = 'entry'
+ let new_elt = document.createElement('div');
+ new_elt.className = 'entry';
- let header_elt = document.createElement('div')
- header_elt.className = 'header'
+ let header_elt = document.createElement('div');
+ header_elt.className = 'header';
// Entry title
- let title_elt = document.createElement('h2')
- title_elt.textContent = name
- header_elt.appendChild(title_elt)
+ let title_elt = document.createElement('h2');
+ title_elt.textContent = name;
+ header_elt.appendChild(title_elt);
// Entry actions
- let action_elt = document.createElement('div')
- action_elt.className = "action"
- let edit_elt = document.createElement('img')
- edit_elt.src = "/icons/edit.svg"
- edit_elt.title = 'Edit'
- edit_elt.addEventListener('click', () => {this.openEdit()})
- action_elt.appendChild(edit_elt)
- let delete_elt = document.createElement('img')
- delete_elt.src = "/icons/delete.svg"
- delete_elt.title = 'Delete'
- delete_elt.addEventListener('click', () => {store.removeEntry(this)})
- action_elt.appendChild(delete_elt)
- header_elt.appendChild(action_elt)
+ let action_elt = document.createElement('div');
+ action_elt.className = "action";
+ let edit_elt = document.createElement('img');
+ edit_elt.src = "/icons/edit.svg";
+ edit_elt.title = 'Edit';
+ edit_elt.addEventListener('click', () => {this.openEdit();});
+ action_elt.appendChild(edit_elt);
+ let delete_elt = document.createElement('img');
+ delete_elt.src = "/icons/delete.svg";
+ delete_elt.title = 'Delete';
+ delete_elt.addEventListener('click', () => {store.removeEntry(this);});
+ action_elt.appendChild(delete_elt);
+ header_elt.appendChild(action_elt);
- new_elt.appendChild(header_elt)
+ new_elt.appendChild(header_elt);
// Entry OTP
- let otp_container_elt = document.createElement('div')
- otp_container_elt.className = 'otp-container'
- let otp_elt = document.createElement('div')
- otp_elt.className = 'otp'
+ let otp_container_elt = document.createElement('div');
+ otp_container_elt.className = 'otp-container';
+ let otp_elt = document.createElement('div');
+ otp_elt.className = 'otp';
otp_elt.addEventListener('click', () => {
- navigator.clipboard.writeText(otp_elt.textContent)
- })
- otp_container_elt.appendChild(otp_elt)
+ navigator.clipboard.writeText(otp_elt.textContent);
+ });
+ otp_container_elt.appendChild(otp_elt);
// let svg_timer_elt = document.createElement('svg')
- const svgNS = 'http://www.w3.org/2000/svg'
- let svg_timer_elt = document.createElementNS(svgNS, 'svg')
- svg_timer_elt.setAttributeNS(null, 'viewBox', '0 0 2 2')
- let timer_circle_elt = document.createElementNS(svgNS, 'circle')
- timer_circle_elt.setAttributeNS(null, 'r', '1')
- timer_circle_elt.setAttributeNS(null, 'cx', '1')
- timer_circle_elt.setAttributeNS(null, 'cy', '1')
- svg_timer_elt.appendChild(timer_circle_elt)
- otp_container_elt.appendChild(svg_timer_elt)
- new_elt.appendChild(otp_container_elt)
+ const svgNS = 'http://www.w3.org/2000/svg';
+ let svg_timer_elt = document.createElementNS(svgNS, 'svg');
+ svg_timer_elt.setAttributeNS(null, 'viewBox', '0 0 2 2');
+ let timer_circle_elt = document.createElementNS(svgNS, 'circle');
+ timer_circle_elt.setAttributeNS(null, 'r', '1');
+ timer_circle_elt.setAttributeNS(null, 'cx', '1');
+ timer_circle_elt.setAttributeNS(null, 'cy', '1');
+ svg_timer_elt.appendChild(timer_circle_elt);
+ otp_container_elt.appendChild(svg_timer_elt);
+ new_elt.appendChild(otp_container_elt);
this.elements = {
main: new_elt,
@@ -307,47 +357,47 @@ class Entry
action: action_elt,
edit: edit_elt,
delete: delete_elt
- }
+ };
}
// static function to construct Entry while being async
static async create(name, secret)
{
- let decoded_secret = ''
+ let decoded_secret = '';
try
{
- decoded_secret = new Uint8Array(b32decode(secret))
+ decoded_secret = new Uint8Array(b32decode(secret));
}
catch(error)
{
- show_error(error)
- return null
+ show_error(error);
+ return null;
}
const key = await window.crypto.subtle.importKey(
- 'raw', decoded_secret, {name: 'HMAC', hash: 'SHA-1'}, false, ['sign'])
- const new_entry = new Entry(name, secret, key)
- const count_time = Date.now() / 30000
- const counter = Math.floor(count_time)
- await new_entry.refresh(counter)
- new_entry.refreshTimer((count_time - counter) * 30)
- return new_entry
+ 'raw', decoded_secret, {name: 'HMAC', hash: 'SHA-1'}, false, ['sign']);
+ const new_entry = new Entry(name, secret, key);
+ const count_time = Date.now() / 30000;
+ const counter = Math.floor(count_time);
+ await new_entry.refresh(counter);
+ new_entry.refreshTimer((count_time - counter) * 30);
+ return new_entry;
}
async regenerateKey()
{
- let decoded_secret = ''
+ let decoded_secret = '';
try
{
- decoded_secret = new Uint8Array(b32decode(this.secret))
+ decoded_secret = new Uint8Array(b32decode(this.secret));
}
catch(error)
{
- show_error(error)
- return
+ show_error(error);
+ return;
}
this.key = await window.crypto.subtle.importKey(
- 'raw', decoded_secret, {name: 'HMAC', hash: 'SHA-1'}, false, ['sign'])
+ 'raw', decoded_secret, {name: 'HMAC', hash: 'SHA-1'}, false, ['sign']);
}
async refresh(counter)
@@ -357,133 +407,129 @@ class Entry
// Set timestamp as byte array for hmac hash (4 bytes is enough)
const buffer = new ArrayBuffer(8); // 8 byte buffer needed for HMAC (initialized with 0)
const view = new DataView(buffer);
- view.setUint32(4, counter)
+ view.setUint32(4, counter);
// HMAC returns am ArrayBuffer wich is unusable as is (no type), using Uint8Array is needed
- const signature = new Uint8Array(await window.crypto.subtle.sign("hmac", this.key, buffer))
+ const signature = new Uint8Array(await window.crypto.subtle.sign("hmac", this.key, buffer));
// Getting the offset from the last byte with its highest bit masked to 0
- const offset = signature[signature.length - 1] & 15
+ const offset = signature[signature.length - 1] & 15;
// Masking the highest bit of the byte located at calculated offset
- signature[offset] = signature[offset] & 127
+ signature[offset] = signature[offset] & 127;
// Reading 4 bytes from the hash from the offset (highest byte was masked) and getting the last
// 6 digits from its Uint32 representation
this.elements.otp.textContent = new DataView(
- signature.slice(offset, offset + 4).buffer).getUint32().toString().slice(-6)
+ signature.slice(offset, offset + 4).buffer).getUint32().toString().slice(-6);
}
refreshTimer(animation_delay)
{
- this.timer_count = 1- this.timer_count
- this.elements.timer_circle.style.animationDelay = `-${animation_delay}s`
- this.elements.timer_circle.style.animationName = `timer${this.timer_count}`
+ this.timer_count = 1- this.timer_count;
+ this.elements.timer_circle.style.animationDelay = `-${animation_delay}s`;
+ this.elements.timer_circle.style.animationName = `timer${this.timer_count}`;
}
insert()
{
- entry_list_elt.appendChild(this.elements.main)
+ entry_list_elt.appendChild(this.elements.main);
}
remove()
{
- entry_list_elt.removeChild(this.elements.main)
+ entry_list_elt.removeChild(this.elements.main);
}
openEdit()
{
- this.elements.title.classList.add('hidden')
- this.elements.otp.classList.add('hidden')
- this.elements.edit.classList.add('hidden')
- this.elements.delete.classList.add('hidden')
- this.elements.svg_timer_elt.classList.add('hidden')
+ [this.elements.title, this.elements.otp, this.elements.edit, this.elements.delete,
+ this.elements.svg_timer_elt].forEach(elt => {elt.classList.add('hidden');} );
- let name_input_elt = document.createElement('input')
- name_input_elt.value = this.name
- this.elements.header.insertBefore(name_input_elt, this.elements.action)
+ let name_input_elt = document.createElement('input');
+ name_input_elt.value = this.name;
+ this.elements.header.insertBefore(name_input_elt, this.elements.action);
- let secret_container_elt = document.createElement('div')
- secret_container_elt.className = 'password-container'
- let secret_input_elt = document.createElement('input')
- secret_input_elt.setAttribute('type', 'password')
- secret_input_elt.value = this.secret
- secret_container_elt.appendChild(secret_input_elt)
- let secret_visibility_elt = document.createElement('img')
- secret_visibility_elt.className = 'icon visibility'
- secret_visibility_elt.src = '/icons/visibility.svg'
- secret_container_elt.appendChild(secret_visibility_elt)
- secret_visibility_elt.addEventListener('click', () => {togglePassword(secret_input_elt)})
- this.elements.main.appendChild(secret_container_elt)
+ let secret_container_elt = document.createElement('div');
+ secret_container_elt.className = 'password-container';
+ let secret_input_elt = document.createElement('input');
+ secret_input_elt.setAttribute('type', 'password');
+ secret_input_elt.value = this.secret;
+ secret_container_elt.appendChild(secret_input_elt);
+ let secret_visibility_elt = document.createElement('img');
+ secret_visibility_elt.className = 'icon visibility';
+ secret_visibility_elt.src = '/icons/visibility.svg';
+ secret_container_elt.appendChild(secret_visibility_elt);
+ secret_visibility_elt.addEventListener('click', () => {togglePassword(secret_input_elt);});
+ this.elements.main.appendChild(secret_container_elt);
- let done_elt = document.createElement('img')
- done_elt.src = "/icons/done.svg"
- done_elt.title = 'Save'
- this.elements.action.appendChild(done_elt)
- let close_elt = document.createElement('img')
- close_elt.src = "/icons/close.svg"
- close_elt.title = 'Cancel'
- this.elements.action.appendChild(close_elt)
+ let done_elt = document.createElement('img');
+ done_elt.src = "/icons/done.svg";
+ done_elt.title = 'Save';
+ this.elements.action.appendChild(done_elt);
+ let close_elt = document.createElement('img');
+ close_elt.src = "/icons/close.svg";
+ close_elt.title = 'Cancel';
+ this.elements.action.appendChild(close_elt);
done_elt.addEventListener('click', () => {
- this.name = name_input_elt.value
- this.elements.title.textContent = this.name
+ this.name = name_input_elt.value;
+ this.elements.title.textContent = this.name;
// Remove any white spaces (can happen for readability)
- this.secret = secret_input_elt.value.replace(/\s/g, '')
+ this.secret = secret_input_elt.value.replace(/\s/g, '');
this.regenerateKey().then(() => {
- const count_time = Date.now() / 30000
- const counter = Math.floor(count_time)
- this.refresh(counter)
- this.refreshTimer((count_time - counter) * 30)
- })
- store.save()
- this.elements.main.removeChild(secret_container_elt)
- this.elements.action.removeChild(done_elt)
- this.elements.action.removeChild(close_elt)
- this.elements.header.removeChild(name_input_elt)
- this.closeEdit()
- })
+ const count_time = Date.now() / 30000;
+ const counter = Math.floor(count_time);
+ this.refresh(counter);
+ this.refreshTimer((count_time - counter) * 30);
+ });
+ store.save();
+ this.elements.main.removeChild(secret_container_elt);
+ this.elements.action.removeChild(done_elt);
+ this.elements.action.removeChild(close_elt);
+ this.elements.header.removeChild(name_input_elt);
+ this.closeEdit();
+ });
close_elt.addEventListener('click', () => {
- this.elements.main.removeChild(secret_container_elt)
- this.elements.action.removeChild(done_elt)
- this.elements.action.removeChild(close_elt)
- this.elements.header.removeChild(name_input_elt)
- this.closeEdit()
- })
+ this.elements.main.removeChild(secret_container_elt);
+ this.elements.action.removeChild(done_elt);
+ this.elements.action.removeChild(close_elt);
+ this.elements.header.removeChild(name_input_elt);
+ this.closeEdit();
+ });
}
closeEdit()
{
- this.elements.title.classList.remove('hidden')
- this.elements.otp.classList.remove('hidden')
- this.elements.edit.classList.remove('hidden')
- this.elements.delete.classList.remove('hidden')
- this.elements.svg_timer_elt.classList.remove('hidden')
+ [this.elements.title, this.elements.otp, this.elements.edit, this.elements.delete,
+ this.elements.svg_timer_elt].forEach(elt => { elt.classList.remove('hidden'); } );
}
}
async function addEntry(event)
{
- event.preventDefault()
+ event.preventDefault();
// Remove any white spaces (can happen for readability)
- await store.addEntry(entry_name_elt.value, entry_secret_elt.value.replace(/\s/g, ''))
+ await store.addEntry(entry_name_elt.value, entry_secret_elt.value.replace(/\s/g, ''));
}
function start()
{
- password_container_elt.classList.add('hidden')
- store_action_elt.classList.remove('hidden')
- entry_add_container_elt.classList.remove('hidden')
- const now = Date.now()
+ password_container_elt.classList.add('hidden');
+ [store_action_elt, import_action_elt, entry_add_container_elt].forEach(elt => {
+ elt.classList.remove('hidden'); });
+
+ const now = Date.now();
if(refresh_timeout !== null)
- clearTimeout(refresh_timeout)
- refresh_timeout = setTimeout(() => { store.refresh() }, ((Math.floor(now / 30000) + 1) * 30000) - now + 1)
+ clearTimeout(refresh_timeout);
+ refresh_timeout = setTimeout(() => { store.refresh(); }, ((Math.floor(now / 30000) + 1) * 30000) - now + 1);
}
function stop()
{
- store_action_elt.classList.add('hidden')
- entry_add_container_elt.classList.add('hidden')
- password_container_elt.classList.remove('hidden')
+ [store_action_elt, import_action_elt, entry_add_container_elt].forEach(elt => {
+ elt.classList.add('hidden');
+ });
+ password_container_elt.classList.remove('hidden');
if(refresh_timeout !== null)
- clearTimeout(refresh_timeout)
- store.close()
+ clearTimeout(refresh_timeout);
+ store.close();
}
let background_port = browser.runtime.connect({name:"background"});
@@ -491,34 +537,35 @@ let background_port = browser.runtime.connect({name:"background"});
async function init()
{
password_form_elt.addEventListener('submit', (event) => {
- event.preventDefault()
- background_port.postMessage({command: 'key', password: master_password_elt.value})
- master_password_elt.value = ''
- })
- master_visibility_elt.addEventListener('click', () => {togglePassword(master_password_elt)})
- entry_form_elt.addEventListener('submit', addEntry)
- entry_visibility_elt.addEventListener('click', () => {togglePassword(entry_secret_elt)})
+ event.preventDefault();
+ background_port.postMessage({command: 'key', password: master_password_elt.value});
+ master_password_elt.value = '';
+ });
+ master_visibility_elt.addEventListener('click', () => {togglePassword(master_password_elt);});
+ entry_form_elt.addEventListener('submit', addEntry);
+ entry_visibility_elt.addEventListener('click', () => {togglePassword(entry_secret_elt);});
logout_elt.addEventListener('click', () => {
- background_port.postMessage({command: 'logout', password: master_password_elt.value})
- stop()
- })
- export_elt.addEventListener('click', () => { store.export() })
- document.querySelector('#notification img').addEventListener('click', close_notification)
+ background_port.postMessage({command: 'logout', password: master_password_elt.value});
+ stop();
+ });
+ export_elt.addEventListener('click', () => { store.export(); });
+ import_elt.addEventListener('click', () => { store.import(); });
+ document.querySelector('#notification img').addEventListener('click', close_notification);
- background_port.postMessage({command: 'init'})
+ background_port.postMessage({command: 'init'});
background_port.onMessage.addListener((message) => {
if(message.type === 'state')
{
if(message.data.key !== null)
- store.loadState(message.data).then(start)
+ store.loadState(message.data).then(start);
}
else if(message.type === 'notification')
- show_notification(message.data)
+ show_notification(message.data);
else
- show_error(`unexpected background message with type ${message.type}`)
- })
+ show_error(`unexpected background message with type ${message.type}`);
+ });
}
init().catch(error => {
- show_error(error)
-})
+ show_error(error);
+});