mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-18 20:49:36 +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,
|
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);
|
||||||
|
|||||||
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,
|
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:
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ export type SectionKeys =
|
|||||||
| "support"
|
| "support"
|
||||||
| "legal"
|
| "legal"
|
||||||
| "developer"
|
| "developer"
|
||||||
| "about";
|
| "about"
|
||||||
|
| "inbox";
|
||||||
|
|
||||||
export type SectionGroupKeys =
|
export type SectionGroupKeys =
|
||||||
| "account"
|
| "account"
|
||||||
|
|||||||
@@ -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}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"> {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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> }>;
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user