feat(new tool): PGP Encryption

Fix #451 and #945
This commit is contained in:
sharevb 2024-04-28 13:01:20 +02:00 committed by ShareVB
parent 9eac9cb2a9
commit 01a0e06c88
7 changed files with 278 additions and 15 deletions

8
components.d.ts vendored
View file

@ -130,27 +130,22 @@ declare module '@vue/runtime-core' {
NCode: typeof import('naive-ui')['NCode'] NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NConfigProvider: typeof import('naive-ui')['NConfigProvider'] NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDivider: typeof import('naive-ui')['NDivider']
NEllipsis: typeof import('naive-ui')['NEllipsis'] NEllipsis: typeof import('naive-ui')['NEllipsis']
NFormItem: typeof import('naive-ui')['NFormItem'] NFormItem: typeof import('naive-ui')['NFormItem']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NH1: typeof import('naive-ui')['NH1'] NH1: typeof import('naive-ui')['NH1']
NH3: typeof import('naive-ui')['NH3'] NH3: typeof import('naive-ui')['NH3']
NIcon: typeof import('naive-ui')['NIcon'] NIcon: typeof import('naive-ui')['NIcon']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLabel: typeof import('naive-ui')['NLabel']
NLayout: typeof import('naive-ui')['NLayout'] NLayout: typeof import('naive-ui')['NLayout']
NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NLayoutSider: typeof import('naive-ui')['NLayoutSider']
NMenu: typeof import('naive-ui')['NMenu'] NMenu: typeof import('naive-ui')['NMenu']
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpin: typeof import('naive-ui')['NSpin']
NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default']
OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default']
PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default']
PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default'] PdfSignatureChecker: typeof import('./src/tools/pdf-signature-checker/pdf-signature-checker.vue')['default']
PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default'] PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default']
PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default'] PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
PgpEncryption: typeof import('./src/tools/pgp-encryption/pgp-encryption.vue')['default']
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default'] PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default'] QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default'] RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
@ -159,6 +154,7 @@ declare module '@vue/runtime-core' {
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default'] RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default']
SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default']
SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default'] SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default']
SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default']
SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default']

View file

@ -75,6 +75,7 @@
"naive-ui": "^2.35.0", "naive-ui": "^2.35.0",
"netmask": "^2.0.2", "netmask": "^2.0.2",
"node-forge": "^1.3.1", "node-forge": "^1.3.1",
"openpgp": "^5.11.1",
"oui-data": "^1.0.10", "oui-data": "^1.0.10",
"pdf-signature-reader": "^1.4.2", "pdf-signature-reader": "^1.4.2",
"pinia": "^2.0.34", "pinia": "^2.0.34",

41
pnpm-lock.yaml generated
View file

