global: add enable inbox setting to generate/discard inbox key pair (#8527)

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
01zulfi
2025-09-09 22:12:44 +05:00
committed by GitHub
parent 5e4972c1d0
commit 251145ea74
17 changed files with 357 additions and 69 deletions

View File

@@ -230,7 +230,8 @@ import {
mdiHamburger, mdiHamburger,
mdiNotePlus, mdiNotePlus,
mdiNoteEditOutline, mdiNoteEditOutline,
mdiArrowUp mdiArrowUp,
mdiInbox
} from "@mdi/js"; } from "@mdi/js";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme"; import { Theme } from "@notesnook/theme";
@@ -584,3 +585,4 @@ export const ColorRemove = createIcon(mdiCloseCircleOutline);
export const ExpandSidebar = createIcon(mdiArrowCollapseRight); export const ExpandSidebar = createIcon(mdiArrowCollapseRight);
export const HamburgerMenu = createIcon(mdiMenu); export const HamburgerMenu = createIcon(mdiMenu);
export const ArrowUp = createIcon(mdiArrowUp); export const ArrowUp = createIcon(mdiArrowUp);
export const Inbox = createIcon(mdiInbox);

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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()
}
]
}
]
}
];

View File

@@ -38,7 +38,8 @@ import {
Pro, Pro,
Servers, Servers,
ShieldLock, ShieldLock,
Sync Sync,
Inbox
} from "../../components/icons"; } from "../../components/icons";
import NavigationItem from "../../components/navigation-menu/navigation-item"; import NavigationItem from "../../components/navigation-menu/navigation-item";
import { FlexScrollContainer } from "../../components/scroll-container"; import { FlexScrollContainer } from "../../components/scroll-container";
@@ -78,6 +79,7 @@ import { BaseDialogProps, DialogManager } from "../../common/dialog-manager";
import { ServersSettings } from "./servers-settings"; import { ServersSettings } from "./servers-settings";
import { strings } from "@notesnook/intl"; import { strings } from "@notesnook/intl";
import { mdToHtml } from "../../utils/md"; import { mdToHtml } from "../../utils/md";
import { InboxSettings } from "./inbox-settings";
type SettingsDialogProps = BaseDialogProps<false> & { type SettingsDialogProps = BaseDialogProps<false> & {
activeSection?: SectionKeys; activeSection?: SectionKeys;
@@ -106,6 +108,12 @@ const sectionGroups: SectionGroup[] = [
title: strings.sync(), title: strings.sync(),
icon: Sync, icon: Sync,
isHidden: () => !useUserStore.getState().isLoggedIn isHidden: () => !useUserStore.getState().isLoggedIn
},
{
key: "inbox",
title: "Inbox",
icon: Inbox,
isHidden: () => true // hidden until complete
} }
] ]
}, },
@@ -176,7 +184,8 @@ const SettingsGroups = [
...SupportSettings, ...SupportSettings,
...AboutSettings, ...AboutSettings,
...SubscriptionSettings, ...SubscriptionSettings,
...ServersSettings ...ServersSettings,
...InboxSettings
]; ];
// Thoughts: // Thoughts:

View File

@@ -39,7 +39,8 @@ export type SectionKeys =
| "support" | "support"
| "legal" | "legal"
| "developer" | "developer"
| "about"; | "about"
| "inbox";
export type SectionGroupKeys = export type SectionGroupKeys =
| "account" | "account"

View File

@@ -128,6 +128,10 @@ export class NNStorage implements IStorage {
return await NNCrypto.exportKey(password, salt); return await NNCrypto.exportKey(password, salt);
} }
async generateCryptoKeyPair() {
return await NNCrypto.exportKeyPair();
}
async hash(password: string, email: string): Promise<string> { async hash(password: string, email: string): Promise<string> {
return await NNCrypto.hash(password, `${APP_SALT}${email}`); return await NNCrypto.hash(password, `${APP_SALT}${email}`);
} }

View File

