diff --git a/app/fileManager.js b/app/fileManager.js index 8e9b3f40..0dd46d21 100644 --- a/app/fileManager.js +++ b/app/fileManager.js @@ -173,9 +173,9 @@ export default function(state, emitter) { } }); - emitter.on('password', async ({ password, file }) => { + emitter.on('password', async ({ existingPassword, password, file }) => { try { - await FileSender.setPassword(password, file); + await FileSender.setPassword(existingPassword, password, file); metrics.addedPassword({ size: file.size }); file.password = password; state.storage.writeFiles(); diff --git a/app/fileSender.js b/app/fileSender.js index 5b04b884..3965edf5 100644 --- a/app/fileSender.js +++ b/app/fileSender.js @@ -19,11 +19,9 @@ async function sendPassword(file, authKey, rawAuth) { xhr.onreadystatechange = () => { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { - return resolve(xhr.response); - } - if (xhr.status === 401) { const nonce = xhr.getResponseHeader('WWW-Authenticate').split(' ')[1]; file.nonce = nonce; + return resolve(xhr.response); } reject(new Error(xhr.status)); } @@ -262,7 +260,7 @@ export default class FileSender extends Nanobus { return this.uploadFile(encrypted, metadata, new Uint8Array(rawAuth)); } - static async setPassword(password, file) { + static async setPassword(existingPassword, password, file) { const encoder = new TextEncoder(); const secretKey = await window.crypto.subtle.importKey( 'raw', @@ -293,6 +291,28 @@ export default class FileSender extends Nanobus { false, ['deriveKey'] ); + const oldPwdkey = await window.crypto.subtle.importKey( + 'raw', + encoder.encode(existingPassword), + { name: 'PBKDF2' }, + false, + ['deriveKey'] + ); + const oldAuthKey = await window.crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt: encoder.encode(file.url), + iterations: 100, + hash: 'SHA-256' + }, + oldPwdkey, + { + name: 'HMAC', + hash: 'SHA-256' + }, + true, + ['sign'] + ); const newAuthKey = await window.crypto.subtle.deriveKey( { name: 'PBKDF2', @@ -309,11 +329,12 @@ export default class FileSender extends Nanobus { ['sign'] ); const rawAuth = await window.crypto.subtle.exportKey('raw', newAuthKey); + const aKey = existingPassword ? oldAuthKey : authKey; try { - await sendPassword(file, authKey, rawAuth); + await sendPassword(file, aKey, rawAuth); } catch (e) { if (e.message === '401' && file.nonce !== e.nonce) { - await sendPassword(file, authKey, rawAuth); + await sendPassword(file, aKey, rawAuth); } else { throw e; } diff --git a/app/templates/share.js b/app/templates/share.js index 622d7f90..291f2ff9 100644 --- a/app/templates/share.js +++ b/app/templates/share.js @@ -10,8 +10,25 @@ function passwordComplete(state, password) { const el = html([ `
${state.translate('passwordResult', { password: '
'
-    })}
` + })} + + + ` ]); + + el.querySelector('#resetButton').onclick = toggleResetInput; + el.querySelector('#unlock-reset-input').oninput = inputChanged; + const passwordOriginal = document.createElement('div'); passwordOriginal.className = 'passwordOriginal'; passwordOriginal.innerText = password; @@ -19,11 +36,35 @@ function passwordComplete(state, password) { const passwordStar = document.createElement('div'); passwordStar.className = 'passwordStar'; passwordStar.innerText = password.replace(/./g, '●'); - el.lastElementChild.appendChild(passwordOriginal); - el.lastElementChild.appendChild(passwordStar); + + el.firstElementChild.appendChild(passwordOriginal); + el.firstElementChild.appendChild(passwordStar); + return el; } +function inputChanged() { + const resetInput = document.getElementById('unlock-reset-input'); + const resetBtn = document.getElementById('unlock-reset-btn'); + if (resetInput.value.length > 0) { + resetBtn.classList.remove('btn-hidden'); + resetInput.classList.remove('input-no-btn'); + } else { + resetBtn.classList.add('btn-hidden'); + resetInput.classList.add('input-no-btn'); + } +} + +function toggleResetInput(event) { + const form = event.target.parentElement.querySelector('form'); + if (form.style.visibility === 'hidden' || form.style.visibility === '') { + form.style.visibility = 'visible'; + } else { + form.style.visibility = 'hidden'; + } + inputChanged(); +} + function expireInfo(file, translate, emit) { const hours = Math.floor(EXPIRE_SECONDS / 60 / 60); const el = html([ @@ -99,6 +140,21 @@ module.exports = function(state, emit) { `; + if (div.querySelector('#reset-form')) + div.querySelector('#reset-form').onsubmit = resetPassword; + + function resetPassword(event) { + event.preventDefault(); + const existingPassword = document.querySelector('.passwordOriginal') + .innerText; + const password = document.querySelector('#unlock-reset-input').value; + if (password.length > 0) { + document.getElementById('copy').classList.remove('wait-password'); + document.getElementById('copy-btn').disabled = false; + emit('password', { existingPassword, password, file }); + } + } + function showPopup() { const popupText = document.querySelector('.popuptext'); popupText.classList.add('show'); diff --git a/app/templates/uploadPassword.js b/app/templates/uploadPassword.js index 0df27f05..e2e72d9d 100644 --- a/app/templates/uploadPassword.js +++ b/app/templates/uploadPassword.js @@ -52,11 +52,12 @@ module.exports = function(state, emit) { function setPassword(event) { event.preventDefault(); + const existingPassword = null; const password = document.getElementById('unlock-input').value; if (password.length > 0) { document.getElementById('copy').classList.remove('wait-password'); document.getElementById('copy-btn').disabled = false; - emit('password', { password, file }); + emit('password', { existingPassword, password, file }); } } diff --git a/assets/main.css b/assets/main.css index 3048c86d..81b7873b 100644 --- a/assets/main.css +++ b/assets/main.css @@ -648,6 +648,25 @@ tbody { background: #efeff1; } +#resetButton { + width: 80px; + height: 30px; + background: #fff; + border: 1px solid rgba(12, 12, 13, 0.3); + border-radius: 5px; + font-size: 15px; + margin-top: 5px; + margin-left: 15px; + margin-bottom: 12px; + line-height: 24px; + cursor: pointer; + color: #313131; +} + +#resetButton:hover { + background: #efeff1; +} + .send-new { font-size: 15px; margin: auto; @@ -857,7 +876,8 @@ tbody { padding-right: 10px; } -#unlock-btn { +#unlock-btn, +#unlock-reset-btn { flex: 0 1 165px; background: #0297f8; border-radius: 0 6px 6px 0; @@ -874,7 +894,8 @@ tbody { white-space: nowrap; } -#unlock-btn:hover { +#unlock-btn:hover, +#unlock-reset-btn:hover { background-color: #0287e8; } @@ -1163,7 +1184,8 @@ tbody { } #copy-btn, - #unlock-btn { + #unlock-btn, + #unlock-reset-btn { border-radius: 0 0 6px 6px; flex: 0 1 65px; } diff --git a/public/locales/en-US/send.ftl b/public/locales/en-US/send.ftl index a2c8367a..765b5bb1 100644 --- a/public/locales/en-US/send.ftl +++ b/public/locales/en-US/send.ftl @@ -95,6 +95,7 @@ footerLinkTerms = Terms footerLinkCookies = Cookies requirePasswordCheckbox = Require a password to download this file addPasswordButton = Add password +changePasswordButton = Change passwordTryAgain = Incorrect password. Try again. // This label is followed by the password needed to download a file passwordResult = Password: { $password }