@ -125,6 +125,9 @@ dependencies:
node-forge: node-forge:
specifier: ^1.3.1 specifier: ^1.3.1
version: 1.3.1 version: 1.3.1
openpgp:
specifier: ^5.11.1
version: 5.11.1
oui-data: oui-data:
specifier: ^1.0.10 specifier: ^1.0.10
version: 1.0.10 version: 1.0.10
@ -3351,7 +3354,7 @@ packages:
dependencies: dependencies:
'@unhead/dom': 0.5.1 '@unhead/dom': 0.5.1
'@unhead/schema': 0.5.1 '@unhead/schema': 0.5.1
'@vueuse/shared': 10.7.2(vue@3.3.4) '@vueuse/shared': 10.9.0(vue@3.3.4)
unhead: 0.5.1 unhead: 0.5.1
vue: 3.3.4 vue: 3.3.4
transitivePeerDependencies: transitivePeerDependencies:
@ -3993,10 +3996,10 @@ packages:
- vue - vue
dev: false dev: false
/@vueuse/shared@10.7.2(vue@3.3.4): /@vueuse/shared@10.9.0(vue@3.3.4):
resolution: {integrity: sha512-qFbXoxS44pi2FkgFjPvF4h7c9oMDutpyBdcJdMYIMg9XyXli2meFMuaKn+UMgsClo//Th6+beeCgqweT/79BVA==} resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==}
dependencies: dependencies:
vue-demi: 0.14.6(vue@3.3.4) vue-demi: 0.14.7(vue@3.3.4)
transitivePeerDependencies: transitivePeerDependencies:
- '@vue/composition-api' - '@vue/composition-api'
- vue - vue
@ -4135,6 +4138,15 @@ packages:
is-shared-array-buffer: 1.0.2 is-shared-array-buffer: 1.0.2
dev: true dev: true
/asn1.js@5.4.1:
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
dependencies:
bn.js: 4.12.0
inherits: 2.0.4
minimalistic-assert: 1.0.1
safer-buffer: 2.1.2
dev: false
/assertion-error@1.1.0: /assertion-error@1.1.0:
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
dev: true dev: true
@ -4229,6 +4241,10 @@ packages:
readable-stream: 3.6.2 readable-stream: 3.6.2
dev: true dev: true
/bn.js@4.12.0:
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
dev: false
/boolbase@1.0.0: /boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
dev: true dev: true
@ -6905,6 +6921,10 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
/minimalistic-assert@1.0.1:
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
dev: false
/minimatch@3.1.2: /minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
dependencies: dependencies:
@ -7172,6 +7192,13 @@ packages:
is-wsl: 2.2.0 is-wsl: 2.2.0
dev: true dev: true
/openpgp@5.11.1:
resolution: {integrity: sha512-TynUBPuaSI7dN0gP+A38CjNRLxkOkkptefNanalDQ71BFAKKm+dLbksymSW5bUrB7RcAneMySL/Y+r/TbLpOnQ==}
engines: {node: '>= 8.0.0'}
dependencies:
asn1.js: 5.4.1
dev: false
/optionator@0.9.3: /optionator@0.9.3:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -7947,7 +7974,6 @@ packages:
/safer-buffer@2.1.2: /safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
requiresBuild: true requiresBuild: true
dev: true
/sax@1.2.4: /sax@1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
@ -9151,8 +9177,8 @@ packages:
vue: 3.3.4 vue: 3.3.4
dev: false dev: false
/vue-demi@0.14.6(vue@3.3.4): /vue-demi@0.14.7(vue@3.3.4):
resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
engines: {node: '>=12'} engines: {node: '>=12'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
@ -9442,6 +9468,7 @@ packages:
/workbox-google-analytics@7.0.0: /workbox-google-analytics@7.0.0:
resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==} resolution: {integrity: sha512-MEYM1JTn/qiC3DbpvP2BVhyIH+dV/5BjHk756u9VbwuAhu0QHyKscTnisQuz21lfRpOwiS9z4XdqeVAKol0bzg==}
deprecated: It is not compatible with newer versions of GA starting with v4, as long as you are using GAv3 it should be ok, but the package is not longer being maintained
dependencies: dependencies:
workbox-background-sync: 7.0.0 workbox-background-sync: 7.0.0
workbox-core: 7.0.0 workbox-core: 7.0.0

View file

@ -1,6 +1,6 @@
import { type Ref, ref, watchEffect } from 'vue'; import { type Ref, ref, watchEffect } from 'vue';
export { computedCatch }; export { computedCatch, computedCatchAsync };
function computedCatch<T, D>(getter: () => T, { defaultValue }: { defaultValue: D; defaultErrorMessage?: string }): [Ref<T | D>, Ref<string | undefined>]; function computedCatch<T, D>(getter: () => T, { defaultValue }: { defaultValue: D; defaultErrorMessage?: string }): [Ref<T | D>, Ref<string | undefined>];
function computedCatch<T, D>(getter: () => T, { defaultValue, defaultErrorMessage = 'Unknown error' }: { defaultValue?: D; defaultErrorMessage?: string } = {}) { function computedCatch<T, D>(getter: () => T, { defaultValue, defaultErrorMessage = 'Unknown error' }: { defaultValue?: D; defaultErrorMessage?: string } = {}) {
@ -20,3 +20,22 @@ function computedCatch<T, D>(getter: () => T, { defaultValue, defaultErrorMessag
return [value, error] as const; return [value, error] as const;
} }
function computedCatchAsync<T, D>(getterAsync: () => Promise<T>, { defaultValue }: { defaultValue: D; defaultErrorMessage?: string }): [Ref<T | D>, Ref<string | undefined>];
function computedCatchAsync<T, D>(getterAsync: () => Promise<T>, { defaultValue, defaultErrorMessage = 'Unknown error' }: { defaultValue?: D; defaultErrorMessage?: string } = {}) {
const error = ref<string | undefined>();
const value = ref<T | D | undefined>();
watchEffect(async () => {
try {
error.value = undefined;
value.value = await getterAsync();
}
catch (err) {
error.value = err instanceof Error ? err.message : err?.toString() ?? defaultErrorMessage;
value.value = defaultValue;
}
});
return [value, error] as const;
}

View file

@ -6,6 +6,7 @@ import { tool as asciiTextDrawer } from './ascii-text-drawer';
import { tool as textToUnicode } from './text-to-unicode'; import { tool as textToUnicode } from './text-to-unicode';
import { tool as safelinkDecoder } from './safelink-decoder'; import { tool as safelinkDecoder } from './safelink-decoder';
import { tool as pgpEncryption } from './pgp-encryption';
import { tool as pdfSignatureChecker } from './pdf-signature-checker'; import { tool as pdfSignatureChecker } from './pdf-signature-checker';
import { tool as numeronymGenerator } from './numeronym-generator'; import { tool as numeronymGenerator } from './numeronym-generator';
import { tool as macAddressGenerator } from './mac-address-generator'; import { tool as macAddressGenerator } from './mac-address-generator';
@ -85,7 +86,20 @@ import { tool as yamlViewer } from './yaml-viewer';
export const toolsByCategory: ToolCategory[] = [ export const toolsByCategory: ToolCategory[] = [
{ {
name: 'Crypto', name: 'Crypto',
components: [tokenGenerator, hashText, bcrypt, uuidGenerator, ulidGenerator, cypher, bip39, hmacGenerator, rsaKeyPairGenerator, passwordStrengthAnalyser, pdfSignatureChecker], components: [
tokenGenerator,
hashText,
bcrypt,
uuidGenerator,
ulidGenerator,
cypher,
bip39,
hmacGenerator,
rsaKeyPairGenerator,
passwordStrengthAnalyser,
pdfSignatureChecker,
pgpEncryption,
],
}, },
{ {
name: 'Converter', name: 'Converter',

View file

@ -0,0 +1,12 @@
import { Lock } from '@vicons/tabler';
import { defineTool } from '../tool';
export const tool = defineTool({
name: 'PGP encryption',
path: '/pgp-encryption',
description: 'Encrypt and decrypt text clear text using PGP Keys.',
keywords: ['pgp', 'openpgp', 'encryption', 'cypher', 'encipher', 'crypt', 'decrypt'],
component: () => import('./pgp-encryption.vue'),
icon: Lock,
createdAt: new Date('2024-04-20'),
});

View file

@ -0,0 +1,194 @@
<script setup lang="ts">
import * as openpgp from 'openpgp';
import { computedCatchAsync } from '@/composable/computed/catchedComputed';
const cryptInput = ref('');
const cryptPublicKey = ref('');
const cryptPrivateKey = ref('');
const cryptPrivateKeyPassphrase = ref('');
const [cryptOutput, cryptError] = computedCatchAsync(async () => {
const publicKeyArmored = cryptPublicKey.value;
const privateKeyArmored = cryptPrivateKey.value;
const passphrase = cryptPrivateKeyPassphrase.value;
const text = cryptInput.value;
const publicKey = await openpgp.readKey({ armoredKey: publicKeyArmored });
const privateKey = privateKeyArmored !== ''
? (passphrase !== ''
? await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }),
passphrase,
})
: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }))
: undefined;
return await openpgp.encrypt({
message: await openpgp.createMessage({ text }),
encryptionKeys: publicKey,
signingKeys: privateKey,
});
}, {
defaultValue: '',
defaultErrorMessage: 'Unable to encrypt your text',
});
const decryptInput = ref('');
const decryptPublicKey = ref('');
const decryptPrivateKey = ref('');
const decryptPrivateKeyPassphrase = ref('');
const [decryptOutput, decryptError] = computedCatchAsync(async () => {
const publicKeyArmored = decryptPublicKey.value;
const privateKeyArmored = decryptPrivateKey.value;
const passphrase = decryptPrivateKeyPassphrase.value;
const encrypted = decryptInput.value;
const publicKey = publicKeyArmored !== '' ? await openpgp.readKey({ armoredKey: publicKeyArmored }) : undefined;
const privateKey = passphrase !== ''
? await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored }),
passphrase,
})
: await openpgp.readPrivateKey({ armoredKey: privateKeyArmored });
const message = await openpgp.readMessage({
armoredMessage: encrypted, // parse armored message
});
const { data: decrypted, signatures } = await openpgp.decrypt({
message,
verificationKeys: publicKey, // optional
decryptionKeys: privateKey,
});
if (signatures.length > 0) {
try {
await signatures[0].verified; // throws on invalid signature
}
catch (e: any) {
return {
decryptedText: decrypted,
signatureError: `Signature could not be verified: ${e.toString()}`,
};
}
}
return {
decryptedText: decrypted,
signatureError: '',
};
}, {
defaultValue: { decryptedText: '', signatureError: '' },
defaultErrorMessage: 'Unable to encrypt your text',
});
</script>
<template>
<div>
<c-card title="Encrypt">
<div>
<c-input-text
v-model:value="cryptInput"
label="Your text:"
placeholder="The string to encrypt"
rows="4"
multiline raw-text monospace autosize flex-1
/>
<div flex flex-1 flex-col gap-2>
<c-input-text
v-model:value="cryptPublicKey"
label="Target public key:"
placeholder="Target public key"
rows="5"
multiline raw-text monospace autosize flex-1
/>
<details>
<summary>Signing private key (optional)</summary>
<c-input-text
v-model:value="cryptPrivateKey"
label="Your private key:"
placeholder="The private key to use to sign message"
rows="5"
multiline raw-text monospace autosize flex-1
/>
<c-input-text
v-model:value="cryptPrivateKeyPassphrase"
label="Your private key password:" clearable raw-text
/>
</details>
</div>
</div>
<c-alert
v-if="cryptError && cryptPublicKey !== ''"
type="error" mt-12 title="Error while encrypting"
>
{{ cryptError }}
</c-alert>
<n-form-item label="Your text encrypted:" mt-3>
<TextareaCopyable
:value="cryptOutput || ''"
rows="3"
placeholder="Your string encrypted"
multiline monospace readonly autosize mt-5
/>
</n-form-item>
</c-card>
<c-card title="Decrypt">
<div>
<c-input-text
v-model:value="decryptInput"
label="Your PGP Message to decrypt:"
placeholder="The string to decrypt"
rows="4"
multiline raw-text monospace autosize flex-1
/>
<div flex flex-1 flex-col gap-2>
<c-input-text
v-model:value="decryptPrivateKey"
label="Your private key:"
placeholder="The private key to use to decrypt message"
rows="5"
multiline raw-text monospace autosize flex-1
/>
<c-input-text
v-model:value="decryptPrivateKeyPassphrase"
label="Your private key password:" clearable raw-text
/>
<details>
<summary>Signing public key (optional)</summary>
<c-input-text
v-model:value="decryptPublicKey"
label="Sender public key:"
placeholder="Sender public key"
rows="5"
multiline raw-text monospace autosize flex-1
/>
</details>
</div>
</div>
<c-alert v-if="decryptError && decryptPrivateKey !== ''" type="error" mt-3 title="Error while decrypting">
{{ decryptError }}
</c-alert>
<c-alert v-if="decryptOutput?.signatureError !== ''" type="error" mt-3 title="Signature verification">
{{ decryptOutput?.signatureError }}
</c-alert>
<n-form-item label="Your text decrypted:" mt-3>
<TextareaCopyable
:value="decryptOutput?.decryptedText || ''"
rows="3"
placeholder="Your string decrypted"
multiline monospace readonly autosize mt-5
/>
</n-form-item>
</c-card>
</div>
</template>