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 @@
- +
 
@@ -38,8 +38,13 @@
- 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); +});