This commit is contained in:
2024-09-28 17:37:29 +02:00
parent 8dfafa9404
commit fb4d566007
64 changed files with 32325 additions and 573 deletions
+30 -87
View File
@@ -8,25 +8,26 @@
<link rel="shortcut icon" href="{% static 'img/favicon.png' %}">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-resource@1.5.3"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@3.0.5/dist/js.cookie.min.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script src="{% static "vue/index.js" %}"></script>
<script src="{% static "sweetalert2/index" %}"></script>
<script src="{% static "vue-resource/index" %}"></script>
<script src="{% static "js.cookie.min/index.js" %}"></script>
<script src="{% static "vue-router/index.js" %}"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<link href="{% static "bootstrap.min/index.css" %}" rel="stylesheet">
<script src="{% static "bootstrap.bundle.min/index.js" %}"></script>
<link href="{% static "bootswatch.min.css/index.css" %}" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/lux/bootstrap.min.css " rel="stylesheet">
<script src="{% url 'reverse_js' %}" type="text/javascript"></script>
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
<link href="{% static "vuetify.min/index.css" %}" rel="stylesheet">
<script src="{% static "vuetify/index.js" %}"></script>
<script src="{% static "vuex/index.js" %}"></script>
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@7.4.47/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://fonts.cdnfonts.com/css/jetbrains-mono" rel="stylesheet">
<link href="{% static "jetbrains-mono/index" %}">
<style>
.font {
font-family: 'JetBrains Mono', sans-serif;
@@ -49,7 +50,7 @@
<div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<router-link href="#" to="{% url 'main:home' %}" class="nav-link active" to="{% url 'main:home' %}">
<router-link class="nav-link active" to="{% url 'main:home' %}">
{% trans "K356" %}
<span class="visually-hidden"></span>
</router-link>
@@ -58,21 +59,23 @@
<router-link class="nav-link" to="/ItemView">{% trans "Items" %}</router-link>
</li>
<li class="nav-item">
<a class="nav-link" href="#">{% trans "Properties" %}</a>
<router-link class="nav-link" to="/PropertyView">{% trans "Properties" %}</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/EncryptionTesting">{% trans "Encryption" %}</router-link>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "More" %}</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/admin/">{% trans "Admin" %}</a>
</div>
</li>
{% if request.user.is_superuser %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">{% trans "More" %}</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="#">Action</a>
<a class="dropdown-item" href="#">Another action</a>
<a class="dropdown-item" href="#">Something else here</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="/admin/">{% trans "Admin" %}</a>
</div>
</li>
{% endif %}
</ul>
<form class="d-flex">
<input class="form-control me-sm-2" type="search" placeholder="Search">
@@ -83,9 +86,9 @@
</nav>
<div id="app" class="container">
<Loading :crypto_key="key" @update_key="update_key" v-if="locked"></Loading>
<Loading @update_key="update_key" v-if="locked"></Loading>
<template v-if="!locked">
<router-view :crypto_key="key"></router-view>
<router-view></router-view>
</template>
</div>
@@ -103,56 +106,7 @@
{% endfor %}
<script type="text/javascript">
Vue.use(VueRouter);
Vue.config.delimiters = ["[[", "]]"];
{% for name, path in components.items %}
{% include path %}
Vue.component("{{ name }}", {{ name }});
{% endfor %}
const routes = [
{ path: '/', component: null },
{% for name, path in components.items %}
{
path: "/{{ name }}",
component: {{ name }},
},
{% endfor %}
];
const router = new VueRouter({routes});
const approuter = new Vue({
router,
vuetify: new Vuetify(),
el: "#main",
data: {
key: {
key: null,
uuid: "{{ user_settings.id }}",
},
locked: true,
},
methods: {
update_key: function(key) {
this.key.key = key;
this.locked = key == null;
},
lock_me: function() {
this.locked = true;
this.key.key = null;
}
}
});
router.beforeEach((to, from, next) => {
// Prevent from routing if key is not present.
next(approuter.key.key != null);
});
{% include "vue/index.js" %}
</script>
<script type="text/javascript">
@@ -164,16 +118,5 @@
refresh_csrftoken();
</script>
<script type="module">
import { useDate } from './vuetify.js'
const date = useDate()
const formatted = date.format('2010-04-13', 'fullDateWithWeekday')
console.log(formatted) // Tuesday, April 13, 2010
</script>
</body>
</html>
@@ -2,7 +2,7 @@
{% block component %}
<v-data-table
:headers="items_headers"
:headers="citems_headers"
:items="items"
:items-per-page="50"
:search="search"
@@ -34,17 +34,29 @@
<v-row>
<template v-for="field in editable_fields">
<template v-if="field.field_widget == 'v-select'">
<v-select
v-model="editedItem[field.value]"
:items="items_relations[field.value]"
:label="field.text"
item-text="name"
item-value="id"
persistent-hint>
<template slot="item" slot-scope="data">
[[ data.item.name ]] - [[ data.item.custom_identifier ]]
</template>
</v-select>
<template v-if="field.choices">
<v-select
v-model="editedItem[field.value]"
:items="field.choices"
:label="field.text"
item-text="text"
item-value="value"
persistent-hint>
</v-select>
</template>
<template v-else>
<v-select
v-model="editedItem[field.value]"
:items="items_relations[field.value]"
:label="field.text"
item-text="name"
item-value="id"
persistent-hint>
<template slot="item" slot-scope="data">
[[ data.item.name ]] - [[ data.item.custom_identifier ]]
</template>
</v-select>
</template>
</template>
<template v-else>
<component :is="field.field_widget" v-model="editedItem[field.value]" :label="field.text"></component>
@@ -91,7 +103,23 @@
<template v-slot:item.id="{ item }">
[[ item.id.slice(0, 8) ]]...
</template>
<template v-slot:item.description="{ item }">
<template v-if="item.description && item.description.length > 15">
[[ item.description.slice(0, 15) ]]...
</template>
<template v-else>
[[ item.description ]]
</template>
</template>
<template v-slot:item.last_modified_at="{ item }">
[[ formatDate(item.last_modified_at) ]]
</template>
<template v-slot:item.created_at="{ item }">
[[ formatDate(item.created_at) ]]
</template>
<template v-slot:item.actions="{ item }">
<v-icon v-if="show_url" small class="mr-2" @click="showItem(item)">mdi-eye</v-icon>
<v-icon small class="mr-2" @click="editItem(item)">mdi-pencil</v-icon>
<v-icon small @click="deleteItem(item)">mdi-delete</v-icon>
</template>
+16 -5
View File
@@ -1,8 +1,9 @@
{% block component %}
{{ name }} = {
template: "#{{ name }}",
router_path: "/{{ name }}",
delimiters: ["[[", "]]"],
props: ["crypto_key", "items", "items_headers", "items_relations", "group_by"],
props: ["crypto_key", "items", "items_headers", "items_relations", "group_by", "hidden_fields"],
data: function() {
return {
@@ -12,6 +13,8 @@
defaultItem: {},
editedItem: {},
search: null,
show_url: "{{ show_url|default:'' }}",
default_hidden_fields: [{% for field in default_hidden_fields %}"{{ field }}",{% endfor %}]
}
},
@@ -19,6 +22,10 @@
editable_fields: function() {
return this.items_headers.filter(e => e.editable)
},
citems_headers: function() {
return this.items_headers.filter(e => !this.hidden_fields.includes(e.value) && !this.default_hidden_fields.includes(e.value))
},
},
watch: {
@@ -32,6 +39,14 @@
methods: {
formatDate (date) {
return formatDate(date)
},
showItem (item) {
this.$router.replace({ name: this.show_url, params: { id: item.id }})
},
editItem (item) {
this.editedIndex = this.items.indexOf(item)
this.editedItem = Object.assign({}, item)
@@ -45,9 +60,6 @@
},
deleteItemConfirm () {
var self = this
var item = this.items[this.editedIndex]
this.$emit("deleteItem", this.editedIndex)
this.closeDelete()
@@ -74,7 +86,6 @@
} else {
console.log('createItem emit', this.editedItem)
this.$emit("createItem", this.editedItem)
}
+66
View File
@@ -1,3 +1,51 @@
async function init() {
const operations = crypto.subtle
const KEY_LEN = 256
const ENCRYPTION_ALGO = "AES-GCM"
const ENCRYPTION_USAGES = ["encrypt", "decrypt"]
const ENCRYPTION_PARAMS = { name: ENCRYPTION_ALGO, length: KEY_LEN }
const WRAPPING_ALGO = "AES-KW"
const WRAPPING_USAGES = ["wrapKey", "unwrapKey"]
const DERIVATION_ALGO = "ECDH"
const { publicKey, privateKey } = await operations.generateKey(
{
name: DERIVATION_ALGO,
namedCurve: "P-384",
},
false,
["derivekey"]
)
const encryptionKey = await operations.deriveKey(
{
name: DERIVATION_ALGO,
public: publicKey,
},
privateKey,
ENCRYPTION_PARAMS,
false,
["encrypt"]
)
const decryptionKey = await operations.deriveKey(
{ name: DERIVATION_ALGO, public: publicKey },
privateKey,
ENCRYPTION_PARAMS,
false,
['decrypt']
)
return { encryptionKey, decryptionKey }
}
function stringToArrayBuffer(str) {
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
@@ -18,6 +66,24 @@ function arrayBufferToString(str) {
}
async function aEncryptWithKey(key, data) {
// Encrypt data with key. Return a Promise
return new Promise((resolve) => {
window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: stringToArrayBuffer(key.uuid),
},
key.key,
stringToArrayBuffer(data)
).then(text => {
resolve(btoa(arrayBufferToString(text)));
});
});
}
function encryptWithKey(key, data) {
// Encrypt data with key. Return a Promise
return new Promise((resolve) => {
+117
View File
@@ -0,0 +1,117 @@
{% include "vue/plugins.js" %}
Vue.use(VueRouter)
Vue.use(Vuex)
Vue.use(EncryptionPlugin)
Vue.config.delimiters = ["[[", "]]"];
{% for name, path in components.items %}
{% include path %}
Vue.component("{{ name }}", {{ name }})
{% endfor %}
const routes = [
{ path: '/', component: null },
{% for name, path in components.items %}
{
path: {{ name }}.router_path,
name: "{{ name }}",
component: {{ name }},
},
{% endfor %}
]
const encryptionStore = new Vuex.Store({
state: {
aes_key: null,
keyPair: null,
},
mutations: {
update_aes_key (state, key) {
state.aes_key = key
},
update_keyPair (state, keyPair) {
state.keyPair = keyPair
},
}
})
const router = new VueRouter({routes})
const approuter = new Vue({
router,
vuetify: new Vuetify(),
store: encryptionStore,
el: "#main",
data: {
uuid: "{{ user_settings.id }}",
},
computed: {
locked: function() {
return this.$store.state.aes_key == null || this.$store.state.keyPair?.privateKey == null
}
},
mounted: function() {},
methods: {
async load_keys (aes_key) {
const response = await this.$http.get(Urls["users:keys"]())
const iv_private = `${this.uuid}--private`
const iv_public = `${this.uuid}--public`
if (response.data.privateKey != null) {
const keyPair = {
privateKey: await this.unwrapKey(aes_key, response.data.privateKey, iv_private, ["decrypt"]),
publicKey: await this.unwrapKey(aes_key, response.data.publicKey, iv_public, ["encrypt"]),
}
this.$store.commit('update_keyPair', keyPair)
Swal.fire({title: "Successfully loaded K356!", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
} else {
const keyPair = await this.generateKeyPair()
await this.$http.post(
Urls["users:keys"](),
{
privateKey: await this.wrapKey(this.keyPair.privateKey, aes_key, iv_private),
publicKey: await this.wrapKey(this.keyPair.publicKey, aes_key, iv_public),
}
)
this.$store.commit('update_keyPair', keyPair)
Swal.fire({title: "Successfully created K356!", icon: "success", position:"top-end", showConfirmButton: false, toast: true, timer: 1000});
}
},
update_key: function(key) {
this.$store.commit('update_aes_key', key)
this.load_keys(key)
},
lock_me: function() {
this.$store.commit('update_keyPair', null)
this.$store.commit('update_aes_key', null)
}
}
})
router.beforeEach((to, from, next) => {
// Prevent from routing if key is not present.
next(!approuter.locked)
})
+160
View File
@@ -0,0 +1,160 @@
const operations = crypto.subtle
const pbkdf2_iterations = 250000
function stringToArrayBuffer(str) {
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
function arrayBufferToString(str) {
var byteArray = new Uint8Array(str);
var byteString = '';
for (var i = 0; i < byteArray.byteLength; i++) {
byteString += String.fromCodePoint(byteArray[i]);
}
return byteString;
}
function formatDate (date) {
const d = new Date(date)
const hours = d.getHours().toString().padStart(2, '0')
const minutes = d.getMinutes().toString().padStart(2, '0')
const formattedTime = `${hours}:${minutes}`
return `${d.toLocaleDateString()} ${formattedTime}`
}
const EncryptionPlugin = {
install(Vue, options) {
Vue.prototype.deriveKeyFromPassphrase = async (passphrase, salt) => {
const encoder = new TextEncoder();
const keyFromPassword = await operations.importKey(
"raw",
encoder.encode(passphrase),
"PBKDF2",
false,
["deriveKey"]
)
return await operations.deriveKey(
{
name: "PBKDF2",
salt: stringToArrayBuffer(salt),
iterations: pbkdf2_iterations,
hash: "SHA-256",
},
keyFromPassword,
{
name: "AES-GCM",
length: 256
},
true,
["wrapKey", "unwrapKey"]
)
},
Vue.prototype.generateKeyPair = async () => {
return await operations.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true,
["encrypt", "decrypt"]
);
},
Vue.prototype.wrapKey = async (key, wrappingKey, iv) => {
return btoa(arrayBufferToString(await operations.wrapKey(
"jwk",
key,
wrappingKey,
{name: "AES-GCM", iv: stringToArrayBuffer(iv)}
)))
},
Vue.prototype.unwrapKey = async (unwrappingKey, armored_jwk_data, iv, args) => {
return await operations.unwrapKey(
"jwk",
stringToArrayBuffer(atob(armored_jwk_data)),
unwrappingKey,
{name: "AES-GCM", iv: stringToArrayBuffer(iv)},
{
name: "RSA-OAEP",
hash: "SHA-256",
},
true,
args,
)
},
Vue.prototype.encrypt = async function(data) {
return btoa(arrayBufferToString(await operations.encrypt(
{ name: "RSA-OAEP" },
this.$store.state.keyPair.publicKey,
stringToArrayBuffer(data),
)))
},
Vue.prototype.decrypt = async function(armored_data) {
return arrayBufferToString(await operations.decrypt(
{ name: "RSA-OAEP" },
this.$store.state.keyPair.privateKey,
stringToArrayBuffer(atob(armored_data))
))
},
Vue.prototype.decryptObject = async function(efields, obj) {
// Decrypt all fields and return a new object
var newobj = {}
await Promise.all(Object.keys(obj).map(async field => {
if (efields.includes(field) && obj[field] != null && obj[field] != "") {
// TODO: Catch error
newobj[field] = await this.decrypt(obj[field])
} else {
newobj[field] = obj[field]
}
}))
return newobj
},
Vue.prototype.encryptObject = async function(efields, obj) {
// Encrypt all fields and return a new object
var newobj = {}
await Promise.all(Object.keys(obj).map(async field => {
if (efields.includes(field) && obj[field] != null) {
// TODO: Catch error
newobj[field] = await this.encrypt(obj[field])
} else {
newobj[field] = obj[field]
}
}))
return newobj
}
}
}