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:
Knut Ahlers 2023-03-17 20:31:06 +01:00
parent 895705bacb
commit fb05e214f1
Signed by: luzifer
GPG Key ID: D91C3E91E4CAD6F5
4 changed files with 63 additions and 31 deletions

View File

@ -166,7 +166,7 @@
<script> <script>
import axios from 'axios' import axios from 'axios'
import AES from 'gibberish-aes/src/gibberish-aes' import crypto from './crypto.js'
const passwordCharset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' const passwordCharset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
const passwordLength = 20 const passwordLength = 20
@ -218,9 +218,8 @@ export default {
this.securePassword = [...window.crypto.getRandomValues(new Uint8Array(passwordLength))] this.securePassword = [...window.crypto.getRandomValues(new Uint8Array(passwordLength))]
.map(n => passwordCharset[n % passwordCharset.length]) .map(n => passwordCharset[n % passwordCharset.length])
.join('') .join('')
const secret = AES.enc(this.secret, this.securePassword) crypto.enc(this.secret, this.securePassword)
.then(secret => axios.post('api/create', { secret })
axios.post('api/create', { secret })
.then(resp => { .then(resp => {
this.secretId = resp.data.secret_id this.secretId = resp.data.secret_id
this.secret = '' this.secret = ''
@ -238,7 +237,7 @@ export default {
this.error = this.$t('alert-something-went-wrong') this.error = this.$t('alert-something-went-wrong')
this.showError = true this.showError = true
} }
}) }))
return false return false
}, },
@ -267,11 +266,16 @@ export default {
requestSecret() { requestSecret() {
axios.get(`api/get/${this.secretId}`) axios.get(`api/get/${this.secretId}`)
.then(resp => { .then(resp => {
let secret = resp.data.secret const secret = resp.data.secret
if (this.securePassword) { if (!this.securePassword) {
secret = AES.dec(secret, this.securePassword)
}
this.secret = secret this.secret = secret
return
}
crypto.dec(secret, this.securePassword)
.then(secret => {
this.secret = secret
})
}) })
.catch(err => { .catch(err => {
switch (err.response.status) { switch (err.response.status) {

35
src/crypto.js Normal file
View 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
View File

@ -10,7 +10,6 @@
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"bootstrap-vue": "^2.21.2", "bootstrap-vue": "^2.21.2",
"bootswatch": "^4.6.0", "bootswatch": "^4.6.0",
"gibberish-aes": "^1.0.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-i18n": "^8.25.0" "vue-i18n": "^8.25.0"
@ -3907,11 +3906,6 @@
"assert-plus": "^1.0.0" "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": { "node_modules/glob": {
"version": "7.1.7", "version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",

View File

@ -27,7 +27,6 @@
"bootstrap": "^4.6.0", "bootstrap": "^4.6.0",
"bootstrap-vue": "^2.21.2", "bootstrap-vue": "^2.21.2",
"bootswatch": "^4.6.0", "bootswatch": "^4.6.0",
"gibberish-aes": "^1.0.0",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-i18n": "^8.25.0" "vue-i18n": "^8.25.0"