Breaking: Replace deprecated / archived crypto library (#80)
This commit is contained in:
parent
965b37780b
commit
314afb287e
7 changed files with 93 additions and 55 deletions
|
@ -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:
|
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
|
```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=
|
U2FsdGVkX18wJtHr6YpTe8QrvMUUdaLZ+JMBNi1OvOQ=
|
||||||
|
|
||||||
# curl -X POST -H 'content-type: application/json' -i -s -d '{"secret": "U2FsdGVkX18wJtHr6YpTe8QrvMUUdaLZ+JMBNi1OvOQ="}' https://ots.fyi/api/create
|
# curl -X POST -H 'content-type: application/json' -i -s -d '{"secret": "U2FsdGVkX18wJtHr6YpTe8QrvMUUdaLZ+JMBNi1OvOQ="}' https://ots.fyi/api/create
|
||||||
|
|
|
@ -1,36 +1,38 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
: ${INSTANCE:=https://ots.fyi} # Where to reach the API of the instance (omit trailing slash)
|
||||||
|
|
||||||
deps=(curl jq)
|
deps=(curl jq)
|
||||||
for cmd in "${deps[@]}"; do
|
for cmd in "${deps[@]}"; do
|
||||||
which ${cmd} >/dev/null || {
|
which ${cmd} >/dev/null || {
|
||||||
echo "'${cmd}' util is required for this script"
|
echo "'${cmd}' util is required for this script"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
done
|
||||||
|
|
||||||
# Get secret from CLI argument
|
# Get secret from CLI argument
|
||||||
SECRET=${1:-}
|
SECRET=${1:-}
|
||||||
[[ -n $SECRET ]] || {
|
[[ -n $SECRET ]] || {
|
||||||
echo "Usage: $0 'secret to share'"
|
echo "Usage: $0 'secret to share'"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Generate a random 8 character password
|
# Generate a random 20 character password
|
||||||
pass=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 8 || true)
|
pass=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | head -c 20 || true)
|
||||||
|
|
||||||
# Encrypt the secret
|
# 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
|
# Create a secret and extract the secret ID
|
||||||
id=$(
|
id=$(
|
||||||
curl -sSf \
|
curl -sSf \
|
||||||
-X POST \
|
-X POST \
|
||||||
-H 'content-type: application/json' \
|
-H 'content-type: application/json' \
|
||||||
-d "$(jq --arg secret "${ciphertext}" -cn '{"secret": $secret}')" \
|
-d "$(jq --arg secret "${ciphertext}" -cn '{"secret": $secret}')" \
|
||||||
https://ots.fyi/api/create |
|
"${INSTANCE}/api/create" |
|
||||||
jq -r '.secret_id'
|
jq -r '.secret_id'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Display URL to user
|
# 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}"
|
||||||
|
|
14
cli_get.sh
14
cli_get.sh
|
@ -3,17 +3,17 @@ set -euo pipefail
|
||||||
|
|
||||||
deps=(curl jq)
|
deps=(curl jq)
|
||||||
for cmd in "${deps[@]}"; do
|
for cmd in "${deps[@]}"; do
|
||||||
which ${cmd} >/dev/null || {
|
which ${cmd} >/dev/null || {
|
||||||
echo "'${cmd}' util is required for this script"
|
echo "'${cmd}' util is required for this script"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
done
|
||||||
|
|
||||||
# Get URL from CLI argument
|
# Get URL from CLI argument
|
||||||
url="${1:-}"
|
url="${1:-}"
|
||||||
[[ -n $url ]] || {
|
[[ -n $url ]] || {
|
||||||
echo "Usage: $0 'URL to get the secret'"
|
echo "Usage: $0 'URL to get the secret'"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
# normalize url and extract parts
|
# normalize url and extract parts
|
||||||
url="${url/|/%7C}"
|
url="${url/|/%7C}"
|
||||||
|
@ -25,4 +25,4 @@ geturl="${host}/api/get/${id}"
|
||||||
|
|
||||||
# fetch secret and decrypt to STDOUT
|
# fetch secret and decrypt to STDOUT
|
||||||
curl -sSf "${geturl}" | jq -r ".secret" |
|
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
|
||||||
|
|
56
src/app.vue
56
src/app.vue
|
@ -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,27 +218,26 @@ 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 })
|
||||||
|
.then(resp => {
|
||||||
|
this.secretId = resp.data.secret_id
|
||||||
|
this.secret = ''
|
||||||
|
|
||||||
axios.post('api/create', { secret })
|
// Give the interface a moment to transistion and focus
|
||||||
.then(resp => {
|
window.setTimeout(() => this.$refs.secretUrl.focus(), 100)
|
||||||
this.secretId = resp.data.secret_id
|
})
|
||||||
this.secret = ''
|
.catch(err => {
|
||||||
|
switch (err.response.status) {
|
||||||
// Give the interface a moment to transistion and focus
|
case 404:
|
||||||
window.setTimeout(() => this.$refs.secretUrl.focus(), 100)
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
switch (err.response.status) {
|
|
||||||
case 404:
|
|
||||||
// Mock for interface testing
|
// Mock for interface testing
|
||||||
this.secretId = 'foobar'
|
this.secretId = 'foobar'
|
||||||
break
|
break
|
||||||
default:
|
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,20 @@ 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
|
||||||
|
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 => {
|
.catch(err => {
|
||||||
switch (err.response.status) {
|
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": "^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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue