mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-16 19:57:52 +01:00
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:
@@ -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);
|
||||
|
||||
46
apps/web/src/dialogs/settings/inbox-settings.ts
Normal file
46
apps/web/src/dialogs/settings/inbox-settings.ts
Normal 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()
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -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<false> & {
|
||||
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:
|
||||
|
||||
@@ -39,7 +39,8 @@ export type SectionKeys =
|
||||
| "support"
|
||||
| "legal"
|
||||
| "developer"
|
||||
| "about";
|
||||
| "about"
|
||||
| "inbox";
|
||||
|
||||
export type SectionGroupKeys =
|
||||
| "account"
|
||||
|
||||
@@ -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<string> {
|
||||
return await NNCrypto.hash(password, `${APP_SALT}${email}`);
|
||||
}
|
||||
|
||||
@@ -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<SettingStore> {
|
||||
isFlatpak = false;
|
||||
isSnap = false;
|
||||
proxyRules?: string;
|
||||
isInboxEnabled = false;
|
||||
|
||||
refresh = async () => {
|
||||
this.set({
|
||||
@@ -105,7 +107,8 @@ class SettingStore extends BaseStore<SettingStore> {
|
||||
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<SettingStore> {
|
||||
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<SettingStore>(
|
||||
|
||||
@@ -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<T>(config: {
|
||||
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 {
|
||||
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<SerializedKey>({
|
||||
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<SerializedKey>({
|
||||
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<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) {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
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<string>;
|
||||
getCryptoKey(): Promise<string | undefined>;
|
||||
generateCryptoKey(password: string, salt?: string): Promise<SerializedKey>;
|
||||
generateCryptoKeyPair(): Promise<SerializedKeyPair>;
|
||||
|
||||
generateCryptoKeyFallback(
|
||||
password: string,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"> {
|
||||
|
||||
@@ -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<EncryptionKeyPair> {
|
||||
await this.init();
|
||||
return KeyUtils.deriveKeyPair(this.sodium);
|
||||
}
|
||||
|
||||
async exportKey(password: string, salt?: string): Promise<SerializedKey> {
|
||||
await this.init();
|
||||
return KeyUtils.exportKey(this.sodium, password, salt);
|
||||
}
|
||||
|
||||
async exportKeyPair(): Promise<SerializedKeyPair> {
|
||||
await this.init();
|
||||
return KeyUtils.exportKeyPair(this.sodium);
|
||||
}
|
||||
|
||||
async createEncryptionStream(key: SerializedKey) {
|
||||
await this.init();
|
||||
return Encryption.createStream(this.sodium, key);
|
||||
|
||||
@@ -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<EncryptionKey>;
|
||||
|
||||
deriveKeyPair(): Promise<EncryptionKeyPair>;
|
||||
|
||||
exportKey(password: string, salt?: string): Promise<SerializedKey>;
|
||||
|
||||
exportKeyPair(): Promise<SerializedKeyPair>;
|
||||
|
||||
createEncryptionStream(
|
||||
key: SerializedKey
|
||||
): Promise<{ iv: string; stream: TransformStream<Chunk, Uint8Array> }>;
|
||||
|
||||
@@ -18,7 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ToBufferInput> = 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 };
|
||||
|
||||
@@ -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/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user