Skip to content

Commit

Permalink
feat: basic web crypto implementation (P.P. research-project) + node …
Browse files Browse the repository at this point in the history
…key-pair generation (#912)

* feat: basic web crypto implementation (P.P. research-project) + node key-pair generation

* fix: import in node crypto component
  • Loading branch information
tada5hi authored Nov 18, 2024
1 parent e11bc5f commit 8cdb9d8
Show file tree
Hide file tree
Showing 18 changed files with 681 additions and 34 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/client-ui/pages/admin/nodes/[id].vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export default defineComponent({

const tabs = [
{ name: 'Overview', icon: 'fas fa-bars', urlSuffix: '' },
{ name: 'Crypto', icon: 'fas fa-shield-alt', urlSuffix: 'crypto' },
{ name: 'Robot', icon: 'fas fa-robot', urlSuffix: 'robot' },
{ name: 'Registry', icon: 'fab fa-docker', urlSuffix: 'registry' },
];
Expand Down
72 changes: 72 additions & 0 deletions packages/client-ui/pages/admin/nodes/[id]/crypto.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!--
- Copyright (c) 2022-2024.
- Author Peter Placzek (tada5hi)
- For the full copyright and license information,
- view the LICENSE file that was distributed with this source code.
-->
<script lang="ts">
import type { Node } from '@privateaim/core-kit';
import type { PropType } from 'vue';
import { FNodeCrypto } from '@privateaim/client-vue';
import { defineNuxtComponent } from '#app';
import { useToast } from '../../../../composables/toast';

export default defineNuxtComponent({
components: { FNodeCrypto },
props: {
entity: {
type: Object as PropType<Node>,
required: true,
},
},
emits: ['failed', 'updated'],
methods: {
handleFailed(e: Error) {
this.$emit('failed', e);
},
},
setup(props, { emit }) {
const toast = useToast();

const handleKeyCopied = () => {
toast.show({
variant: 'dark',
body: 'The key was successfully copied to the clipboard.',
});
};

const handleKeyPairGenerated = () => {
toast.show({
variant: 'dark',
body: 'A key pair was successfully generated.',
});
};

const handleFailed = (e: Error) => {
emit('failed', e);
};

const handleUpdated = () => {
emit('updated', props.entity);
};

return {
handleKeyCopied,
handleKeyPairGenerated,
handleFailed,
handleUpdated,
};
},
});
</script>
<template>
<FNodeCrypto
v-if="entity"
:entity="entity"
:realm-id="entity.realm_id"
@keyCopied="handleKeyCopied"
@keyPairGenerated="handleKeyPairGenerated"
@failed="handleFailed"
@updated="handleUpdated"
/>
</template>
1 change: 1 addition & 0 deletions packages/client-vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@vuecs/timeago": "^1.1.0",
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"@vueuse/core": "^11.2.0",
"bootstrap-vue-next": "^0.24.23",
"cross-env": "^7.0.3",
"pinia": "^2.2.2",
Expand Down
220 changes: 220 additions & 0 deletions packages/client-vue/src/components/node/FNodeCrypto.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
<!--
- Copyright (c) 2024.
- Author Peter Placzek (tada5hi)
- For the full copyright and license information,
- view the LICENSE file that was distributed with this source code.
-->
<script lang="ts">
import type { Node } from '@privateaim/core-kit';
import {
CryptoAsymmetricAlgorithm,
exportAsymmetricPrivateKey,
exportAsymmetricPublicKey,
hexToUTF8,
isHex,
} from '@privateaim/kit';
import { useClipboard } from '@vueuse/core';
import {
type PropType, defineComponent, ref,
} from 'vue';
import { injectCoreHTTPClient } from '../../core';

export default defineComponent({
props: {
entity: {
type: Object as PropType<Node>,
required: true,
},
},
emits: ['updated', 'failed', 'keyCopied', 'keyPairGenerated'],
setup(props, { emit }) {
const httpClient = injectCoreHTTPClient();
const algorithm = new CryptoAsymmetricAlgorithm({
name: 'RSA-OAEP',
modulusLength: 2048,
hash: 'SHA-256',
publicExponent: new Uint8Array([1, 0, 1]),
});

const privateKey = ref<string | null>(null);
const publicKey = ref<string | null>(null);

const clipboard = useClipboard();

const init = () => {
if (props.entity.public_key) {
publicKey.value = isHex(props.entity.public_key) ?
hexToUTF8(props.entity.public_key) :
props.entity.public_key;
}
};

init();

const copy = (type: string) => {
if (type === 'privateKey') {
if (!privateKey.value) {
return;
}

clipboard.copy(privateKey.value);

emit('keyCopied');
return;
}

if (!publicKey.value) {
return;
}

clipboard.copy(publicKey.value);
emit('keyCopied');
};

const busy = ref(false);

const save = async () => {
if (busy.value) return;
busy.value = true;

try {
const response = await httpClient.node.update(props.entity.id, {
public_key: publicKey.value,
});

emit('updated', response);
} catch (e) {
emit('failed', e);
} finally {
busy.value = false;
}
};

const generate = async () => {
if (busy.value) return;

busy.value = true;

try {
const keyPair = await algorithm.generateKeyPair();

publicKey.value = await exportAsymmetricPublicKey(keyPair.publicKey);
privateKey.value = await exportAsymmetricPrivateKey(keyPair.privateKey);

emit('keyPairGenerated');
} finally {
busy.value = false;
}
};

return {
busy,

copy,
generate,
save,

publicKey,
privateKey,
};
},
});
</script>
<template>
<div>
<h6>KeyPair</h6>

<p>
The public key of the key pair is used to encrypt data that is transmitted between
different nodes through the storage service.
</p>

<div class="d-flex flex-column gap-1">
<div class="row">
<div class="col-4">
<VCFormGroup :label-class="'w-100 mb-1'">
<template #label>
<div class="d-flex flex-row">
<div>
PublicKey
</div>
<div class="ms-auto">
<button
v-show="!!publicKey"
type="button"
class="btn btn-xs btn-dark"
@click.prevent="copy('publicKey')"
>
<i class="fa fa-copy" /> Copy
</button>
</div>
</div>
</template>
<template #default>
<VCFormTextarea
v-model="publicKey"
rows="8"
/>
</template>
</VCFormGroup>
</div>
<div class="col-8">
<VCFormGroup :label-class="'w-100 mb-1'">
<template #label>
<div class="d-flex flex-row">
<div>
PrivateKey
</div>
<div class="ms-auto">
<button
v-show="!!privateKey"
type="button"
class="btn btn-xs btn-dark"
@click.prevent="copy('privateKey')"
>
<i class="fa fa-copy" /> Copy
</button>
</div>
</div>
</template>
<template #default>
<VCFormTextarea
v-model="privateKey"
:disabled="true"
rows="8"
/>
</template>
</VCFormGroup>

<template v-if="privateKey">
<div class="alert alert-sm alert-warning">
Please copy the key to a safe location, as it is not stored remotely.
</div>
</template>
</div>
</div>
<div class="d-flex flex-row gap-1">
<div>
<button
:disabled="busy"
type="button"
class="btn btn-primary btn-xs"
@click.prevent="save"
>
<i class="fa fa-save" /> Save
</button>
</div>
<div>
<button
:disabled="busy"
type="button"
class="btn btn-dark btn-xs"
@click.prevent="generate"
>
<i class="fas fa-sync-alt" /> Generate
</button>
</div>
</div>
</div>
</div>
</template>
33 changes: 2 additions & 31 deletions packages/client-vue/src/components/node/FNodeForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import {
DomainType,
NodeType,
} from '@privateaim/core-kit';
import { alphaNumHyphenUnderscoreRegex, hexToUTF8, isHex } from '@privateaim/kit';
import { alphaNumHyphenUnderscoreRegex } from '@privateaim/kit';
import {
buildFormGroup,
buildFormInput, buildFormInputCheckbox, buildFormSelect, buildFormTextarea,
buildFormInput, buildFormInputCheckbox, buildFormSelect,
} from '@vuecs/form-controls';
import type { ListBodySlotProps, ListItemSlotProps } from '@vuecs/list-controls';
import useVuelidate from '@vuelidate/core';
Expand Down Expand Up @@ -57,7 +57,6 @@ export default defineComponent({
const busy = ref(false);
const form = reactive({
name: '',
public_key: '',
external_name: '',
realm_id: '',
registry_id: '',
Expand All @@ -71,10 +70,6 @@ export default defineComponent({
minLength: minLength(3),
maxLength: maxLength(128),
},
public_key: {
minLength: minLength(10),
maxLength: maxLength(4096),
},
hidden: {

},
Expand Down Expand Up @@ -108,12 +103,6 @@ export default defineComponent({
const initForm = () => {
initFormAttributesFromSource(form, manager.data.value);

if (form.public_key) {
form.public_key = isHex(form.public_key) ?
hexToUTF8(form.public_key) :
form.public_key;
}

if (!form.realm_id && props.realmId) {
form.realm_id = props.realmId;
}
Expand Down Expand Up @@ -224,22 +213,6 @@ export default defineComponent({
}),
});

const publicKey = buildFormGroup({
validationMessages: translationsValidation.public_key.value,
validationSeverity: getSeverity($v.value.public_key),
label: true,
labelContent: 'PublicKey',
content: buildFormTextarea({
value: form.public_key,
onChange(input) {
form.public_key = input;
},
props: {
rows: 6,
},
}),
});

const hidden = buildFormGroup({
validationMessages: translationsValidation.hidden.value,
validationSeverity: getSeverity($v.value.hidden),
Expand Down Expand Up @@ -311,8 +284,6 @@ export default defineComponent({
h('hr'),
hidden,
h('hr'),
publicKey,
h('hr'),
submitNode,
]),
]),
Expand Down
Loading

0 comments on commit 8cdb9d8

Please sign in to comment.