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>
import axios from 'axios'
import AES from 'gibberish-aes/src/gibberish-aes'
import crypto from './crypto.js'
const passwordCharset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
const passwordLength = 20
@ -218,27 +218,26 @@ 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)
crypto.enc(this.secret, this.securePassword)
.then(secret => axios.post('api/create', { secret })
.then(resp => {
this.secretId = resp.data.secret_id
this.secret = ''
axios.post('api/create', { secret })
.then(resp => {
this.secretId = resp.data.secret_id
this.secret = ''
// Give the interface a moment to transistion and focus
window.setTimeout(() => this.$refs.secretUrl.focus(), 100)
})
.catch(err => {
switch (err.response.status) {
case 404:
// Give the interface a moment to transistion and focus
window.setTimeout(() => this.$refs.secretUrl.focus(), 100)
})
.catch(err => {
switch (err.response.status) {
case 404:
// Mock for interface testing
this.secretId = 'foobar'
break
default:
this.error = this.$t('alert-something-went-wrong')
this.showError = true
}
})
this.secretId = 'foobar'
break
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
}
this.secret = secret
crypto.dec(secret, this.securePassword)
.then(secret => {
this.secret = secret
})
})
.catch(err => {
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-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",

View File

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