Implement import function

* Add ESLint rules
* Change code style : using semi-columns
* Change error notification : using pre + wrap to be able to add
  newlines
* Bump version to 1.4.0
This commit is contained in:
Corentin 2024-03-04 14:07:46 +09:00
commit c20a6397fc
11 changed files with 433 additions and 336 deletions

28
.eslintrc.json Normal file
View file

@ -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"]
}
}

4
.gitignore vendored
View file

@ -2,4 +2,6 @@
*.zip
*.png
.web-extension-id
.web-extension-id
node_modules

1
.npmrc Normal file
View file

@ -0,0 +1 @@
package-lock = false

View file

@ -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);

View file

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48" style="stroke:#ccc;fill:#ccc;">
<path d="M11 40q-1.2 0-2.1-.9Q8 38.2 8 37v-7.15h3V37h26v-7.15h3V37q0 1.2-.9 2.1-.9.9-2.1.9Zm13-7.65-9.65-9.65 2.15-2.15 6 6V8h3v18.55l6-6 2.15 2.15Z"/>
<path d="M11 40q-1.2 0-2.1-.9Q8 38.2 8 37v-7.15h3V37h26v-7.15h3V37q0 1.2-.9 2.1-.9.9-2.1.9Zm11.5-7.65V13.8l-6 6-2.15-2.15L24 8l9.65 9.65-2.15 2.15-6-6v18.55Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 274 B

After

Width:  |  Height:  |  Size: 283 B

Before After
Before After

View file

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" viewBox="0 0 48 48" style="stroke:#ccc;fill:#ccc;">
<path d="M11 40q-1.2 0-2.1-.9Q8 38.2 8 37v-7.15h3V37h26v-7.15h3V37q0 1.2-.9 2.1-.9.9-2.1.9Zm11.5-7.65V13.8l-6 6-2.15-2.15L24 8l9.65 9.65-2.15 2.15-6-6v18.55Z"/>
<path d="M11 40q-1.2 0-2.1-.9Q8 38.2 8 37v-7.15h3V37h26v-7.15h3V37q0 1.2-.9 2.1-.9.9-2.1.9Zm13-7.65-9.65-9.65 2.15-2.15 6 6V8h3v18.55l6-6 2.15 2.15Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 283 B

After

Width:  |  Height:  |  Size: 274 B

Before After
Before After

View file

@ -20,5 +20,5 @@
"storage",
"clipboardWrite"
],
"version": "1.3.2"
"version": "1.4.0"
}

6
package.json Normal file
View file

@ -0,0 +1,6 @@
{
"devDependencies": {
"@stylistic/eslint-plugin-js": "^1.6.3",
"eslint": "^8.57.0"
}
}

View file

@ -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;

View file

@ -7,7 +7,7 @@
</head>
<body>
<div id="notification">
<span id="notification-text"> </span>
<pre id="notification-text"> </pre>
<img src="/icons/close.svg"/>
</div>
<div id="password-container">
@ -38,8 +38,13 @@
</form>
</div>
<div id="entry-list">
<div id="store-action" class="hidden">
<img id="export" src="/icons/export.svg" title="Export" />
<div id="action">
<div id="store-action" class="hidden">
<img id="export" src="/icons/export.svg" title="Export" />
</div>
<div id="import-action" class="hidden">
<img id="import" src="/icons/import.svg" title="Import" />
</div>
</div>
</div>
<script src="/popup.js"></script>

579
popup.js
View file

@ -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);
});