vericrypt: Update to support DEK. (#10005)

* vericrypt: Update to support DEK.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* vericrypt: Clear account data on restart.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Update apps/vericrypt/src/components/decrypted-result.tsx

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Update data encryption documentation to be accurate with the current client versions.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: Explain what happens better.

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Fix typo in encryption key generation section

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* Update docs/help/contents/how-is-my-data-encrypted.md

Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

* docs: update wording

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>

---------

Signed-off-by: Chloe Oletto <NeedsChloesure@riseup.net>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
This commit is contained in:
Chloe Oletto
2026-07-07 22:24:17 -07:00
committed by GitHub
parent afd63db043
commit a4cbe83211
8 changed files with 64 additions and 56 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@notesnook/vericrypt", "name": "@notesnook/vericrypt",
"version": "1.3.0", "version": "1.4.0",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@types/platform": "^1.3.6", "@types/platform": "^1.3.6",
@@ -43,4 +43,4 @@
"last 1 safari version" "last 1 safari version"
] ]
} }
} }

View File

@@ -31,7 +31,7 @@ import { PasteEncryptedData, SyncRequestBody } from "./components/step-4";
import { StepSeperator } from "./components/step-seperator"; import { StepSeperator } from "./components/step-seperator";
import { Footer } from "./components/footer"; import { Footer } from "./components/footer";
import { useState } from "react"; import { useState } from "react";
import { NNCrypto } from "@notesnook/crypto"; import { NNCrypto, Cipher } from "@notesnook/crypto";
import { Code } from "./components/code"; import { Code } from "./components/code";
import { Accordion } from "./components/accordion"; import { Accordion } from "./components/accordion";
import { DecryptedResult } from "./components/decrypted-result"; import { DecryptedResult } from "./components/decrypted-result";
@@ -41,7 +41,7 @@ const instructions = [
"Go to Notesnook", "Go to Notesnook",
"Open Settings", "Open Settings",
<> <>
Click on <Code text="Backup data recovery key" /> Click on <Code text="Save data recovery key" />
</>, </>,
"Enter your account password for verification", "Enter your account password for verification",
"Confirm that your generated encryption key matches" "Confirm that your generated encryption key matches"
@@ -52,6 +52,7 @@ function App() {
const [salt, setSalt] = useState<string>(); const [salt, setSalt] = useState<string>();
const [key, setKey] = useState<string>(); const [key, setKey] = useState<string>();
const [data, setData] = useState<SyncRequestBody | undefined>(); const [data, setData] = useState<SyncRequestBody | undefined>();
const [accountDataKey, setAccountDataKey] = useState<Cipher<"base64"> | null | undefined>();
const theme = useTheme({ accent: getDefaultAccentColor(), theme: "light" }); const theme = useTheme({ accent: getDefaultAccentColor(), theme: "light" });
return ( return (
@@ -67,7 +68,7 @@ function App() {
> >
<LoginToNotesnook /> <LoginToNotesnook />
<StepSeperator /> <StepSeperator />
<GetAccountSalt onSaltSubmitted={setSalt} /> <GetAccountSalt onSaltSubmitted={setSalt} setAccountDataKey={setAccountDataKey} />
{salt && ( {salt && (
<> <>
@@ -91,7 +92,7 @@ function App() {
return true; return true;
}} }}
popup={{ popup={{
title: "Your data encryption key", title: "Your master encryption key",
body: key ? ( body: key ? (
<> <>
<Text <Text
@@ -146,11 +147,13 @@ function App() {
password={password} password={password}
salt={salt} salt={salt}
data={data} data={data}
accountKey={accountDataKey}
onRestartProcess={() => { onRestartProcess={() => {
setSalt(undefined); setSalt(undefined);
setPassword(undefined); setPassword(undefined);
setKey(undefined); setKey(undefined);
setData(undefined); setData(undefined);
setAccountDataKey(undefined);
}} }}
/> />
</> </>

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

View File

@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Flex, Button, Text, Link } from "@theme-ui/components"; import { Flex, Button, Text, Link } from "@theme-ui/components";
import { StepContainer } from "./step-container"; import { StepContainer } from "./step-container";
import { SyncRequestBody } from "./step-4"; import { SyncRequestBody } from "./step-4";
import { NNCrypto } from "@notesnook/crypto"; import { NNCrypto, Cipher } from "@notesnook/crypto";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { FcDataEncryption } from "react-icons/fc"; import { FcDataEncryption } from "react-icons/fc";
import { Code } from "./code"; import { Code } from "./code";
@@ -32,6 +32,7 @@ type DecryptedResultProps = {
password: string; password: string;
salt: string; salt: string;
data: SyncRequestBody; data: SyncRequestBody;
accountKey: Cipher<"base64"> | null | undefined;
onRestartProcess: () => void; onRestartProcess: () => void;
}; };
@@ -51,10 +52,19 @@ export function DecryptedResult(props: DecryptedResultProps) {
}; };
const crypto = new NNCrypto(); const crypto = new NNCrypto();
const key = await crypto.exportKey(props.password, props.salt); const key = await crypto.exportKey(props.password, props.salt);
let encryptionKey = undefined;
if (props.accountKey != null){
// Need to decrypt the account key
props.accountKey.format = "base64"; // account keys don't have the format set, but they're base64.
const dataEncryptionKey = JSON.parse(await crypto.decrypt(key, props.accountKey, "text"))
encryptionKey = dataEncryptionKey;
} else {
encryptionKey = key;
}
for (const arrayKey in data) { for (const arrayKey in data) {
const array = data[arrayKey]; const array = data[arrayKey];
for (const encryptedItem of (props.data as any)[arrayKey]) { for (const encryptedItem of (props.data as any)[arrayKey]) {
const data = await crypto.decrypt(key, encryptedItem, "text"); const data = await crypto.decrypt(encryptionKey, encryptedItem, "text");
array.push(JSON.parse(data)); array.push(JSON.parse(data));
} }
} }
@@ -150,19 +160,9 @@ export function DecryptedResult(props: DecryptedResultProps) {
support@streetwriters.co support@streetwriters.co
</Link>{" "} </Link>{" "}
or{" "} or{" "}
<Link href="https://discord.gg/">joining our Discord community</Link>. <Link href="https://go.notesnook.com/discord">join our Discord community</Link>.
We&apos;ll do our best to alleviate all your worries. We&apos;ll do our best to alleviate all your worries.
</Text> </Text>
<Text as="p" variant="body" sx={{ mx: 2, mt: 2, fontWeight: "bold" }}>
What about open sourcing Notesnook?
</Text>
<Text as="p" variant="body" sx={{ mx: 2, my: 2 }}>
Open sourcing is another part of garnering our users&apos; trust. We
have <Link href="https://notesnook.com/roadmap">plans</Link> to begin
open sourcing in May but open sourcing will not make this tool
obsolete. Verifying the integrity of encrypted data at any point in
time is very important even if the software is open source.
</Text>
</Accordion> </Accordion>
<Flex sx={{ alignSelf: "center", mt: 4 }}> <Flex sx={{ alignSelf: "center", mt: 4 }}>
<Button <Button

View File

@@ -38,11 +38,6 @@ export function LoginToNotesnook() {
> >
<Text variant="title">Welcome to Vericrypt</Text> <Text variant="title">Welcome to Vericrypt</Text>
<Text as="p" variant="body" sx={{ mt: 1 }}> <Text as="p" variant="body" sx={{ mt: 1 }}>
<del>
Trust is a huge problem in closed source end-to-end encrypted
applications. How can you be sure that the app is actually encrypting
your data?
</del>
</Text> </Text>
<Text as="p" variant="body" sx={{ mt: 1 }}> <Text as="p" variant="body" sx={{ mt: 1 }}>
The only way to earn a user&apos;s trust is by allowing them to see how The only way to earn a user&apos;s trust is by allowing them to see how
@@ -56,9 +51,10 @@ export function LoginToNotesnook() {
</Link> </Link>
</Text> </Text>
<Text as="p" variant="body" sx={{ mt: 1 }}> <Text as="p" variant="body" sx={{ mt: 1 }}>
Yes, that&apos;s right. Notesnook is now 100% open source under the Yes, that&apos;s right. Notesnook is 100% open source under the
GPLv3 license. That includes the app, the encryption library, the GPLv3 license. That includes the app, the encryption library, and
backend server, and everything else. everything else. We&apos;ve even open sourced the server, available under the
AGPLv3 license.
</Text> </Text>
<Text as="p" variant="body" sx={{ mt: 1 }}> <Text as="p" variant="body" sx={{ mt: 1 }}>
However, even with an open source app, how can you be sure that the app However, even with an open source app, how can you be sure that the app
@@ -86,7 +82,7 @@ export function LoginToNotesnook() {
sx={{ alignSelf: "center", mt: 2 }} sx={{ alignSelf: "center", mt: 2 }}
onClick={() => window.open("https://app.notesnook.com/login", "_blank")} onClick={() => window.open("https://app.notesnook.com/login", "_blank")}
> >
Login to Notesnook Get Started by Logging in
</Button> </Button>
</StepContainer> </StepContainer>
); );

View File

@@ -16,11 +16,11 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License 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 { Flex, Text, Image, Input } from "@theme-ui/components"; import { Flex, Text, Image, Textarea } from "@theme-ui/components";
import { StepContainer } from "./step-container"; import { StepContainer } from "./step-container";
import DevtoolsCopySaltChrome from "../assets/screenshots/devtools_copy_salt.png"; import DevtoolsCopyUsersChrome from "../assets/screenshots/devtools_copy_users.png";
import DevtoolsCopySaltFirefox from "../assets/screenshots/firefox/firefox_copy_salt.png"; import DevtoolsCopyUsersFirefox from "../assets/screenshots/firefox/firefox_copy_users.png";
import { Accordion } from "./accordion"; import { Accordion } from "./accordion";
import { getCombo } from "../utils/keycombos"; import { getCombo } from "../utils/keycombos";
@@ -29,9 +29,11 @@ import { KeyCombo } from "./key-combo";
import { Code } from "./code"; import { Code } from "./code";
import { useState } from "react"; import { useState } from "react";
import { getSourceUrl } from "../utils/links"; import { getSourceUrl } from "../utils/links";
import { Cipher } from "@notesnook/crypto";
type GetAccountSaltProps = { type GetAccountSaltProps = {
onSaltSubmitted: (salt: string) => void; onSaltSubmitted: (salt: string) => void;
setAccountDataKey: (accountDataKey: Cipher<"base64"> | null) => void;
}; };
const steps = { const steps = {
@@ -52,10 +54,10 @@ const steps = {
</Text>, </Text>,
<Flex key="key-named-salt" sx={{ flexDirection: "column" }}> <Flex key="key-named-salt" sx={{ flexDirection: "column" }}>
<Text as="p">Follow the steps as shown in the image:</Text> <Text as="p">Follow the steps as shown in the image:</Text>
<Image src={DevtoolsCopySaltChrome} width={"100%"} sx={{ mt: 1 }} /> <Image src={DevtoolsCopyUsersChrome} width={"100%"} sx={{ mt: 1 }} />
</Flex>, </Flex>,
<Flex key="copy-salt" sx={{ flexDirection: "column" }}> <Flex key="copy-salt" sx={{ flexDirection: "column" }}>
<Text as="p">Copy the salt and paste it below.</Text> <Text as="p">Copy everything in the response and paste it below.</Text>
</Flex> </Flex>
], ],
firefox: [ firefox: [
@@ -75,10 +77,10 @@ const steps = {
</Text>, </Text>,
<Flex key="key-named-salt" sx={{ flexDirection: "column" }}> <Flex key="key-named-salt" sx={{ flexDirection: "column" }}>
<Text as="p">Follow the steps as shown in the image:</Text> <Text as="p">Follow the steps as shown in the image:</Text>
<Image src={DevtoolsCopySaltFirefox} width={"100%"} sx={{ mt: 1 }} /> <Image src={DevtoolsCopyUsersFirefox} width={"100%"} sx={{ mt: 1 }} />
</Flex>, </Flex>,
<Flex key="copy-salt" sx={{ flexDirection: "column" }}> <Flex key="copy-salt" sx={{ flexDirection: "column" }}>
<Text as="p">Copy the salt and paste it below.</Text> <Text as="p">Copy everything in the response and paste it below.</Text>
</Flex> </Flex>
] ]
}; };
@@ -114,14 +116,14 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
sx={{ flexDirection: "column" }} sx={{ flexDirection: "column" }}
> >
<Flex sx={{ justifyContent: "space-between", alignItems: "center" }}> <Flex sx={{ justifyContent: "space-between", alignItems: "center" }}>
<Text variant="title">Account salt</Text> <Text variant="title">Account Key Data</Text>
<Code <Code
text="src/components/step-2.tsx" text="src/components/step-2.tsx"
href={getSourceUrl("src/components/step-2.tsx")} href={getSourceUrl("src/components/step-2.tsx")}
/> />
</Flex> </Flex>
<Accordion <Accordion
title="How to get your account salt?" title="How to get your key data?"
sx={{ sx={{
border: "1px solid var(--border)", border: "1px solid var(--border)",
mt: 2, mt: 2,
@@ -129,9 +131,9 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
}} }}
> >
<Text variant="body" sx={{ mx: 2 }}> <Text variant="body" sx={{ mx: 2 }}>
We&apos;ll be extracting your account&apos;s salt right from You&apos;ll be extracting your account&apos;s keys right from
Notesnook&apos;s local database that lives in your web browser. So put Notesnook&apos;s local database that lives in your web browser. So put
on your seat belt and let&apos;s get some salt! on your seat belt and let&apos;s retrieve those keys.
</Text> </Text>
<Text as="ol" variant="body" sx={{ mb: 2 }}> <Text as="ol" variant="body" sx={{ mb: 2 }}>
{instructions?.map((item, index) => ( {instructions?.map((item, index) => (
@@ -141,12 +143,11 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
))} ))}
</Text> </Text>
</Accordion> </Accordion>
<Input <Textarea
variant="forms.clean" variant="forms.clean"
id="salt" id="account-data"
name="salt" name="account-data"
type="text" placeholder="Paste the response here"
placeholder="Enter your account salt"
sx={{ sx={{
mt: 2, mt: 2,
fontSize: "subheading", fontSize: "subheading",
@@ -162,13 +163,21 @@ export function GetAccountSalt(props: GetAccountSaltProps) {
spellCheck={false} spellCheck={false}
onChange={(e) => { onChange={(e) => {
setIsSaltValid(undefined); setIsSaltValid(undefined);
/*
If there is no dataEncryptionKey, try reading the legacyDataEncryptionKey.
If there is a legacyDataEncryptionKey, use this one.
If there is no legacyDataEncryptionKey, it is a legacy account (pre DEK update) that has not
changed passwords. (Use master key instead.)
*/
try { try {
const value = e.target.value; const value = JSON.parse(e.target.value);
const isValid = Buffer.from(value, "base64").length === 16; props.setAccountDataKey(value.dataEncryptionKey ?? value.legacyDataEncryptionKey ?? null);
const salt = value.salt;
const isValid = Buffer.from(salt, "base64").length === 16;
if (!isValid) setIsSaltValid(false); if (!isValid) setIsSaltValid(false);
else { else {
setIsSaltValid(true); setIsSaltValid(true);
props.onSaltSubmitted(value); props.onSaltSubmitted(salt);
} }
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@@ -41,13 +41,7 @@ This process is repeated every time you sign in.
### 2. Key generation ### 2. Key generation
After you are signed in, the app requests your user data which includes, among other things, your salt. When you first sign up for an account, your client generates two encryption keys. One is a unique data encryption key that encrypts all your notes and other data. The second is your master encryption key, this is derived by your password and predictable salt. This key protects all your encryption keys, like the aforementioned data encryption key. If you change your password, your client will re-encrypt your existing data encryption key with your new master key.
> info Salt generation
>
> When you create an account, the server generates a cryptographically secure random salt for you. This salt is used for key generation.
You password & salt is then used to derive a strong irreversible key using Argon2 as the password key derivation function (PKDF).
### 3. Encryption key storage ### 3. Encryption key storage
@@ -55,7 +49,7 @@ You password & salt is then used to derive a strong irreversible key using Argon
Instead of storing the key as plain text (and allowing anyone to copy/move it), we use browser's `IndexedDB` to store the key as a `CryptoKey`. Instead of storing the key as plain text (and allowing anyone to copy/move it), we use browser's `IndexedDB` to store the key as a `CryptoKey`.
`CryptoKey` is stored securely by the browser and cannot be exported, viewed, copied except by the app & browser. `CryptoKey` is stored securely by the browser and cannot be exported, viewed, or copied except by the app & browser.
# [Mobile](#/tab/mobile) # [Mobile](#/tab/mobile)
@@ -70,7 +64,7 @@ Encryption only takes place when you sync. Each item in the database is encrypte
#### How it works #### How it works
1. The item is read from the database as JSON object and stringified (i.e. converted to a string). 1. The item is read from the database as JSON object and stringified (i.e. converted to a string).
2. The string is encrypted using the encryption key generated earlier. 2. The string is encrypted using the data encryption key generated earlier.
3. The result is a JSON object which contains: 3. The result is a JSON object which contains:
1. A base64 encoded `cipher` 1. A base64 encoded `cipher`
2. A 192-bit nonce (`iv`) 2. A 192-bit nonce (`iv`)
@@ -83,3 +77,9 @@ Encryption only takes place when you sync. Each item in the database is encrypte
> See the whole process in action [here.](https://vericrypt.notesnook.com/) > See the whole process in action [here.](https://vericrypt.notesnook.com/)
This object is then sent to the server for storage. The server performs no further operation on this data (because it can't). This object is then sent to the server for storage. The server performs no further operation on this data (because it can't).
## Faqs
### I am an old user of Notesnook, I don't have a data encryption key.
Your data encryption key will be created when you change your password.