Breaking: Replace deprecated / archived crypto library (#80)

This commit is contained in:
Knut Ahlers 2023-04-14 13:06:14 +02:00 committed by GitHub
parent 965b37780b
commit 314afb287e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 93 additions and 55 deletions

View file

@ -36,7 +36,7 @@ As `ots` is designed to never let the server know the secret you are sharing you
This is slightly more complex as you first need to encrypt your secret before sending it to the API but in this case you can be sure the server will in no case be able to access the secret. Especially if you are using ots.fyi (my public hosted instance) you should not trust me with your secret but use an encrypted secret:
```console
# echo "my password" | openssl aes-256-cbc -base64 -pass pass:mypass -md md5
# echo "my password" | openssl aes-256-cbc -base64 -pass pass:mypass -iter 300000 -md sha512
U2FsdGVkX18wJtHr6YpTe8QrvMUUdaLZ+JMBNi1OvOQ=
# curl -X POST -H 'content-type: application/json' -i -s -d '{"secret": "U2FsdGVkX18wJtHr6YpTe8QrvMUUdaLZ+JMBNi1OvOQ="}' https://ots.fyi/api/create

View file

@ -1,36 +1,38 @@
#!/bin/bash
set -euo pipefail
: ${INSTANCE:=https://ots.fyi} # Where to reach the API of the instance (omit trailing slash)
deps=(curl jq)
for cmd in "${deps[@]}"; do
which ${cmd} >/dev/null || {
echo "'${cmd}' util is required for this script"
exit 1
}
which ${cmd} >/dev/null || {
echo "'${cmd}' util is required for this script"
exit 1
}
done
# Get secret from CLI argument
SECRET=${1:-}
[[ -n $SECRET ]] || {
echo "Usage: $0 'secret to share'"
exit 1
echo "Usage: $0 'secret to share'"
exit 1
}
# Generate a random 8 character password
pass=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 8 || true)
# Generate a random 20 character password
pass=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 20 || true)
# Encrypt the secret
ciphertext=$(echo "${SECRET}" | openssl aes-256-cbc -base64 -pass "pass:${pass}" -md md5 2>/dev/null)
ciphertext=$(echo "${SECRET}" | openssl aes-256-cbc -base64 -pass "pass:${pass}" -iter 300000 -md sha512 2>/dev/null)
# Create a secret and extract the secret ID
id=$(
curl -sSf \
-X POST \
-H 'content-type: application/json' \
-d "$(jq --arg secret "${ciphertext}" -cn '{"secret": $secret}')" \
https://ots.fyi/api/create |
jq -r '.secret_id'
curl -sSf \
-X POST \
-H 'content-type: application/json' \
-d "$(jq --arg secret "${ciphertext}" -cn '{"secret": $secret}')" \
"${INSTANCE}/api/create" |
jq -r '.secret_id'
)
# Display URL to user
echo -e "Secret is now available at:\nhttps://ots.fyi/#${id}%7C${pass}"
echo -e "Secret is now available at:\n${INSTANCE}/#${id}%7C${pass}"

View file

@ -3,17 +3,17 @@ set -euo pipefail
deps=(curl jq)
for cmd in "${deps[@]}"; do
which ${cmd} >/dev/null || {
echo "'${cmd}' util is required for this script"
exit 1
}
which ${cmd} >/dev/null || {
echo "'${cmd}' util is required for this script"
exit 1
}
done
# Get URL from CLI argument
url="${1:-}"
[[ -n $url ]] || {
echo "Usage: $0 'URL to get the secret'"
exit 1
echo "Usage: $0 'URL to get the secret'"
exit 1
}
# normalize url and extract parts
url="${url/|/%7C}"
@ -25,4 +25,4 @@ geturl="${host}/api/get/${id}"
# fetch secret and decrypt to STDOUT
curl -sSf "${geturl}" | jq -r ".secret" |
openssl aes-256-cbc -base64 -pass "pass:${pass}" -md md5 -d 2>/dev/null
openssl aes-256-cbc -base64 -pass "pass:${pass}" -iter 300000 -md sha512 -d 2>/dev/null

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,20 @@ 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(() => {
this.error = this.$t('alert-something-went-wrong')
this.showError = true
})
})
.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"