diff --git a/apps/web/src/components/icons/index.tsx b/apps/web/src/components/icons/index.tsx
index ec4eae8bc..c1cf197b3 100644
--- a/apps/web/src/components/icons/index.tsx
+++ b/apps/web/src/components/icons/index.tsx
@@ -230,7 +230,8 @@ import {
mdiHamburger,
mdiNotePlus,
mdiNoteEditOutline,
- mdiArrowUp
+ mdiArrowUp,
+ mdiInbox
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -584,3 +585,4 @@ export const ColorRemove = createIcon(mdiCloseCircleOutline);
export const ExpandSidebar = createIcon(mdiArrowCollapseRight);
export const HamburgerMenu = createIcon(mdiMenu);
export const ArrowUp = createIcon(mdiArrowUp);
+export const Inbox = createIcon(mdiInbox);
diff --git a/apps/web/src/dialogs/settings/inbox-settings.ts b/apps/web/src/dialogs/settings/inbox-settings.ts
new file mode 100644
index 000000000..e6834feb9
--- /dev/null
+++ b/apps/web/src/dialogs/settings/inbox-settings.ts
@@ -0,0 +1,46 @@
+/*
+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 .
+*/
+
+import { SettingsGroup } from "./types";
+import { useStore as useSettingStore } from "../../stores/setting-store";
+
+export const InboxSettings: SettingsGroup[] = [
+ {
+ key: "inbox",
+ section: "inbox",
+ header: "Inbox",
+ settings: [
+ {
+ key: "toggle-inbox",
+ title: "Enable Inbox API",
+ description: "Enable/disable Inbox API",
+ keywords: ["inbox"],
+ onStateChange: (listener) =>
+ useSettingStore.subscribe((s) => s.isInboxEnabled, listener),
+ components: [
+ {
+ type: "toggle",
+ isToggled: () => useSettingStore.getState().isInboxEnabled,
+ toggle: () => useSettingStore.getState().toggleInbox()
+ }
+ ]
+ }
+ ]
+ }
+];
diff --git a/apps/web/src/dialogs/settings/index.tsx b/apps/web/src/dialogs/settings/index.tsx
index eea1b2bc8..1ca89af29 100644
--- a/apps/web/src/dialogs/settings/index.tsx
+++ b/apps/web/src/dialogs/settings/index.tsx
@@ -38,7 +38,8 @@ import {
Pro,
Servers,
ShieldLock,
- Sync
+ Sync,
+ Inbox
} from "../../components/icons";
import NavigationItem from "../../components/navigation-menu/navigation-item";
import { FlexScrollContainer } from "../../components/scroll-container";
@@ -78,6 +79,7 @@ import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
import { ServersSettings } from "./servers-settings";
import { strings } from "@notesnook/intl";
import { mdToHtml } from "../../utils/md";
+import { InboxSettings } from "./inbox-settings";
type SettingsDialogProps = BaseDialogProps & {
activeSection?: SectionKeys;
@@ -106,6 +108,12 @@ const sectionGroups: SectionGroup[] = [
title: strings.sync(),
icon: Sync,
isHidden: () => !useUserStore.getState().isLoggedIn
+ },
+ {
+ key: "inbox",
+ title: "Inbox",
+ icon: Inbox,
+ isHidden: () => true // hidden until complete
}
]
},
@@ -176,7 +184,8 @@ const SettingsGroups = [
...SupportSettings,
...AboutSettings,
...SubscriptionSettings,
- ...ServersSettings
+ ...ServersSettings,
+ ...InboxSettings
];
// Thoughts:
diff --git a/apps/web/src/dialogs/settings/types.ts b/apps/web/src/dialogs/settings/types.ts
index 621264f92..345f1da30 100644
--- a/apps/web/src/dialogs/settings/types.ts
+++ b/apps/web/src/dialogs/settings/types.ts
@@ -39,7 +39,8 @@ export type SectionKeys =
| "support"
| "legal"
| "developer"
- | "about";
+ | "about"
+ | "inbox";
export type SectionGroupKeys =
| "account"
diff --git a/apps/web/src/interfaces/storage.ts b/apps/web/src/interfaces/storage.ts
index 6a9f06e5d..ecd0e3727 100644
--- a/apps/web/src/interfaces/storage.ts
+++ b/apps/web/src/interfaces/storage.ts
@@ -128,6 +128,10 @@ export class NNStorage implements IStorage {
return await NNCrypto.exportKey(password, salt);
}
+ async generateCryptoKeyPair() {
+ return await NNCrypto.exportKeyPair();
+ }
+
async hash(password: string, email: string): Promise {
return await NNCrypto.hash(password, `${APP_SALT}${email}`);
}
diff --git a/apps/web/src/stores/setting-store.ts b/apps/web/src/stores/setting-store.ts
index fc921b96f..05250e614 100644
--- a/apps/web/src/stores/setting-store.ts
+++ b/apps/web/src/stores/setting-store.ts
@@ -27,6 +27,7 @@ import { useEditorStore } from "./editor-store";
import { setDocumentTitle } from "../utils/dom";
import { TimeFormat } from "@notesnook/core";
import { Profile, TrashCleanupInterval } from "@notesnook/core";
+import { showToast } from "../utils/toast";
export const HostIds = [
"API_HOST",
@@ -89,6 +90,7 @@ class SettingStore extends BaseStore {
isFlatpak = false;
isSnap = false;
proxyRules?: string;
+ isInboxEnabled = false;
refresh = async () => {
this.set({
@@ -105,7 +107,8 @@ class SettingStore extends BaseStore {
customDns: await desktop?.integration.customDns.query(),
zoomFactor: await desktop?.integration.zoomFactor.query(),
autoUpdates: await desktop?.updater.autoUpdates.query(),
- proxyRules: await desktop?.integration.proxyRules.query()
+ proxyRules: await desktop?.integration.proxyRules.query(),
+ isInboxEnabled: await db.user.hasInboxKeys()
});
};
@@ -271,6 +274,25 @@ class SettingStore extends BaseStore {
this.set({ serverUrls: { ...serverUrls, ...urls } });
Config.set("serverUrls", { ...serverUrls, ...urls });
};
+
+ toggleInbox = async () => {
+ const { isInboxEnabled } = this.get();
+ const newState = !isInboxEnabled;
+
+ try {
+ if (newState) {
+ await db.user.getInboxKeys();
+ } else {
+ await db.user.discardInboxKeys();
+ }
+
+ this.set((state) => (state.isInboxEnabled = newState));
+ } catch (e) {
+ if (e instanceof Error) {
+ showToast("error", e.message);
+ }
+ }
+ };
}
const [useStore, store] = createStore(
diff --git a/packages/core/src/api/user-manager.ts b/packages/core/src/api/user-manager.ts
index 20aa9b2c8..1e7b98073 100644
--- a/packages/core/src/api/user-manager.ts
+++ b/packages/core/src/api/user-manager.ts
@@ -24,7 +24,7 @@ import TokenManager from "./token-manager.js";
import { EV, EVENTS } from "../common.js";
import { HealthCheck } from "./healthcheck.js";
import Database from "./index.js";
-import { SerializedKey } from "@notesnook/crypto";
+import { SerializedKeyPair, SerializedKey, Cipher } from "@notesnook/crypto";
import { logger } from "../logger.js";
const ENDPOINTS = {
@@ -44,6 +44,7 @@ class UserManager {
private tokenManager: TokenManager;
private cachedAttachmentKey?: SerializedKey;
private cachedMonographPasswordsKey?: SerializedKey;
+ private cachedInboxKeys?: SerializedKeyPair;
constructor(private readonly db: Database) {
this.tokenManager = new TokenManager(this.db.kv);
@@ -278,6 +279,7 @@ class UserManager {
logger.error(e, "Error logging out user.", { revoke, reason });
} finally {
this.cachedAttachmentKey = undefined;
+ this.cachedInboxKeys = undefined;
await this.db.reset();
EV.publish(EVENTS.userLoggedOut, reason);
EV.publish(EVENTS.appRefreshRequested);
@@ -420,13 +422,26 @@ class UserManager {
return { key, salt: user.salt };
}
- async getAttachmentsKey() {
- if (this.cachedAttachmentKey) return this.cachedAttachmentKey;
+ private async getUserKey(config: {
+ getCache: () => T | undefined;
+ setCache: (key: T) => void;
+ userProperty: keyof User;
+ generateKey: () => Promise;
+ errorContext: string;
+ decrypt: (user: User, userEncryptionKey: SerializedKey) => Promise;
+ encrypt: (
+ key: T,
+ userEncryptionKey: SerializedKey
+ ) => Promise>;
+ }): Promise {
+ const cachedKey = config.getCache();
+ if (cachedKey) return cachedKey;
+
try {
let user = await this.getUser();
if (!user) return;
- if (!user.attachmentsKey) {
+ if (!user[config.userProperty]) {
const token = await this.tokenManager.getAccessToken();
user = await http.get(`${constants.API_HOST}${ENDPOINTS.user}`, token);
}
@@ -435,74 +450,134 @@ class UserManager {
const userEncryptionKey = await this.getEncryptionKey();
if (!userEncryptionKey) return;
- if (!user.attachmentsKey) {
- const key = await this.db.crypto().generateRandomKey();
- user.attachmentsKey = await this.db
- .storage()
- .encrypt(userEncryptionKey, JSON.stringify(key));
-
- await this.updateUser({ attachmentsKey: user.attachmentsKey });
+ if (!user[config.userProperty]) {
+ const key = await config.generateKey();
+ const updatePayload = await config.encrypt(key, userEncryptionKey);
+ await this.updateUser(updatePayload);
return key;
}
- const plainData = await this.db
- .storage()
- .decrypt(userEncryptionKey, user.attachmentsKey);
- if (!plainData) return;
- this.cachedAttachmentKey = JSON.parse(plainData) as SerializedKey;
- return this.cachedAttachmentKey;
+ const decryptedKey = await config.decrypt(user, userEncryptionKey);
+ config.setCache(decryptedKey);
+ return decryptedKey;
} catch (e) {
- logger.error(e, "Could not get attachments encryption key.");
+ logger.error(e, `Could not get ${config.errorContext}.`);
if (e instanceof Error)
throw new Error(
- `Could not get attachments encryption key. Error: ${e.message}`
+ `Could not get ${config.errorContext}. Error: ${e.message}`
);
}
}
- async getMonographPasswordsKey() {
- if (this.cachedMonographPasswordsKey) {
- return this.cachedMonographPasswordsKey;
- }
-
- try {
- let user = await this.getUser();
- if (!user) return;
-
- if (!user.monographPasswordsKey) {
- const token = await this.tokenManager.getAccessToken();
- user = await http.get(`${constants.API_HOST}${ENDPOINTS.user}`, token);
- }
- if (!user) return;
-
- const userEncryptionKey = await this.getEncryptionKey();
- if (!userEncryptionKey) return;
-
- if (!user.monographPasswordsKey) {
- const key = await this.db.crypto().generateRandomKey();
- user.monographPasswordsKey = await this.db
+ async getAttachmentsKey() {
+ return this.getUserKey({
+ getCache: () => this.cachedAttachmentKey,
+ setCache: (key) => {
+ this.cachedAttachmentKey = key;
+ },
+ userProperty: "attachmentsKey",
+ generateKey: () => this.db.crypto().generateRandomKey(),
+ errorContext: "attachments encryption key",
+ encrypt: async (key, userEncryptionKey) => {
+ const encryptedKey = await this.db
.storage()
.encrypt(userEncryptionKey, JSON.stringify(key));
-
- await this.updateUser({
- monographPasswordsKey: user.monographPasswordsKey
- });
- return key;
+ return { attachmentsKey: encryptedKey };
+ },
+ decrypt: async (user, userEncryptionKey) => {
+ const encryptedKey = user.attachmentsKey as Cipher<"base64">;
+ const plainData = await this.db
+ .storage()
+ .decrypt(userEncryptionKey, encryptedKey);
+ if (!plainData) throw new Error("Failed to decrypt attachments key");
+ return JSON.parse(plainData) as SerializedKey;
}
+ });
+ }
- const plainData = await this.db
- .storage()
- .decrypt(userEncryptionKey, user.monographPasswordsKey);
- if (!plainData) return;
- this.cachedMonographPasswordsKey = JSON.parse(plainData) as SerializedKey;
- return this.cachedMonographPasswordsKey;
- } catch (e) {
- logger.error(e, "Could not get monographs encryption key.");
- if (e instanceof Error)
- throw new Error(
- `Could not get monographs encryption key. Error: ${e.message}`
- );
- }
+ async getMonographPasswordsKey() {
+ return this.getUserKey({
+ getCache: () => this.cachedMonographPasswordsKey,
+ setCache: (key) => {
+ this.cachedMonographPasswordsKey = key;
+ },
+ userProperty: "monographPasswordsKey",
+ generateKey: () => this.db.crypto().generateRandomKey(),
+ errorContext: "monographs encryption key",
+ encrypt: async (key, userEncryptionKey) => {
+ const encryptedKey = await this.db
+ .storage()
+ .encrypt(userEncryptionKey, JSON.stringify(key));
+ return { monographPasswordsKey: encryptedKey };
+ },
+ decrypt: async (user, userEncryptionKey) => {
+ const encryptedKey = user.monographPasswordsKey as Cipher<"base64">;
+ const plainData = await this.db
+ .storage()
+ .decrypt(userEncryptionKey, encryptedKey);
+ if (!plainData)
+ throw new Error("Failed to decrypt monograph passwords key");
+ return JSON.parse(plainData) as SerializedKey;
+ }
+ });
+ }
+
+ async getInboxKeys() {
+ return this.getUserKey({
+ getCache: () => this.cachedInboxKeys,
+ setCache: (key) => {
+ this.cachedInboxKeys = key;
+ },
+ userProperty: "inboxKeys",
+ generateKey: () => this.db.crypto().generateCryptoKeyPair(),
+ errorContext: "inbox encryption keys",
+ encrypt: async (keys, userEncryptionKey) => {
+ const encryptedPrivateKey = await this.db
+ .storage()
+ .encrypt(userEncryptionKey, JSON.stringify(keys.privateKey));
+ return {
+ inboxKeys: {
+ public: keys.publicKey,
+ private: encryptedPrivateKey
+ }
+ };
+ },
+ decrypt: async (user, userEncryptionKey) => {
+ if (!user.inboxKeys) throw new Error("Inbox keys not found");
+ const decryptedPrivateKey = await this.db
+ .storage()
+ .decrypt(userEncryptionKey, user.inboxKeys.private);
+ return {
+ publicKey: user.inboxKeys.public,
+ privateKey: JSON.parse(decryptedPrivateKey)
+ };
+ }
+ });
+ }
+
+ async hasInboxKeys() {
+ if (this.cachedInboxKeys) return true;
+
+ let user = await this.getUser();
+ if (!user) return false;
+
+ return !!user.inboxKeys;
+ }
+
+ async discardInboxKeys() {
+ this.cachedInboxKeys = undefined;
+
+ const user = await this.getUser();
+ if (!user) return;
+
+ const token = await this.tokenManager.getAccessToken();
+ await http.patch.json(
+ `${constants.API_HOST}${ENDPOINTS.user}`,
+ { inboxKeys: { public: null, private: null } },
+ token
+ );
+
+ await this.setUser({ ...user, inboxKeys: undefined });
}
async sendVerificationEmail(newEmail?: string) {
@@ -616,6 +691,16 @@ class UserManager {
.encrypt(userEncryptionKey, JSON.stringify(monographPasswordsKey));
updateUserPayload.monographPasswordsKey = user.monographPasswordsKey;
}
+ const inboxKeys = await this.getInboxKeys();
+ if (inboxKeys) {
+ user.inboxKeys = {
+ public: inboxKeys.publicKey,
+ private: await this.db
+ .storage()
+ .encrypt(userEncryptionKey, JSON.stringify(inboxKeys.privateKey))
+ };
+ updateUserPayload.inboxKeys = user.inboxKeys;
+ }
if (Object.keys(updateUserPayload).length > 0) {
await this.updateUser(updateUserPayload);
}
diff --git a/packages/core/src/interfaces.ts b/packages/core/src/interfaces.ts
index 177e2aa42..c00d66dae 100644
--- a/packages/core/src/interfaces.ts
+++ b/packages/core/src/interfaces.ts
@@ -17,7 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { Cipher, DataFormat, SerializedKey } from "@notesnook/crypto";
+import {
+ Cipher,
+ DataFormat,
+ SerializedKey,
+ SerializedKeyPair
+} from "@notesnook/crypto";
import { KVStorage } from "./database/kv.js";
import { ConfigStorage } from "./database/config.js";
@@ -65,6 +70,7 @@ export interface IStorage {
): Promise;
getCryptoKey(): Promise;
generateCryptoKey(password: string, salt?: string): Promise;
+ generateCryptoKeyPair(): Promise;
generateCryptoKeyFallback(
password: string,
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 3d35ee766..827656fcb 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -557,6 +557,7 @@ export type User = {
salt: string;
attachmentsKey?: Cipher<"base64">;
monographPasswordsKey?: Cipher<"base64">;
+ inboxKeys?: { public: string; private: Cipher<"base64"> };
marketingConsent?: boolean;
mfa: {
isEnabled: boolean;
diff --git a/packages/core/src/utils/crypto.ts b/packages/core/src/utils/crypto.ts
index 96b30eee4..b6e65bc4a 100644
--- a/packages/core/src/utils/crypto.ts
+++ b/packages/core/src/utils/crypto.ts
@@ -29,6 +29,10 @@ export class Crypto {
const password = passwordBytes.toString("base64");
return await this.storage().generateCryptoKey(password);
}
+
+ async generateCryptoKeyPair() {
+ return await this.storage().generateCryptoKeyPair();
+ }
}
export function isCipher(item: any): item is Cipher<"base64"> {
diff --git a/packages/crypto/src/index.ts b/packages/crypto/src/index.ts
index 00f93f96d..03d12e146 100644
--- a/packages/crypto/src/index.ts
+++ b/packages/crypto/src/index.ts
@@ -29,7 +29,9 @@ import {
Input,
Output,
DataFormat,
- SerializedKey
+ SerializedKey,
+ SerializedKeyPair,
+ EncryptionKeyPair
} from "./types.js";
export class NNCrypto implements INNCrypto {
@@ -104,11 +106,21 @@ export class NNCrypto implements INNCrypto {
return KeyUtils.deriveKey(this.sodium, password, salt);
}
+ async deriveKeyPair(): Promise {
+ await this.init();
+ return KeyUtils.deriveKeyPair(this.sodium);
+ }
+
async exportKey(password: string, salt?: string): Promise {
await this.init();
return KeyUtils.exportKey(this.sodium, password, salt);
}
+ async exportKeyPair(): Promise {
+ await this.init();
+ return KeyUtils.exportKeyPair(this.sodium);
+ }
+
async createEncryptionStream(key: SerializedKey) {
await this.init();
return Encryption.createStream(this.sodium, key);
diff --git a/packages/crypto/src/interfaces.ts b/packages/crypto/src/interfaces.ts
index 9bcd9640a..a508a8a7d 100644
--- a/packages/crypto/src/interfaces.ts
+++ b/packages/crypto/src/interfaces.ts
@@ -24,7 +24,9 @@ import {
SerializedKey,
Chunk,
Output,
- Input
+ Input,
+ EncryptionKeyPair,
+ SerializedKeyPair
} from "./types.js";
export interface IStreamable {
@@ -63,8 +65,12 @@ export interface INNCrypto {
deriveKey(password: string, salt?: string): Promise;
+ deriveKeyPair(): Promise;
+
exportKey(password: string, salt?: string): Promise;
+ exportKeyPair(): Promise;
+
createEncryptionStream(
key: SerializedKey
): Promise<{ iv: string; stream: TransformStream }>;
diff --git a/packages/crypto/src/keyutils.ts b/packages/crypto/src/keyutils.ts
index 0cc20bd7d..9a106c274 100644
--- a/packages/crypto/src/keyutils.ts
+++ b/packages/crypto/src/keyutils.ts
@@ -18,7 +18,12 @@ along with this program. If not, see .
*/
import { ISodium } from "@notesnook/sodium";
-import { EncryptionKey, SerializedKey } from "./types.js";
+import {
+ EncryptionKey,
+ EncryptionKeyPair,
+ SerializedKey,
+ SerializedKeyPair
+} from "./types.js";
export default class KeyUtils {
static deriveKey(
@@ -51,6 +56,14 @@ export default class KeyUtils {
};
}
+ static deriveKeyPair(sodium: ISodium): EncryptionKeyPair {
+ const keypair = sodium.crypto_box_keypair();
+ return {
+ publicKey: keypair.publicKey,
+ privateKey: keypair.privateKey
+ };
+ }
+
static exportKey(
sodium: ISodium,
password: string,
@@ -60,6 +73,14 @@ export default class KeyUtils {
return { key: sodium.to_base64(key), salt: keySalt };
}
+ static exportKeyPair(sodium: ISodium): SerializedKeyPair {
+ const { publicKey, privateKey } = this.deriveKeyPair(sodium);
+ return {
+ publicKey: sodium.to_base64(publicKey),
+ privateKey: sodium.to_base64(privateKey)
+ };
+ }
+
/**
* Takes in either a password or a serialized encryption key
* and spits out a key that can be directly used for encryption/decryption.
diff --git a/packages/crypto/src/types.ts b/packages/crypto/src/types.ts
index 8031364a9..1fda92cd7 100644
--- a/packages/crypto/src/types.ts
+++ b/packages/crypto/src/types.ts
@@ -49,3 +49,13 @@ export type Chunk = {
data: Uint8Array;
final: boolean;
};
+
+export type EncryptionKeyPair = {
+ publicKey: Uint8Array;
+ privateKey: Uint8Array;
+};
+
+export type SerializedKeyPair = {
+ publicKey: string;
+ privateKey: string;
+};
diff --git a/packages/sodium/src/browser.ts b/packages/sodium/src/browser.ts
index 5a2528320..6f2abaf39 100644
--- a/packages/sodium/src/browser.ts
+++ b/packages/sodium/src/browser.ts
@@ -118,6 +118,9 @@ 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;
+ }
}
function convertVariant(variant: base64_variants): sodium.base64_variants {
diff --git a/packages/sodium/src/node.ts b/packages/sodium/src/node.ts
index 9379b1548..d90d44db4 100644
--- a/packages/sodium/src/node.ts
+++ b/packages/sodium/src/node.ts
@@ -46,14 +46,29 @@ import {
crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES,
crypto_secretstream_xchacha20poly1305_TAG_FINAL,
- crypto_secretstream_xchacha20poly1305_TAG_MESSAGE
+ crypto_secretstream_xchacha20poly1305_TAG_MESSAGE,
+ crypto_box_keypair as sodium_native_crypto_box_keypair,
+ crypto_box_PUBLICKEYBYTES,
+ crypto_box_SECRETKEYBYTES
} from "sodium-native";
import { Buffer } from "node:buffer";
import { base64_variants, ISodium } from "./types";
export type Uint8ArrayOutputFormat = "uint8array";
export type StringOutputFormat = "text" | "hex" | "base64";
+export type KeyType = "curve25519" | "ed25519" | "x25519";
export type StateAddress = { name: string };
+export interface KeyPair {
+ keyType: KeyType;
+ privateKey: Uint8Array;
+ publicKey: Uint8Array;
+}
+
+export interface StringKeyPair {
+ keyType: KeyType;
+ privateKey: string;
+ publicKey: string;
+}
export interface MessageTag {
message: Uint8Array;
tag: number;
@@ -329,6 +344,39 @@ 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 randombytes_buf(
length: number,
outputFormat?: Uint8ArrayOutputFormat | null
@@ -391,6 +439,10 @@ 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
| undefined
@@ -539,6 +591,9 @@ 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;
+ }
}
export { base64_variants, type ISodium };
diff --git a/packages/sodium/src/types.ts b/packages/sodium/src/types.ts
index 7ff807104..515b824d3 100644
--- a/packages/sodium/src/types.ts
+++ b/packages/sodium/src/types.ts
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import type sodium from "libsodium-wrappers-sumo";
+import sodium from "libsodium-wrappers-sumo";
export enum base64_variants {
ORIGINAL = 1,
@@ -61,4 +61,5 @@ 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;
}