Breaking: Replace deprecated / archived crypto library
- Remove `gibberish-aes` - Switch to Web Crypto API for encryption - Replace old `md5` key-derivation with modern PBKDF2 - Follow OWASP recommendation for number of iterations in PBKDF2 This is marked as a breaking change as it fully removes the old encryption code which breaks any secret stored with the previous version. During the update the store must be cleared or the user will receive a lot of garbage instead of their data. Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
895705bacb
commit
fb05e214f1
4 changed files with 63 additions and 31 deletions
22
src/app.vue
22
src/app.vue
|
@ -166,7 +166,7 @@
|
|||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import AES from 'gibberish-aes/src/gibberish-aes'
|
||||
import crypto from './crypto.js'
|
||||
|
||||
const passwordCharset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
const passwordLength = 20
|
||||
|
@ -218,9 +218,8 @@ export default {
|
|||
this.securePassword = [...window.crypto.getRandomValues(new Uint8Array(passwordLength))]
|
||||
.map(n => passwordCharset[n % passwordCharset.length])
|
||||
.join('')
|
||||
const secret = AES.enc(this.secret, this.securePassword)
|
||||
|
||||
axios.post('api/create', { secret })
|
||||
crypto.enc(this.secret, this.securePassword)
|
||||
.then(secret => axios.post('api/create', { secret })
|
||||
.then(resp => {
|
||||
this.secretId = resp.data.secret_id
|
||||
this.secret = ''
|
||||
|
@ -238,7 +237,7 @@ export default {
|
|||
this.error = this.$t('alert-something-went-wrong')
|
||||
this.showError = true
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
return false
|
||||
},
|
||||
|
@ -267,11 +266,16 @@ export default {
|
|||
requestSecret() {
|
||||
axios.get(`api/get/${this.secretId}`)
|
||||
.then(resp => {
|
||||
let secret = resp.data.secret
|
||||
if (this.securePassword) {
|
||||
secret = AES.dec(secret, this.securePassword)
|
||||
}
|
||||
const secret = resp.data.secret
|
||||
if (!this.securePassword) {
|
||||
this.secret = secret
|
||||
return
|
||||
}
|
||||
|
||||
crypto.dec(secret, this.securePassword)
|
||||
.then(secret => {
|
||||
this.secret = secret
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
switch (err.response.status) {
|
||||
|
|
35
src/crypto.js
Normal file
35
src/crypto.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
const opensslBanner = new Uint8Array(new TextEncoder('utf8').encode('Salted__'))
|
||||
const pbkdf2Params = { hash: 'SHA-512', iterations: 300000, name: 'PBKDF2' }
|
||||
|
||||
function decrypt(passphrase, encData) {
|
||||
const data = new Uint8Array(atob(encData).split('')
|
||||
.map(c => c.charCodeAt(0)))
|
||||
|
||||
return deriveKey(passphrase, data.slice(8, 16))
|
||||
.then(({ iv, key }) => window.crypto.subtle.decrypt({ iv, name: 'AES-CBC' }, key, data.slice(16)))
|
||||
.then(data => new TextDecoder('utf8').decode(data))
|
||||
}
|
||||
|
||||
function deriveKey(passphrase, salt) {
|
||||
return window.crypto.subtle.importKey('raw', new TextEncoder('utf8').encode(passphrase), 'PBKDF2', false, ['deriveBits'])
|
||||
.then(passwordKey => window.crypto.subtle.deriveBits({ ...pbkdf2Params, salt }, passwordKey, 384))
|
||||
.then(key => window.crypto.subtle.importKey('raw', key.slice(0, 32), { name: 'AES-CBC' }, false, ['encrypt', 'decrypt'])
|
||||
.then(aesKey => ({ iv: key.slice(32, 48), key: aesKey })))
|
||||
}
|
||||
|
||||
function encrypt(passphrase, salt, plainData) {
|
||||
return deriveKey(passphrase, salt)
|
||||
.then(({ iv, key }) => window.crypto.subtle.encrypt({ iv, name: 'AES-CBC' }, key, new TextEncoder('utf8').encode(plainData)))
|
||||
.then(encData => new Uint8Array([...opensslBanner, ...salt, ...new Uint8Array(encData)]))
|
||||
.then(data => btoa(String.fromCharCode.apply(null, data)))
|
||||
}
|
||||
|
||||
function generateSalt() {
|
||||
const salt = new Uint8Array(8) // Salt MUST consist of 8 byte
|
||||
return window.crypto.getRandomValues(salt)
|
||||
}
|
||||
|
||||
export default {
|
||||
dec: (cipherText, passphrase) => decrypt(passphrase, cipherText),
|
||||
enc: (plainText, passphrase) => encrypt(passphrase, generateSalt(), plainText),
|
||||
}
|
6
src/package-lock.json
generated
6
src/package-lock.json
generated
|
@ -10,7 +10,6 @@
|
|||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"bootswatch": "^4.6.0",
|
||||
"gibberish-aes": "^1.0.0",
|
||||
"popper.js": "^1.16.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-i18n": "^8.25.0"
|
||||
|
@ -3907,11 +3906,6 @@
|
|||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gibberish-aes": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gibberish-aes/-/gibberish-aes-1.0.0.tgz",
|
||||
"integrity": "sha1-9kHEWPuCLgrWHDwN6hWOC5Q+n8U="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.1.7",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
|
||||
|
|
|
@ -27,7 +27,6 @@
|
|||
"bootstrap": "^4.6.0",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"bootswatch": "^4.6.0",
|
||||
"gibberish-aes": "^1.0.0",
|
||||
"popper.js": "^1.16.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-i18n": "^8.25.0"
|
||||
|
|
Loading…
Reference in a new issue