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 965b37780b
commit b967a3ddc3
No known key found for this signature in database
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"