From 251145ea74b7b62d176a96e803f8d2283d694dd8 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Tue, 9 Sep 2025 22:12:44 +0500 Subject: [PATCH] global: add enable inbox setting to generate/discard inbox key pair (#8527) Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- apps/web/src/components/icons/index.tsx | 4 +- .../src/dialogs/settings/inbox-settings.ts | 46 ++++ apps/web/src/dialogs/settings/index.tsx | 13 +- apps/web/src/dialogs/settings/types.ts | 3 +- apps/web/src/interfaces/storage.ts | 4 + apps/web/src/stores/setting-store.ts | 24 ++- packages/core/src/api/user-manager.ts | 201 +++++++++++++----- packages/core/src/interfaces.ts | 8 +- packages/core/src/types.ts | 1 + packages/core/src/utils/crypto.ts | 4 + packages/crypto/src/index.ts | 14 +- packages/crypto/src/interfaces.ts | 8 +- packages/crypto/src/keyutils.ts | 23 +- packages/crypto/src/types.ts | 10 + packages/sodium/src/browser.ts | 3 + packages/sodium/src/node.ts | 57 ++++- packages/sodium/src/types.ts | 3 +- 17 files changed, 357 insertions(+), 69 deletions(-) create mode 100644 apps/web/src/dialogs/settings/inbox-settings.ts 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; }