web: use pgp keys for inbox

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2025-11-07 16:35:12 +05:00
committed by Ammar Ahmed
parent 60df472d53
commit 9a2ce9873b
18 changed files with 369 additions and 199 deletions

View File

@@ -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",

View File

@@ -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",

View 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>
);
}
);

View File

@@ -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: "",

View File

@@ -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;
}
/**

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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 = {

View File

@@ -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;

View File

@@ -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,

View File

@@ -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();
}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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>;

View File

@@ -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>;

View File

@@ -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 {

View File

@@ -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 };

View File

@@ -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;
}