mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-02-23 19:49:56 +01:00
web: use pgp keys for inbox
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
13
apps/web/package-lock.json
generated
13
apps/web/package-lock.json
generated
@@ -115,6 +115,7 @@
|
||||
"happy-dom": "16.0.1",
|
||||
"ip": "^2.0.1",
|
||||
"lorem-ipsum": "^2.0.4",
|
||||
"openpgp": "^6.2.2",
|
||||
"otplib": "^12.0.1",
|
||||
"rollup-plugin-visualizer": "^5.13.1",
|
||||
"vite": "5.4.11",
|
||||
@@ -507,7 +508,7 @@
|
||||
},
|
||||
"../desktop": {
|
||||
"name": "@notesnook/desktop",
|
||||
"version": "3.3.8-beta.0",
|
||||
"version": "3.3.8-beta.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
@@ -9655,6 +9656,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openpgp": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/openpgp/-/openpgp-6.2.2.tgz",
|
||||
"integrity": "sha512-P/dyEqQ3gfwOCo+xsqffzXjmUhGn4AZTOJ1LCcN21S23vAk+EAvMJOQTsb/C8krL6GjOSBxqGYckhik7+hneNw==",
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0+",
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/otplib": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/otplib/-/otplib-12.0.1.tgz",
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
"happy-dom": "16.0.1",
|
||||
"ip": "^2.0.1",
|
||||
"lorem-ipsum": "^2.0.4",
|
||||
"openpgp": "^6.2.2",
|
||||
"otplib": "^12.0.1",
|
||||
"rollup-plugin-visualizer": "^5.13.1",
|
||||
"vite": "5.4.11",
|
||||
|
||||
210
apps/web/src/dialogs/inbox-pgp-keys-dialog.tsx
Normal file
210
apps/web/src/dialogs/inbox-pgp-keys-dialog.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import Dialog from "../components/dialog";
|
||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||
import { db } from "../common/db";
|
||||
import Field from "../components/field";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { SerializedKeyPair } from "@notesnook/crypto";
|
||||
import { ConfirmDialog } from "./confirm";
|
||||
|
||||
type InboxPGPKeysDialogProps = BaseDialogProps<boolean> & {
|
||||
keys?: SerializedKeyPair | null;
|
||||
};
|
||||
|
||||
export const InboxPGPKeysDialog = DialogManager.register(
|
||||
function InboxPGPKeysDialog(props: InboxPGPKeysDialogProps) {
|
||||
const { keys: initialKeys, onClose } = props;
|
||||
const [mode, setMode] = useState<"choose" | "edit">(
|
||||
initialKeys ? "edit" : "choose"
|
||||
);
|
||||
const [publicKey, setPublicKey] = useState(initialKeys?.publicKey || "");
|
||||
const [privateKey, setPrivateKey] = useState(initialKeys?.privateKey || "");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const hasChanges =
|
||||
publicKey !== (initialKeys?.publicKey || "") ||
|
||||
privateKey !== (initialKeys?.privateKey || "");
|
||||
|
||||
async function handleAutoGenerate() {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await db.user.getInboxKeys();
|
||||
showToast("success", "Inbox keys generated");
|
||||
onClose(true);
|
||||
} catch (error) {
|
||||
showToast("error", "Failed to generate inbox keys");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const trimmedPublicKey = publicKey.trim();
|
||||
const trimmedPrivateKey = privateKey.trim();
|
||||
if (!trimmedPublicKey || !trimmedPrivateKey) {
|
||||
showToast("error", "Both public and private keys are required");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const isValid = await db.storage().validatePGPKeyPair({
|
||||
publicKey: trimmedPublicKey,
|
||||
privateKey: trimmedPrivateKey
|
||||
});
|
||||
if (!isValid) {
|
||||
showToast(
|
||||
"error",
|
||||
"Invalid PGP key pair. Please check your keys and try again."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (initialKeys) {
|
||||
const ok = await ConfirmDialog.show({
|
||||
title: "Change Inbox PGP Keys",
|
||||
message:
|
||||
"Changing Inbox PGP keys will delete all your unsynced inbox items. Are you sure?",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
await db.user.saveInboxKeys({
|
||||
publicKey: trimmedPublicKey,
|
||||
privateKey: trimmedPrivateKey
|
||||
});
|
||||
showToast("success", "Inbox keys saved");
|
||||
onClose(true);
|
||||
} catch (error) {
|
||||
showToast("error", "Failed to save inbox keys");
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === "choose") {
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title="Setup Inbox PGP Keys"
|
||||
width={500}
|
||||
negativeButton={{
|
||||
text: "Cancel",
|
||||
onClick: () => onClose(false)
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column", gap: 3 }}>
|
||||
<Text sx={{ fontSize: "body", color: "paragraph" }}>
|
||||
Choose how you want to set up your Inbox PGP keys:
|
||||
</Text>
|
||||
<Flex sx={{ flexDirection: "column", gap: 2 }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleAutoGenerate}
|
||||
disabled={isLoading}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
{isLoading ? "Generating..." : "Auto-generate keys"}
|
||||
</Button>
|
||||
<Text
|
||||
sx={{
|
||||
fontSize: "body",
|
||||
color: "paragraph",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Or
|
||||
</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setMode("edit")}
|
||||
disabled={isLoading}
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
Provide your own keys
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title="Inbox PGP Keys"
|
||||
width={600}
|
||||
positiveButton={{
|
||||
text: isLoading ? "Saving..." : "Save",
|
||||
onClick: handleSave,
|
||||
disabled: isLoading || !hasChanges
|
||||
}}
|
||||
negativeButton={{
|
||||
text: "Cancel",
|
||||
onClick: () => onClose(false)
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column", gap: 3 }}>
|
||||
<Field
|
||||
label="Public Key"
|
||||
id="publicKey"
|
||||
name="publicKey"
|
||||
as="textarea"
|
||||
required
|
||||
value={publicKey}
|
||||
onChange={(e) => setPublicKey(e.target.value)}
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "body",
|
||||
minHeight: 150,
|
||||
resize: "vertical"
|
||||
}}
|
||||
placeholder="Enter your PGP public key..."
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Field
|
||||
label="Private Key"
|
||||
id="privateKey"
|
||||
name="privateKey"
|
||||
as="textarea"
|
||||
required
|
||||
value={privateKey}
|
||||
onChange={(e) => setPrivateKey(e.target.value)}
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "body",
|
||||
minHeight: 150,
|
||||
resize: "vertical"
|
||||
}}
|
||||
placeholder="Enter your PGP private key..."
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -20,6 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { SettingsGroup } from "./types";
|
||||
import { useStore as useSettingStore } from "../../stores/setting-store";
|
||||
import { InboxApiKeys } from "./components/inbox-api-keys";
|
||||
import { InboxPGPKeysDialog } from "../inbox-pgp-keys-dialog";
|
||||
import { db } from "../../common/db";
|
||||
import { showPasswordDialog } from "../password-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const InboxSettings: SettingsGroup[] = [
|
||||
{
|
||||
@@ -42,6 +46,41 @@ export const InboxSettings: SettingsGroup[] = [
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "show-inbox-pgp-keys",
|
||||
title: "Inbox PGP Keys",
|
||||
description: "View/edit your Inbox PGP keys",
|
||||
keywords: ["inbox", "pgp", "keys"],
|
||||
onStateChange: (listener) =>
|
||||
useSettingStore.subscribe((s) => s.isInboxEnabled, listener),
|
||||
isHidden: () => !useSettingStore.getState().isInboxEnabled,
|
||||
components: [
|
||||
{
|
||||
type: "button",
|
||||
title: "Show",
|
||||
variant: "secondary",
|
||||
action: async () => {
|
||||
const ok = await showPasswordDialog({
|
||||
title: "Authenticate to view/edit Inbox PGP keys",
|
||||
inputs: {
|
||||
password: {
|
||||
label: strings.accountPassword(),
|
||||
autoComplete: "current-password"
|
||||
}
|
||||
},
|
||||
validate: ({ password }) => {
|
||||
return db.user.verifyPassword(password);
|
||||
}
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
InboxPGPKeysDialog.show({
|
||||
keys: await db.user.getInboxKeys()
|
||||
});
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "inbox-api-keys",
|
||||
title: "",
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
} from "./key-value";
|
||||
import { NNCrypto } from "./nncrypto";
|
||||
import type {
|
||||
AsymmetricCipher,
|
||||
Cipher,
|
||||
SerializedKey,
|
||||
SerializedKeyPair
|
||||
@@ -34,6 +33,7 @@ import type {
|
||||
import { isFeatureSupported } from "../utils/feature-check";
|
||||
import { IKeyStore } from "./key-store";
|
||||
import { User } from "@notesnook/core";
|
||||
import * as openpgp from "openpgp";
|
||||
|
||||
type EncryptedKey = { iv: Uint8Array; cipher: BufferSource };
|
||||
export type DatabasePersistence = "memory" | "db";
|
||||
@@ -133,8 +133,44 @@ export class NNStorage implements IStorage {
|
||||
return await NNCrypto.exportKey(password, salt);
|
||||
}
|
||||
|
||||
async generateCryptoKeyPair() {
|
||||
return await NNCrypto.exportKeyPair();
|
||||
async generatePGPKeyPair(): Promise<SerializedKeyPair> {
|
||||
const keys = await openpgp.generateKey({
|
||||
userIDs: [{ name: "NN", email: "NN@NN.NN" }]
|
||||
});
|
||||
return { publicKey: keys.publicKey, privateKey: keys.privateKey };
|
||||
}
|
||||
|
||||
async validatePGPKeyPair(keys: SerializedKeyPair): Promise<boolean> {
|
||||
try {
|
||||
const dummyData = JSON.stringify({
|
||||
favorite: true,
|
||||
title: "Hello world"
|
||||
});
|
||||
|
||||
const publicKey = await openpgp.readKey({ armoredKey: keys.publicKey });
|
||||
const encrypted = await openpgp.encrypt({
|
||||
message: await openpgp.createMessage({
|
||||
text: dummyData
|
||||
}),
|
||||
encryptionKeys: publicKey
|
||||
});
|
||||
|
||||
const message = await openpgp.readMessage({
|
||||
armoredMessage: encrypted
|
||||
});
|
||||
const privateKey = await openpgp.readPrivateKey({
|
||||
armoredKey: keys.privateKey
|
||||
});
|
||||
const decrypted = await openpgp.decrypt({
|
||||
message,
|
||||
decryptionKeys: privateKey
|
||||
});
|
||||
|
||||
return decrypted.data === dummyData;
|
||||
} catch (e) {
|
||||
console.error("PGP key pair validation error:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async hash(password: string, email: string): Promise<string> {
|
||||
@@ -165,12 +201,21 @@ export class NNStorage implements IStorage {
|
||||
return NNCrypto.decryptMulti(key, items, "text");
|
||||
}
|
||||
|
||||
decryptAsymmetric(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<"base64">
|
||||
async decryptPGPMessage(
|
||||
privateKeyArmored: string,
|
||||
encryptedMessage: string
|
||||
): Promise<string> {
|
||||
cipherData.format = "base64";
|
||||
return NNCrypto.decryptAsymmetric(keyPair, cipherData, "base64");
|
||||
const message = await openpgp.readMessage({
|
||||
armoredMessage: encryptedMessage
|
||||
});
|
||||
const privateKey = await openpgp.readPrivateKey({
|
||||
armoredKey: privateKeyArmored
|
||||
});
|
||||
const decrypted = await openpgp.decrypt({
|
||||
message,
|
||||
decryptionKeys: privateKey
|
||||
});
|
||||
return decrypted.data;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,8 @@ import { TimeFormat, DayFormat, WeekFormat } from "@notesnook/core";
|
||||
import { Profile, TrashCleanupInterval } from "@notesnook/core";
|
||||
import { showToast } from "../utils/toast";
|
||||
import { ConfirmDialog } from "../dialogs/confirm";
|
||||
import * as openpgp from "openpgp";
|
||||
import { InboxPGPKeysDialog } from "../dialogs/inbox-pgp-keys-dialog";
|
||||
|
||||
export const HostIds = [
|
||||
"API_HOST",
|
||||
@@ -294,17 +296,14 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
|
||||
try {
|
||||
if (isInboxEnabled) {
|
||||
const inboxTokens = await db.inboxApiKeys.get();
|
||||
if (inboxTokens && inboxTokens.length > 0) {
|
||||
const ok = await ConfirmDialog.show({
|
||||
title: "Disable Inbox API",
|
||||
message:
|
||||
"Disabling will revoke all existing API keys, they will no longer work. Are you sure?",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
if (!ok) return;
|
||||
}
|
||||
const ok = await ConfirmDialog.show({
|
||||
title: "Disable Inbox API",
|
||||
message:
|
||||
"Disabling will delete all your unsynced inbox items. Additionally, disabling will revoke all existing API keys, they will no longer work. Are you sure?",
|
||||
positiveButtonText: "Yes",
|
||||
negativeButtonText: "No"
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
await db.user.discardInboxKeys();
|
||||
this.set({ isInboxEnabled: false });
|
||||
@@ -312,8 +311,10 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
return;
|
||||
}
|
||||
|
||||
await db.user.getInboxKeys();
|
||||
this.set({ isInboxEnabled: true });
|
||||
const ok = await InboxPGPKeysDialog.show({ keys: null });
|
||||
if (ok) {
|
||||
this.set({ isInboxEnabled: true });
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
showToast("error", e.message);
|
||||
|
||||
@@ -187,24 +187,11 @@ export async function handleInboxItems(
|
||||
continue;
|
||||
}
|
||||
|
||||
const decryptedKey = await db.storage().decryptAsymmetric(inboxKeys, {
|
||||
alg: item.key.alg,
|
||||
cipher: item.key.cipher,
|
||||
format: "base64",
|
||||
length: item.key.length
|
||||
});
|
||||
const decryptedItem = await db.storage().decrypt(
|
||||
{ key: decryptedKey },
|
||||
{
|
||||
alg: item.alg,
|
||||
iv: item.iv,
|
||||
cipher: item.cipher,
|
||||
format: "base64",
|
||||
length: item.length,
|
||||
salt: item.salt
|
||||
}
|
||||
);
|
||||
const decryptedItem = await db
|
||||
.storage()
|
||||
.decryptPGPMessage(inboxKeys.privateKey, item.cipher);
|
||||
const parsed = JSON.parse(decryptedItem) as ParsedInboxItem;
|
||||
|
||||
if (parsed.type !== "note") {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -50,8 +50,11 @@ export type SyncTransferItem = {
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type SyncInboxItem = Omit<SyncItem, "format"> & {
|
||||
key: Omit<Cipher<"base64">, "format" | "salt" | "iv">;
|
||||
export type SyncInboxItem = {
|
||||
id: string;
|
||||
v: number;
|
||||
cipher: string;
|
||||
alg: string;
|
||||
};
|
||||
|
||||
export type ParsedInboxItem = {
|
||||
|
||||
@@ -533,7 +533,7 @@ class UserManager {
|
||||
this.cachedInboxKeys = key;
|
||||
},
|
||||
userProperty: "inboxKeys",
|
||||
generateKey: () => this.db.crypto().generateCryptoKeyPair(),
|
||||
generateKey: () => this.db.crypto().generatePGPKeyPair(),
|
||||
errorContext: "inbox encryption keys",
|
||||
encrypt: async (keys, userEncryptionKey) => {
|
||||
const encryptedPrivateKey = await this.db
|
||||
@@ -584,6 +584,23 @@ class UserManager {
|
||||
await this.setUser({ ...user, inboxKeys: undefined });
|
||||
}
|
||||
|
||||
async saveInboxKeys(keys: SerializedKeyPair) {
|
||||
this.cachedInboxKeys = keys;
|
||||
|
||||
const userEncryptionKey = await this.getEncryptionKey();
|
||||
if (!userEncryptionKey) return;
|
||||
|
||||
const updatePayload = {
|
||||
inboxKeys: {
|
||||
public: keys.publicKey,
|
||||
private: await this.db
|
||||
.storage()
|
||||
.encrypt(userEncryptionKey, JSON.stringify(keys.privateKey))
|
||||
}
|
||||
};
|
||||
await this.updateUser(updatePayload);
|
||||
}
|
||||
|
||||
async sendVerificationEmail(newEmail?: string) {
|
||||
const token = await this.tokenManager.getAccessToken();
|
||||
if (!token) return;
|
||||
|
||||
@@ -18,7 +18,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
AsymmetricCipher,
|
||||
Cipher,
|
||||
DataFormat,
|
||||
SerializedKey,
|
||||
@@ -63,10 +62,6 @@ export interface IStorage {
|
||||
key: SerializedKey,
|
||||
items: Cipher<"base64">[]
|
||||
): Promise<string[]>;
|
||||
decryptAsymmetric(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<"base64">
|
||||
): Promise<string>;
|
||||
deriveCryptoKey(credentials: SerializedKey): Promise<void>;
|
||||
hash(
|
||||
password: string,
|
||||
@@ -75,7 +70,12 @@ export interface IStorage {
|
||||
): Promise<string>;
|
||||
getCryptoKey(): Promise<string | undefined>;
|
||||
generateCryptoKey(password: string, salt?: string): Promise<SerializedKey>;
|
||||
generateCryptoKeyPair(): Promise<SerializedKeyPair>;
|
||||
generatePGPKeyPair(): Promise<SerializedKeyPair>;
|
||||
decryptPGPMessage(
|
||||
privateKeyArmored: string,
|
||||
encryptedMessage: string
|
||||
): Promise<string>;
|
||||
validatePGPKeyPair(keys: SerializedKeyPair): Promise<boolean>;
|
||||
|
||||
generateCryptoKeyFallback(
|
||||
password: string,
|
||||
|
||||
@@ -30,8 +30,8 @@ export class Crypto {
|
||||
return await this.storage().generateCryptoKey(password);
|
||||
}
|
||||
|
||||
async generateCryptoKeyPair() {
|
||||
return await this.storage().generateCryptoKeyPair();
|
||||
async generatePGPKeyPair() {
|
||||
return await this.storage().generatePGPKeyPair();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,19 +19,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { ISodium } from "@notesnook/sodium";
|
||||
import KeyUtils, { base64_variants } from "./keyutils.js";
|
||||
import {
|
||||
Cipher,
|
||||
Output,
|
||||
DataFormat,
|
||||
SerializedKey,
|
||||
SerializedKeyPair,
|
||||
AsymmetricCipher
|
||||
} from "./types.js";
|
||||
import { Cipher, Output, DataFormat, SerializedKey } from "./types.js";
|
||||
|
||||
export default class Decryption {
|
||||
private static transformInput(
|
||||
sodium: ISodium,
|
||||
cipherData: Cipher<DataFormat> | AsymmetricCipher<DataFormat>
|
||||
cipherData: Cipher<DataFormat>
|
||||
): Uint8Array {
|
||||
let input: Uint8Array | null = null;
|
||||
if (
|
||||
@@ -80,28 +73,6 @@ export default class Decryption {
|
||||
) as Output<TOutputFormat>;
|
||||
}
|
||||
|
||||
static decryptAsymmetric<TOutputFormat extends DataFormat>(
|
||||
sodium: ISodium,
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<DataFormat>,
|
||||
outputFormat: TOutputFormat = "text" as TOutputFormat
|
||||
): Output<TOutputFormat> {
|
||||
const input = this.transformInput(sodium, cipherData);
|
||||
const plaintext = sodium.crypto_box_seal_open(
|
||||
input,
|
||||
sodium.from_base64(keyPair.publicKey),
|
||||
sodium.from_base64(keyPair.privateKey)
|
||||
);
|
||||
|
||||
return (
|
||||
outputFormat === "base64"
|
||||
? sodium.to_base64(plaintext, base64_variants.URLSAFE_NO_PADDING)
|
||||
: outputFormat === "text"
|
||||
? sodium.to_string(plaintext)
|
||||
: plaintext
|
||||
) as Output<TOutputFormat>;
|
||||
}
|
||||
|
||||
static createStream(
|
||||
sodium: ISodium,
|
||||
header: string,
|
||||
|
||||
@@ -31,8 +31,7 @@ import {
|
||||
DataFormat,
|
||||
SerializedKey,
|
||||
SerializedKeyPair,
|
||||
EncryptionKeyPair,
|
||||
AsymmetricCipher
|
||||
EncryptionKeyPair
|
||||
} from "./types.js";
|
||||
|
||||
export class NNCrypto implements INNCrypto {
|
||||
@@ -98,20 +97,6 @@ export class NNCrypto implements INNCrypto {
|
||||
return decryptedItems;
|
||||
}
|
||||
|
||||
async decryptAsymmetric<TOutputFormat extends DataFormat>(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<DataFormat>,
|
||||
outputFormat: TOutputFormat = "text" as TOutputFormat
|
||||
): Promise<Output<TOutputFormat>> {
|
||||
await this.init();
|
||||
return Decryption.decryptAsymmetric(
|
||||
this.sodium,
|
||||
keyPair,
|
||||
cipherData,
|
||||
outputFormat
|
||||
);
|
||||
}
|
||||
|
||||
async hash(password: string, salt: string): Promise<string> {
|
||||
await this.init();
|
||||
return Password.hash(this.sodium, password, salt);
|
||||
|
||||
@@ -26,8 +26,7 @@ import {
|
||||
Output,
|
||||
Input,
|
||||
EncryptionKeyPair,
|
||||
SerializedKeyPair,
|
||||
AsymmetricCipher
|
||||
SerializedKeyPair
|
||||
} from "./types.js";
|
||||
|
||||
export interface IStreamable {
|
||||
@@ -62,12 +61,6 @@ export interface INNCrypto {
|
||||
outputFormat?: TOutputFormat
|
||||
): Promise<Output<TOutputFormat>[]>;
|
||||
|
||||
decryptAsymmetric<TOutputFormat extends DataFormat>(
|
||||
keyPair: SerializedKeyPair,
|
||||
cipherData: AsymmetricCipher<DataFormat>,
|
||||
outputFormat?: TOutputFormat
|
||||
): Promise<Output<TOutputFormat>>;
|
||||
|
||||
hash(password: string, salt: string): Promise<string>;
|
||||
|
||||
deriveKey(password: string, salt?: string): Promise<EncryptionKey>;
|
||||
|
||||
@@ -30,11 +30,6 @@ export type Cipher<TFormat extends DataFormat> = {
|
||||
length: number;
|
||||
};
|
||||
|
||||
export type AsymmetricCipher<TFormat extends DataFormat> = Omit<
|
||||
Cipher<TFormat>,
|
||||
"iv" | "salt"
|
||||
>;
|
||||
|
||||
export type Output<TFormat extends DataFormat> =
|
||||
TFormat extends StringOutputFormat ? string : Uint8Array;
|
||||
export type Input<TFormat extends DataFormat> = Output<TFormat>;
|
||||
|
||||
@@ -118,12 +118,6 @@ export class Sodium implements ISodium {
|
||||
get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE() {
|
||||
return sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
}
|
||||
get crypto_box_keypair() {
|
||||
return sodium.crypto_box_keypair;
|
||||
}
|
||||
get crypto_box_seal_open() {
|
||||
return sodium.crypto_box_seal_open;
|
||||
}
|
||||
}
|
||||
|
||||
function convertVariant(variant: base64_variants): sodium.base64_variants {
|
||||
|
||||
@@ -46,12 +46,7 @@ import {
|
||||
crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
|
||||
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES,
|
||||
crypto_secretstream_xchacha20poly1305_TAG_FINAL,
|
||||
crypto_secretstream_xchacha20poly1305_TAG_MESSAGE,
|
||||
crypto_box_keypair as sodium_native_crypto_box_keypair,
|
||||
crypto_box_PUBLICKEYBYTES,
|
||||
crypto_box_SECRETKEYBYTES,
|
||||
crypto_box_seal_open as sodium_native_crypto_box_seal_open,
|
||||
crypto_box_SEALBYTES
|
||||
crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
|
||||
} from "sodium-native";
|
||||
import { Buffer } from "node:buffer";
|
||||
import { base64_variants, ISodium } from "./types";
|
||||
@@ -346,71 +341,6 @@ function crypto_secretstream_xchacha20poly1305_pull(
|
||||
return { message, tag: tag.readUInt8() } as MessageTag | StringMessageTag;
|
||||
}
|
||||
|
||||
function crypto_box_keypair(
|
||||
outputFormat?: Uint8ArrayOutputFormat | null
|
||||
): KeyPair;
|
||||
function crypto_box_keypair(outputFormat: StringOutputFormat): StringKeyPair;
|
||||
function crypto_box_keypair(
|
||||
outputFormat?: Uint8ArrayOutputFormat | null | StringOutputFormat
|
||||
): KeyPair | StringKeyPair {
|
||||
const publicBuffer = Buffer.alloc(crypto_box_PUBLICKEYBYTES);
|
||||
const privateBuffer = Buffer.alloc(crypto_box_SECRETKEYBYTES);
|
||||
|
||||
sodium_native_crypto_box_keypair(publicBuffer, privateBuffer);
|
||||
|
||||
if (typeof outputFormat === "string") {
|
||||
const transformer =
|
||||
outputFormat === "base64"
|
||||
? to_base64
|
||||
: outputFormat === "hex"
|
||||
? to_hex
|
||||
: to_string;
|
||||
return {
|
||||
keyType: "x25519" as KeyType,
|
||||
publicKey: transformer(new Uint8Array(publicBuffer)),
|
||||
privateKey: transformer(new Uint8Array(privateBuffer))
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
keyType: "x25519" as KeyType,
|
||||
publicKey: new Uint8Array(publicBuffer),
|
||||
privateKey: new Uint8Array(privateBuffer)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function crypto_box_seal_open(
|
||||
ciphertext: string | Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
privateKey: Uint8Array,
|
||||
outputFormat?: Uint8ArrayOutputFormat | null
|
||||
): Uint8Array;
|
||||
function crypto_box_seal_open(
|
||||
ciphertext: string | Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
privateKey: Uint8Array,
|
||||
outputFormat: StringOutputFormat
|
||||
): string;
|
||||
function crypto_box_seal_open(
|
||||
ciphertext: string | Uint8Array,
|
||||
publicKey: Uint8Array,
|
||||
privateKey: Uint8Array,
|
||||
outputFormat?: StringOutputFormat | Uint8ArrayOutputFormat | null
|
||||
): string | Uint8Array {
|
||||
const cipher = toBuffer(ciphertext);
|
||||
return wrap(
|
||||
cipher.byteLength - crypto_box_SEALBYTES,
|
||||
(message) =>
|
||||
sodium_native_crypto_box_seal_open(
|
||||
message,
|
||||
cipher,
|
||||
toBuffer(publicKey),
|
||||
toBuffer(privateKey)
|
||||
),
|
||||
outputFormat
|
||||
);
|
||||
}
|
||||
|
||||
function randombytes_buf(
|
||||
length: number,
|
||||
outputFormat?: Uint8ArrayOutputFormat | null
|
||||
@@ -473,10 +403,6 @@ function to_string(input: Uint8Array): string {
|
||||
);
|
||||
}
|
||||
|
||||
function to_hex(input: Uint8Array): string {
|
||||
return Buffer.from(input, input.byteOffset, input.byteLength).toString("hex");
|
||||
}
|
||||
|
||||
type ToBufferInput = string | Uint8Array | null | undefined;
|
||||
type ToBufferResult<TInput extends ToBufferInput> = TInput extends
|
||||
| undefined
|
||||
@@ -625,12 +551,6 @@ export class Sodium implements ISodium {
|
||||
get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE() {
|
||||
return crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
}
|
||||
get crypto_box_keypair() {
|
||||
return crypto_box_keypair;
|
||||
}
|
||||
get crypto_box_seal_open() {
|
||||
return crypto_box_seal_open;
|
||||
}
|
||||
}
|
||||
|
||||
export { base64_variants, type ISodium };
|
||||
|
||||
@@ -61,6 +61,4 @@ export interface ISodium {
|
||||
get crypto_aead_xchacha20poly1305_ietf_NPUBBYTES(): typeof sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES;
|
||||
get crypto_secretstream_xchacha20poly1305_TAG_FINAL(): typeof sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
|
||||
get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE(): typeof sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
|
||||
get crypto_box_keypair(): typeof sodium.crypto_box_keypair;
|
||||
get crypto_box_seal_open(): typeof sodium.crypto_box_seal_open;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user