@@ -27,6 +27,7 @@ import { useEditorStore } from "./editor-store";
import { setDocumentTitle } from "../utils/dom"; import { setDocumentTitle } from "../utils/dom";
import { TimeFormat } from "@notesnook/core"; import { TimeFormat } from "@notesnook/core";
import { Profile, TrashCleanupInterval } from "@notesnook/core"; import { Profile, TrashCleanupInterval } from "@notesnook/core";
import { showToast } from "../utils/toast";
export const HostIds = [ export const HostIds = [
"API_HOST", "API_HOST",
@@ -89,6 +90,7 @@ class SettingStore extends BaseStore<SettingStore> {
isFlatpak = false; isFlatpak = false;
isSnap = false; isSnap = false;
proxyRules?: string; proxyRules?: string;
isInboxEnabled = false;
refresh = async () => { refresh = async () => {
this.set({ this.set({
@@ -105,7 +107,8 @@ class SettingStore extends BaseStore<SettingStore> {
customDns: await desktop?.integration.customDns.query(), customDns: await desktop?.integration.customDns.query(),
zoomFactor: await desktop?.integration.zoomFactor.query(), zoomFactor: await desktop?.integration.zoomFactor.query(),
autoUpdates: await desktop?.updater.autoUpdates.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<SettingStore> {
this.set({ serverUrls: { ...serverUrls, ...urls } }); this.set({ serverUrls: { ...serverUrls, ...urls } });
Config.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<SettingStore>( const [useStore, store] = createStore<SettingStore>(

View File

@@ -24,7 +24,7 @@ import TokenManager from "./token-manager.js";
import { EV, EVENTS } from "../common.js"; import { EV, EVENTS } from "../common.js";
import { HealthCheck } from "./healthcheck.js"; import { HealthCheck } from "./healthcheck.js";
import Database from "./index.js"; import Database from "./index.js";
import { SerializedKey } from "@notesnook/crypto"; import { SerializedKeyPair, SerializedKey, Cipher } from "@notesnook/crypto";
import { logger } from "../logger.js"; import { logger } from "../logger.js";
const ENDPOINTS = { const ENDPOINTS = {
@@ -44,6 +44,7 @@ class UserManager {
private tokenManager: TokenManager; private tokenManager: TokenManager;
private cachedAttachmentKey?: SerializedKey; private cachedAttachmentKey?: SerializedKey;
private cachedMonographPasswordsKey?: SerializedKey; private cachedMonographPasswordsKey?: SerializedKey;
private cachedInboxKeys?: SerializedKeyPair;
constructor(private readonly db: Database) { constructor(private readonly db: Database) {
this.tokenManager = new TokenManager(this.db.kv); this.tokenManager = new TokenManager(this.db.kv);
@@ -278,6 +279,7 @@ class UserManager {
logger.error(e, "Error logging out user.", { revoke, reason }); logger.error(e, "Error logging out user.", { revoke, reason });
} finally { } finally {
this.cachedAttachmentKey = undefined; this.cachedAttachmentKey = undefined;
this.cachedInboxKeys = undefined;
await this.db.reset(); await this.db.reset();
EV.publish(EVENTS.userLoggedOut, reason); EV.publish(EVENTS.userLoggedOut, reason);
EV.publish(EVENTS.appRefreshRequested); EV.publish(EVENTS.appRefreshRequested);
@@ -420,13 +422,26 @@ class UserManager {
return { key, salt: user.salt }; return { key, salt: user.salt };
} }
async getAttachmentsKey() { private async getUserKey<T>(config: {
if (this.cachedAttachmentKey) return this.cachedAttachmentKey; getCache: () => T | undefined;
setCache: (key: T) => void;
userProperty: keyof User;
generateKey: () => Promise<T>;
errorContext: string;
decrypt: (user: User, userEncryptionKey: SerializedKey) => Promise<T>;
encrypt: (
key: T,
userEncryptionKey: SerializedKey
) => Promise<Partial<User>>;
}): Promise<T | undefined> {
const cachedKey = config.getCache();
if (cachedKey) return cachedKey;
try { try {
let user = await this.getUser(); let user = await this.getUser();
if (!user) return; if (!user) return;
if (!user.attachmentsKey) { if (!user[config.userProperty]) {
const token = await this.tokenManager.getAccessToken(); const token = await this.tokenManager.getAccessToken();
user = await http.get(`${constants.API_HOST}${ENDPOINTS.user}`, token); user = await http.get(`${constants.API_HOST}${ENDPOINTS.user}`, token);
} }
@@ -435,74 +450,134 @@ class UserManager {
const userEncryptionKey = await this.getEncryptionKey(); const userEncryptionKey = await this.getEncryptionKey();
if (!userEncryptionKey) return; if (!userEncryptionKey) return;
if (!user.attachmentsKey) { if (!user[config.userProperty]) {
const key = await this.db.crypto().generateRandomKey(); const key = await config.generateKey();
user.attachmentsKey = await this.db const updatePayload = await config.encrypt(key, userEncryptionKey);
.storage() await this.updateUser(updatePayload);
.encrypt(userEncryptionKey, JSON.stringify(key));
await this.updateUser({ attachmentsKey: user.attachmentsKey });
return key; return key;
} }
const plainData = await this.db const decryptedKey = await config.decrypt(user, userEncryptionKey);
.storage() config.setCache(decryptedKey);
.decrypt(userEncryptionKey, user.attachmentsKey); return decryptedKey;
if (!plainData) return;
this.cachedAttachmentKey = JSON.parse(plainData) as SerializedKey;
return this.cachedAttachmentKey;
} catch (e) { } catch (e) {
logger.error(e, "Could not get attachments encryption key."); logger.error(e, `Could not get ${config.errorContext}.`);
if (e instanceof Error) if (e instanceof Error)
throw new Error( throw new Error(
`Could not get attachments encryption key. Error: ${e.message}` `Could not get ${config.errorContext}. Error: ${e.message}`
); );
} }
} }
async getMonographPasswordsKey() { async getAttachmentsKey() {
if (this.cachedMonographPasswordsKey) { return this.getUserKey<SerializedKey>({
return this.cachedMonographPasswordsKey; getCache: () => this.cachedAttachmentKey,
} setCache: (key) => {
this.cachedAttachmentKey = key;
try { },
let user = await this.getUser(); userProperty: "attachmentsKey",
if (!user) return; generateKey: () => this.db.crypto().generateRandomKey(),
errorContext: "attachments encryption key",
if (!user.monographPasswordsKey) { encrypt: async (key, userEncryptionKey) => {
const token = await this.tokenManager.getAccessToken(); const encryptedKey = await this.db
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
.storage() .storage()
.encrypt(userEncryptionKey, JSON.stringify(key)); .encrypt(userEncryptionKey, JSON.stringify(key));
return { attachmentsKey: encryptedKey };
await this.updateUser({ },
monographPasswordsKey: user.monographPasswordsKey decrypt: async (user, userEncryptionKey) => {
}); const encryptedKey = user.attachmentsKey as Cipher<"base64">;
return key; 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 async getMonographPasswordsKey() {
.storage() return this.getUserKey<SerializedKey>({
.decrypt(userEncryptionKey, user.monographPasswordsKey); getCache: () => this.cachedMonographPasswordsKey,
if (!plainData) return; setCache: (key) => {
this.cachedMonographPasswordsKey = JSON.parse(plainData) as SerializedKey; this.cachedMonographPasswordsKey = key;
return this.cachedMonographPasswordsKey; },
} catch (e) { userProperty: "monographPasswordsKey",
logger.error(e, "Could not get monographs encryption key."); generateKey: () => this.db.crypto().generateRandomKey(),
if (e instanceof Error) errorContext: "monographs encryption key",
throw new Error( encrypt: async (key, userEncryptionKey) => {
`Could not get monographs encryption key. Error: ${e.message}` 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<SerializedKeyPair>({
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) { async sendVerificationEmail(newEmail?: string) {
@@ -616,6 +691,16 @@ class UserManager {
.encrypt(userEncryptionKey, JSON.stringify(monographPasswordsKey)); .encrypt(userEncryptionKey, JSON.stringify(monographPasswordsKey));
updateUserPayload.monographPasswordsKey = user.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) { if (Object.keys(updateUserPayload).length > 0) {
await this.updateUser(updateUserPayload); await this.updateUser(updateUserPayload);
} }

View File

@@ -17,7 +17,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { Cipher, DataFormat, SerializedKey } from "@notesnook/crypto"; import {
Cipher,
DataFormat,
SerializedKey,
SerializedKeyPair
} from "@notesnook/crypto";
import { KVStorage } from "./database/kv.js"; import { KVStorage } from "./database/kv.js";
import { ConfigStorage } from "./database/config.js"; import { ConfigStorage } from "./database/config.js";
@@ -65,6 +70,7 @@ export interface IStorage {
): Promise<string>; ): Promise<string>;
getCryptoKey(): Promise<string | undefined>; getCryptoKey(): Promise<string | undefined>;
generateCryptoKey(password: string, salt?: string): Promise<SerializedKey>; generateCryptoKey(password: string, salt?: string): Promise<SerializedKey>;
generateCryptoKeyPair(): Promise<SerializedKeyPair>;
generateCryptoKeyFallback( generateCryptoKeyFallback(
password: string, password: string,

View File

@@ -557,6 +557,7 @@ export type User = {
salt: string; salt: string;
attachmentsKey?: Cipher<"base64">; attachmentsKey?: Cipher<"base64">;
monographPasswordsKey?: Cipher<"base64">; monographPasswordsKey?: Cipher<"base64">;
inboxKeys?: { public: string; private: Cipher<"base64"> };
marketingConsent?: boolean; marketingConsent?: boolean;
mfa: { mfa: {
isEnabled: boolean; isEnabled: boolean;

View File

@@ -29,6 +29,10 @@ export class Crypto {
const password = passwordBytes.toString("base64"); const password = passwordBytes.toString("base64");
return await this.storage().generateCryptoKey(password); return await this.storage().generateCryptoKey(password);
} }
async generateCryptoKeyPair() {
return await this.storage().generateCryptoKeyPair();
}
} }
export function isCipher(item: any): item is Cipher<"base64"> { export function isCipher(item: any): item is Cipher<"base64"> {

View File

@@ -29,7 +29,9 @@ import {
Input, Input,
Output, Output,
DataFormat, DataFormat,
SerializedKey SerializedKey,
SerializedKeyPair,
EncryptionKeyPair
} from "./types.js"; } from "./types.js";
export class NNCrypto implements INNCrypto { export class NNCrypto implements INNCrypto {
@@ -104,11 +106,21 @@ export class NNCrypto implements INNCrypto {
return KeyUtils.deriveKey(this.sodium, password, salt); return KeyUtils.deriveKey(this.sodium, password, salt);
} }
async deriveKeyPair(): Promise<EncryptionKeyPair> {
await this.init();
return KeyUtils.deriveKeyPair(this.sodium);
}
async exportKey(password: string, salt?: string): Promise<SerializedKey> { async exportKey(password: string, salt?: string): Promise<SerializedKey> {
await this.init(); await this.init();
return KeyUtils.exportKey(this.sodium, password, salt); return KeyUtils.exportKey(this.sodium, password, salt);
} }
async exportKeyPair(): Promise<SerializedKeyPair> {
await this.init();
return KeyUtils.exportKeyPair(this.sodium);
}
async createEncryptionStream(key: SerializedKey) { async createEncryptionStream(key: SerializedKey) {
await this.init(); await this.init();
return Encryption.createStream(this.sodium, key); return Encryption.createStream(this.sodium, key);

View File

@@ -24,7 +24,9 @@ import {
SerializedKey, SerializedKey,
Chunk, Chunk,
Output, Output,
Input Input,
EncryptionKeyPair,
SerializedKeyPair
} from "./types.js"; } from "./types.js";
export interface IStreamable { export interface IStreamable {
@@ -63,8 +65,12 @@ export interface INNCrypto {
deriveKey(password: string, salt?: string): Promise<EncryptionKey>; deriveKey(password: string, salt?: string): Promise<EncryptionKey>;
deriveKeyPair(): Promise<EncryptionKeyPair>;
exportKey(password: string, salt?: string): Promise<SerializedKey>; exportKey(password: string, salt?: string): Promise<SerializedKey>;
exportKeyPair(): Promise<SerializedKeyPair>;
createEncryptionStream( createEncryptionStream(
key: SerializedKey key: SerializedKey
): Promise<{ iv: string; stream: TransformStream<Chunk, Uint8Array> }>; ): Promise<{ iv: string; stream: TransformStream<Chunk, Uint8Array> }>;

View File

@@ -18,7 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { ISodium } from "@notesnook/sodium"; import { ISodium } from "@notesnook/sodium";
import { EncryptionKey, SerializedKey } from "./types.js"; import {
EncryptionKey,
EncryptionKeyPair,
SerializedKey,
SerializedKeyPair
} from "./types.js";
export default class KeyUtils { export default class KeyUtils {
static deriveKey( 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( static exportKey(
sodium: ISodium, sodium: ISodium,
password: string, password: string,
@@ -60,6 +73,14 @@ export default class KeyUtils {
return { key: sodium.to_base64(key), salt: keySalt }; 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 * Takes in either a password or a serialized encryption key
* and spits out a key that can be directly used for encryption/decryption. * and spits out a key that can be directly used for encryption/decryption.

View File

@@ -49,3 +49,13 @@ export type Chunk = {
data: Uint8Array; data: Uint8Array;
final: boolean; final: boolean;
}; };
export type EncryptionKeyPair = {
publicKey: Uint8Array;
privateKey: Uint8Array;
};
export type SerializedKeyPair = {
publicKey: string;
privateKey: string;
};

View File

@@ -118,6 +118,9 @@ export class Sodium implements ISodium {
get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE() { get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE() {
return sodium.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 { function convertVariant(variant: base64_variants): sodium.base64_variants {

View File

@@ -46,14 +46,29 @@ import {
crypto_aead_xchacha20poly1305_ietf_KEYBYTES, crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, crypto_aead_xchacha20poly1305_ietf_NPUBBYTES,
crypto_secretstream_xchacha20poly1305_TAG_FINAL, 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"; } from "sodium-native";
import { Buffer } from "node:buffer"; import { Buffer } from "node:buffer";
import { base64_variants, ISodium } from "./types"; import { base64_variants, ISodium } from "./types";
export type Uint8ArrayOutputFormat = "uint8array"; export type Uint8ArrayOutputFormat = "uint8array";
export type StringOutputFormat = "text" | "hex" | "base64"; export type StringOutputFormat = "text" | "hex" | "base64";
export type KeyType = "curve25519" | "ed25519" | "x25519";
export type StateAddress = { name: string }; 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 { export interface MessageTag {
message: Uint8Array; message: Uint8Array;
tag: number; tag: number;
@@ -329,6 +344,39 @@ function crypto_secretstream_xchacha20poly1305_pull(
return { message, tag: tag.readUInt8() } as MessageTag | StringMessageTag; 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( function randombytes_buf(
length: number, length: number,
outputFormat?: Uint8ArrayOutputFormat | null 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 ToBufferInput = string | Uint8Array | null | undefined;
type ToBufferResult<TInput extends ToBufferInput> = TInput extends type ToBufferResult<TInput extends ToBufferInput> = TInput extends
| undefined | undefined
@@ -539,6 +591,9 @@ export class Sodium implements ISodium {
get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE() { get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE() {
return crypto_secretstream_xchacha20poly1305_TAG_MESSAGE; return crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
} }
get crypto_box_keypair() {
return crypto_box_keypair;
}
} }
export { base64_variants, type ISodium }; export { base64_variants, type ISodium };

View File

@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import type sodium from "libsodium-wrappers-sumo"; import sodium from "libsodium-wrappers-sumo";
export enum base64_variants { export enum base64_variants {
ORIGINAL = 1, ORIGINAL = 1,
@@ -61,4 +61,5 @@ export interface ISodium {
get crypto_aead_xchacha20poly1305_ietf_NPUBBYTES(): typeof sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES; 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_FINAL(): typeof sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL;
get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE(): typeof sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE; get crypto_secretstream_xchacha20poly1305_TAG_MESSAGE(): typeof sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
get crypto_box_keypair(): typeof sodium.crypto_box_keypair;
} }