From 8cdb9d8ff140400426ccbd61f254a47fa0e3fab1 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 18 Nov 2024 17:11:06 +0100 Subject: [PATCH] feat: basic web crypto implementation (P.P. research-project) + node key-pair generation (#912) * feat: basic web crypto implementation (P.P. research-project) + node key-pair generation * fix: import in node crypto component --- package-lock.json | 1 + packages/client-ui/pages/admin/nodes/[id].vue | 1 + .../pages/admin/nodes/[id]/crypto.vue | 72 ++++++ packages/client-vue/package.json | 1 + .../src/components/node/FNodeCrypto.vue | 220 ++++++++++++++++++ .../src/components/node/FNodeForm.ts | 33 +-- .../client-vue/src/components/node/index.ts | 1 + .../registry-project/FRegistryProject.ts | 6 +- .../kit/src/crypto/asymmetric/constants.ts | 12 + packages/kit/src/crypto/asymmetric/helpers.ts | 58 +++++ packages/kit/src/crypto/asymmetric/index.ts | 11 + packages/kit/src/crypto/asymmetric/module.ts | 190 +++++++++++++++ packages/kit/src/crypto/asymmetric/types.ts | 9 + packages/kit/src/crypto/index.ts | 9 + packages/kit/src/crypto/symmetric/index.ts | 9 + packages/kit/src/crypto/symmetric/module.ts | 71 ++++++ packages/kit/src/crypto/symmetric/types.ts | 10 + packages/kit/src/index.ts | 1 + 18 files changed, 681 insertions(+), 34 deletions(-) create mode 100644 packages/client-ui/pages/admin/nodes/[id]/crypto.vue create mode 100644 packages/client-vue/src/components/node/FNodeCrypto.vue create mode 100644 packages/kit/src/crypto/asymmetric/constants.ts create mode 100644 packages/kit/src/crypto/asymmetric/helpers.ts create mode 100644 packages/kit/src/crypto/asymmetric/index.ts create mode 100644 packages/kit/src/crypto/asymmetric/module.ts create mode 100644 packages/kit/src/crypto/asymmetric/types.ts create mode 100644 packages/kit/src/crypto/index.ts create mode 100644 packages/kit/src/crypto/symmetric/index.ts create mode 100644 packages/kit/src/crypto/symmetric/module.ts create mode 100644 packages/kit/src/crypto/symmetric/types.ts diff --git a/package-lock.json b/package-lock.json index 718cd5828..b1fdd1e31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28512,6 +28512,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", diff --git a/packages/client-ui/pages/admin/nodes/[id].vue b/packages/client-ui/pages/admin/nodes/[id].vue index 1361791a3..4d14a872f 100644 --- a/packages/client-ui/pages/admin/nodes/[id].vue +++ b/packages/client-ui/pages/admin/nodes/[id].vue @@ -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' }, ]; diff --git a/packages/client-ui/pages/admin/nodes/[id]/crypto.vue b/packages/client-ui/pages/admin/nodes/[id]/crypto.vue new file mode 100644 index 000000000..ac15b23c4 --- /dev/null +++ b/packages/client-ui/pages/admin/nodes/[id]/crypto.vue @@ -0,0 +1,72 @@ + + + diff --git a/packages/client-vue/package.json b/packages/client-vue/package.json index 02460ec64..d8a9fdb36 100644 --- a/packages/client-vue/package.json +++ b/packages/client-vue/package.json @@ -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", diff --git a/packages/client-vue/src/components/node/FNodeCrypto.vue b/packages/client-vue/src/components/node/FNodeCrypto.vue new file mode 100644 index 000000000..a147de80c --- /dev/null +++ b/packages/client-vue/src/components/node/FNodeCrypto.vue @@ -0,0 +1,220 @@ + + + diff --git a/packages/client-vue/src/components/node/FNodeForm.ts b/packages/client-vue/src/components/node/FNodeForm.ts index ed8b10e44..7af0c496f 100644 --- a/packages/client-vue/src/components/node/FNodeForm.ts +++ b/packages/client-vue/src/components/node/FNodeForm.ts @@ -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'; @@ -57,7 +57,6 @@ export default defineComponent({ const busy = ref(false); const form = reactive({ name: '', - public_key: '', external_name: '', realm_id: '', registry_id: '', @@ -71,10 +70,6 @@ export default defineComponent({ minLength: minLength(3), maxLength: maxLength(128), }, - public_key: { - minLength: minLength(10), - maxLength: maxLength(4096), - }, hidden: { }, @@ -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; } @@ -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), @@ -311,8 +284,6 @@ export default defineComponent({ h('hr'), hidden, h('hr'), - publicKey, - h('hr'), submitNode, ]), ]), diff --git a/packages/client-vue/src/components/node/index.ts b/packages/client-vue/src/components/node/index.ts index 8755e28f7..907805d83 100644 --- a/packages/client-vue/src/components/node/index.ts +++ b/packages/client-vue/src/components/node/index.ts @@ -6,6 +6,7 @@ */ export { default as FNodeForm } from './FNodeForm'; +export { default as FNodeCrypto } from './FNodeCrypto.vue'; export { default as FNodes } from './FNodes'; export { default as FNodeRegistryProject } from './FNodeRegistryProject'; export { default as FNodeRobotEntity } from './FNodeRobot'; diff --git a/packages/client-vue/src/components/registry-project/FRegistryProject.ts b/packages/client-vue/src/components/registry-project/FRegistryProject.ts index 55665ac15..c2674a92b 100644 --- a/packages/client-vue/src/components/registry-project/FRegistryProject.ts +++ b/packages/client-vue/src/components/registry-project/FRegistryProject.ts @@ -111,7 +111,7 @@ export default defineComponent({ class: 'mb-2 d-flex flex-column', }, [ h('div', { class: 'form-group' }, [ - h('label', { class: 'pe-1' }, 'Namespace'), + h('label', { class: 'pe-1' }, 'Project'), h('input', { class: 'form-control', type: 'text', @@ -122,7 +122,7 @@ export default defineComponent({ h('div', [ h('div', { class: 'form-group' }, [ - h('label', { class: 'pe-1' }, 'ID'), + h('label', { class: 'pe-1' }, 'Account ID'), h('input', { class: 'form-control', type: 'text', @@ -133,7 +133,7 @@ export default defineComponent({ ]), buildFormGroup({ label: true, - labelContent: 'Secret', + labelContent: 'Account Secret', validationMessages: translationsValidation.secret.value, validationSeverity: getSeverity(vuelidate.value.secret), content: buildFormInput({ diff --git a/packages/kit/src/crypto/asymmetric/constants.ts b/packages/kit/src/crypto/asymmetric/constants.ts new file mode 100644 index 000000000..0ba31a023 --- /dev/null +++ b/packages/kit/src/crypto/asymmetric/constants.ts @@ -0,0 +1,12 @@ +/* + * 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. + */ + +export enum AsymmetricCryptoAlgorithmName { + RSA_OAEP = 'RSA-OAEP', + + ECDH = 'ECDH', +} diff --git a/packages/kit/src/crypto/asymmetric/helpers.ts b/packages/kit/src/crypto/asymmetric/helpers.ts new file mode 100644 index 000000000..e9773dcdf --- /dev/null +++ b/packages/kit/src/crypto/asymmetric/helpers.ts @@ -0,0 +1,58 @@ +/* + * 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. + */ + +import type { AsymmetricAlgorithmImportParams } from './types'; + +function arrayBufferToBase64(arrayBuffer: ArrayBuffer): string { + return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))); +} + +export async function exportAsymmetricPublicKey(key: CryptoKey): Promise { + const exported = await crypto.subtle.exportKey( + 'spki', + key, + ); + + return `-----BEGIN PUBLIC KEY-----\n${arrayBufferToBase64(exported)}\n-----END PUBLIC KEY-----`; +} + +export async function exportAsymmetricPrivateKey(key: CryptoKey): Promise { + const exported = await crypto.subtle.exportKey( + 'pkcs8', + key, + ); + + return `-----BEGIN PRIVATE KEY-----\n${arrayBufferToBase64(exported)}\n-----END PRIVATE KEY-----`; +} + +export async function importAsymmetricPublicKey( + pem: string, + params: AsymmetricAlgorithmImportParams, +): Promise { + const pemHeader = '-----BEGIN PUBLIC KEY-----'; + const pemFooter = '-----END PUBLIC KEY-----'; + const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length); + const buffer = Buffer.from(pemContents, 'base64'); + + if (params.name === 'ECDH') { + return crypto.subtle.importKey( + 'spki', + buffer, + params, + true, + ['deriveKey'], + ); + } + + return crypto.subtle.importKey( + 'spki', + buffer, + params, + true, + ['encrypt'], + ); +} diff --git a/packages/kit/src/crypto/asymmetric/index.ts b/packages/kit/src/crypto/asymmetric/index.ts new file mode 100644 index 000000000..a0fb1d70d --- /dev/null +++ b/packages/kit/src/crypto/asymmetric/index.ts @@ -0,0 +1,11 @@ +/* + * 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. + */ + +export * from './constants'; +export * from './helpers'; +export * from './module'; +export * from './types'; diff --git a/packages/kit/src/crypto/asymmetric/module.ts b/packages/kit/src/crypto/asymmetric/module.ts new file mode 100644 index 000000000..c190a8789 --- /dev/null +++ b/packages/kit/src/crypto/asymmetric/module.ts @@ -0,0 +1,190 @@ +/* + * 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. + */ + +import { AsymmetricCryptoAlgorithmName } from './constants'; +import type { AsymmetricAlgorithmImportParams, AsymmetricAlgorithmParams } from './types'; +import { exportAsymmetricPrivateKey, exportAsymmetricPublicKey } from './helpers'; + +export class CryptoAsymmetricAlgorithm { + public readonly algorithm: AsymmetricAlgorithmParams; + + protected keyPair : CryptoKeyPair | undefined; + + constructor(algorithm: AsymmetricAlgorithmParams) { + if (algorithm.name === AsymmetricCryptoAlgorithmName.RSA_OAEP) { + algorithm = { + ...algorithm, + publicExponent: new Uint8Array([1, 0, 1]), + }; + } + + this.algorithm = algorithm; + } + + buildImportParams() : AsymmetricAlgorithmImportParams { + if (this.algorithm.name === AsymmetricCryptoAlgorithmName.RSA_OAEP) { + return { + name: 'RSA-OAEP', + hash: 'SHA-256', + }; + } + + if (this.algorithm.name === AsymmetricCryptoAlgorithmName.ECDH) { + return { + name: 'ECDH', + namedCurve: (this.algorithm as EcKeyGenParams).namedCurve, + hash: 'SHA-256', + }; + } + + throw new Error('Import params could not be created.'); + } + + async generateKeyPair() : Promise { + if (this.algorithm.name === AsymmetricCryptoAlgorithmName.RSA_OAEP) { + this.keyPair = await crypto.subtle.generateKey( + this.algorithm, + true, + ['encrypt', 'decrypt'], + ); + + return this.keyPair; + } + + if (this.algorithm.name === AsymmetricCryptoAlgorithmName.ECDH) { + this.keyPair = await crypto.subtle.generateKey( + this.algorithm, + true, + ['deriveKey'], + ); + + return this.keyPair; + } + + throw new Error('The algorithm is not supported for key generation.'); + } + + async useKeyPair(): Promise { + if (typeof this.keyPair !== 'undefined') { + return this.keyPair; + } + + return this.generateKeyPair(); + } + + async exportPublicKey(): Promise { + const keyPair = await this.useKeyPair(); + + return exportAsymmetricPublicKey(keyPair.publicKey); + } + + async exportPrivateKey(): Promise { + const keyPair = await this.useKeyPair(); + + return exportAsymmetricPrivateKey(keyPair.privateKey); + } + + async encrypt(data: Buffer, remoteKey?: CryptoKey) { + const keyPair = await this.useKeyPair(); + + if (this.algorithm.name === AsymmetricCryptoAlgorithmName.RSA_OAEP) { + return crypto.subtle.encrypt( + { + name: 'RSA-OAEP', + }, + remoteKey || keyPair.publicKey, + data, + ); + } + + if (this.algorithm.name === AsymmetricCryptoAlgorithmName.ECDH) { + if (typeof remoteKey === 'undefined') { + throw new Error('Remote public key is required.'); + } + + const array = new Uint8Array(16); + const iv = crypto.getRandomValues(array); + const key = await crypto.subtle.deriveKey( + { + name: 'ECDH', + public: remoteKey, + }, + keyPair.privateKey, + { + name: 'AES-GCM', + length: 256, + }, + true, + ['encrypt'], + ); + + const arrayBuffer = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + length: 256, + iv, + }, + key, + data, + ); + + const buffer = Buffer.from(arrayBuffer); + + return Buffer.concat([iv, buffer]); + } + + throw new Error('Unsupported algorithm for encryption.'); + } + + async decrypt(data: Buffer, remoteKey?: CryptoKey) { + const keyPair = await this.useKeyPair(); + + if (this.algorithm.name === AsymmetricCryptoAlgorithmName.RSA_OAEP) { + return crypto.subtle.decrypt( + { + name: 'RSA-OAEP', + }, + keyPair.privateKey, + data, + ); + } + + if (this.algorithm.name === AsymmetricCryptoAlgorithmName.ECDH) { + if (typeof remoteKey === 'undefined') { + throw new Error('Remote public key is required.'); + } + + const iv = data.slice(0, 16); + + const key = await crypto.subtle.deriveKey( + { + name: 'ECDH', + public: remoteKey, + }, + keyPair.privateKey, + { + name: 'AES-GCM', + length: 256, + }, + true, + ['decrypt'], + ); + + return crypto.subtle.decrypt( + { + name: 'AES-GCM', + length: 256, + iv, + }, + key, + data.slice(16), + ); + } + + throw new Error('Unsupported algorithm for decryption.'); + } +} diff --git a/packages/kit/src/crypto/asymmetric/types.ts b/packages/kit/src/crypto/asymmetric/types.ts new file mode 100644 index 000000000..9c74c14e2 --- /dev/null +++ b/packages/kit/src/crypto/asymmetric/types.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export type AsymmetricAlgorithmParams = RsaHashedKeyGenParams | EcKeyGenParams; +export type AsymmetricAlgorithmImportParams = RsaHashedImportParams | EcKeyImportParams; diff --git a/packages/kit/src/crypto/index.ts b/packages/kit/src/crypto/index.ts new file mode 100644 index 000000000..5abab76d0 --- /dev/null +++ b/packages/kit/src/crypto/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './asymmetric'; +export * from './symmetric'; diff --git a/packages/kit/src/crypto/symmetric/index.ts b/packages/kit/src/crypto/symmetric/index.ts new file mode 100644 index 000000000..7054e9d66 --- /dev/null +++ b/packages/kit/src/crypto/symmetric/index.ts @@ -0,0 +1,9 @@ +/* + * 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. + */ + +export * from './module'; +export * from './types'; diff --git a/packages/kit/src/crypto/symmetric/module.ts b/packages/kit/src/crypto/symmetric/module.ts new file mode 100644 index 000000000..2f15a61b0 --- /dev/null +++ b/packages/kit/src/crypto/symmetric/module.ts @@ -0,0 +1,71 @@ +/* + * 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. + */ + +import type { SymmetricAlgorithmParams } from './types'; + +export class CryptoSymmetricAlgorithm { + protected algorithm : SymmetricAlgorithmParams; + + constructor(algorithm: SymmetricAlgorithmParams) { + this.algorithm = algorithm; + } + + async generateKey() : Promise { + return crypto.subtle.generateKey( + { + name: this.algorithm.name, + length: 256, + }, + true, + ['encrypt', 'decrypt'], + ); + } + + async importKey(buffer: Buffer | ArrayBuffer) : Promise { + return crypto.subtle.importKey( + 'raw', + buffer, + { + name: this.algorithm.name, + length: 256, + }, + true, + ['encrypt', 'decrypt'], + ); + } + + async encrypt(key: CryptoKey, iv: Buffer, data: Buffer) : Promise { + const arrayBuffer = await crypto.subtle.encrypt( + { + name: this.algorithm.name, + length: 256, + iv, + }, + key, + data, + ); + + const buffer = Buffer.from(arrayBuffer); + + return Buffer.concat([iv, buffer]); + } + + async decrypt(key: CryptoKey, data: Buffer) : Promise { + const iv = data.slice(0, 16); + const arrayBuffer = await crypto.subtle.decrypt( + { + name: this.algorithm.name, + length: 256, + iv, + }, + key, + data.slice(16), + ); + + return Buffer.from(arrayBuffer); + } +} diff --git a/packages/kit/src/crypto/symmetric/types.ts b/packages/kit/src/crypto/symmetric/types.ts new file mode 100644 index 000000000..0c742a9c8 --- /dev/null +++ b/packages/kit/src/crypto/symmetric/types.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +import type { webcrypto } from 'crypto'; + +export type SymmetricAlgorithmParams = webcrypto.AesKeyGenParams; diff --git a/packages/kit/src/index.ts b/packages/kit/src/index.ts index 53de53087..ca596d3d1 100644 --- a/packages/kit/src/index.ts +++ b/packages/kit/src/index.ts @@ -5,6 +5,7 @@ * view the LICENSE file that was distributed with this source code. */ +export * from './crypto'; export * from './domains'; export * from './utils'; export * from './constants';