feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,100 @@
# Encryption
Encrypted data is encoded to ASCII because encryption/decryption functions in React Native can only deal with strings. So for compatibility with all the apps we need to use the lowest common denominator.
## Encrypted data format
### Header
Name | Size
-------------------|-------------------------
Identifier | 3 chars ("JED")
Version number | 2 chars (Hexa string)
This is followed by the encryption metadata:
Name | Size
-------------------|-------------------------
Length | 6 chars (Hexa string)
Encryption method | 2 chars (Hexa string)
Master key ID | 32 chars (Hexa string)
See `lib/services/e2ee/EncryptionService.ts` for the list of available encryption methods.
### Data chunk
The data is encoded in one or more chunks for performance reasons. That way it is possible to take a block of data from one file and encrypt it to another block in another file. Encrypting/decrypting the whole file in one go would not work (on mobile especially).
Name | Size
--------|----------------------------
Length | 6 chars (Hexa string)
Data | ("Length" bytes) (ASCII)
## Master Keys
The master keys are used to encrypt and decrypt data. They can be generated from the Encryption Service and are saved to the database. They are themselves encrypted via a user password using a [strong encryption method](https://github.com/laurent22/joplin/blob/b5b02d8d7bce2c07c89fef50103e1399d792b75e/packages/lib/services/e2ee/EncryptionService.ts#L373).
These encrypted master keys are transmitted with the sync data so that they can be available to each client. Each client will need to supply the user password to decrypt each key.
The application supports multiple master keys in order to handle cases where one offline client starts encrypting notes, then another offline client starts encrypting notes too, and later both sync. Both master keys will have to be decrypted separately with the user password.
Only one master key can be active for encryption purposes. For decryption, the algorithm will check the Master Key ID in the header, then check if it's available to the current app and, if so, use this for decryption.
## Encryption Service
The applications make use of the `EncryptionService` class to handle encryption and decryption. Before it can be used, a least one master key must be loaded into it and be marked as "active".
## Encryption workflow
Items are encrypted only during synchronisation, when they are serialised (via `BaseItem.serializeForSync`), so before being sent to the sync target.
They are decrypted by DecryptionWorker in the background.
The apps handle displaying both decrypted and encrypted items, so that user is aware that these items are there even if not yet decrypted. Encrypted items are mostly read-only to the user, except that they can be deleted.
## Enabling and disabling encryption
Enabling/disabling E2EE while two clients are in sync might have an unintuitive behaviour (although that behaviour might be correct), so below some scenarios are explained:
- If client 1 enables E2EE, all items will be synced to target and will appear encrypted on target. Although all items have been re-uploaded to the target, their timestamps did *not* change (because the item data itself has not changed, only its representation). Because of this, client 2 will not re-download the items - it does not need to do so anyway since it has already the item data.
- When a client sync and download a master key for the first time, encryption will be automatically enabled (user will need to supply the master key password). In that case, all items that are not encrypted will be re-synced. Uploading only non-encrypted items is an optimisation since if an item is already encrypted locally it means it's encrypted on target too.
- If both clients are in sync with E2EE enabled: if client 1 disable E2EE, it's going to re-upload all the items unencrypted. Client 2 again will not re-download the items for the same reason as above (data did not change, only representation). Note that user *must* manually disable E2EE on all clients otherwise some will continue to upload encrypted items. Since synchronisation is stateless, clients do not know whether other clients use E2EE or not so this step has to be manual.
- Although messy, Joplin supports having some clients send encrypted items and others unencrypted ones. The situation gets resolved once all the clients have the same E2EE settings.
- Currently, there is no way to delete encryption keys if you do not need them anymore or if you disabled the encryption completely. You will get a persistent notification to provide a Master Key password on a new device, even if encryption is disabled. Entering the Master Key(s) password and still having the encryption disabled will get rid of the notification. See [Delete E2EE Master Keys](https://discourse.joplinapp.org/t/delete-e2ee-master-keys/906) for more info.
## Types of keys
There are two types of key:
- **Data keys**, which are used to encrypt Joplin items, such as notes, notebooks, tags, etc. when E2EE is enabled. A data key is generated when the user enables E2EE. Data keys are also dynamically generated when a user shares a notebook with another user. In this case, we create a separate key, so that the recipient can only decrypt this specific notebook.
- **Public-private key pairs**, which are used to transfer secrets between users.
## Master password
The master password is used to encrypt E2EE data keys as well as the user's private key.
**It is possible to change the master password** - in this case, all keys are reencrypted with the new password. The data, notes, notebooks, etc. does not need to be reencrypted.
If a master password is forgotten it's not possible to recover it. **It is however possible to reset it**. In that case, all associated keys are disabled, and the public-private key pair is regenerated. In practice it means that any content that was encrypted with the forgotten password can no longer be decrypted.
## Public-private key pairs
Public-private key pairs (PPK) are used to transfer secrets between users. Specifically, they are used when sharing a notebook while E2EE is enabled. The workflow is as follow:
- Alice shares a notebook with Bob
- Since the notebook is encrypted, Alice also sends the key to Bob, but it needs to be encrypted too.
- To do so, she downloads Bob's public key and encrypts the key with it
- When accepting the share, Bob receives this key
- Bob decrypts it with his private key
- Once decrypted, he reencrypts it with his master password
At this point, both users have a copy of the key and can share notes over E2EE.
A user can only have one PPK.
PPKs are generated automatically when E2EE is enabled and when the user synchronises. They are then stored in info.json on the sync target. The key is generated during sync because otherwise multiple clients could generate a PPK, and then there would be a conflict to decide which PPK should be kept. By doing it during sync, it ensures that only one PPK is generated because the synchronizer fetches first info.json - and only generates a PPK if none is already present.

查看文件

@@ -0,0 +1,307 @@
# Native Encryption Method Specification
Starting with Joplin v3.2, we introduced a series of new encryption methods. These methods are built on native libraries and are designed to enhance both performance and security. This document provides an overview and detailed specifications of these new encryption methods.
**Related Links:**</br>
[GitHub Pull Request](https://github.com/laurent22/joplin/pull/10696)</br>
[GSoC Final Report](https://discourse.joplinapp.org/t/final-report-of-the-native-encryption-project/40171)</br>
## 1. General Steps for Encryption/Decryption
### 1.1. Encryption Steps:
```mermaid
graph LR;
pwd[Master<br/>Password]
mk[Master Key]
emk[Encrypted<br/>Master Key]
enc_1(("EncryptionService<br/>.encrypt()"))
sync[(Sync Target)]
pt[Notes/Resources]
enc_2(("EncryptionService<br/>.encrypt()"))
ct[Encrypted<br/>Notes/Resources]
mk-->enc_1
mk-->enc_2
subgraph Master Key Encryption
pwd-->enc_1
enc_1-->emk
end
subgraph Data Encryption
pt-->enc_2
enc_2-->ct
end
emk-->sync
ct-->sync
```
### 1.2. Decryption Steps:
```mermaid
graph LR;
pwd[Master<br/>Password]
mk[Master Key]
emk[Encrypted<br/>Master Key]
dec_1(("EncryptionService<br/>.decrypt()"))
sync[(Sync Target)]
pt[Notes/Resources]
dec_2(("EncryptionService<br/>.decrypt()"))
ct[Encrypted<br/>Notes/Resources]
sync-->ct
sync-->emk
subgraph Master Key Decryption
pwd-->dec_1
emk-->dec_1
dec_1-->mk
end
subgraph Data Decryption
mk-->dec_2
ct-->dec_2
dec_2-->pt
end
```
### 1.3. Explanations
- **Master Password**: The user-provided password used to encrypt the Master Key.
- **Master Key**: A randomly generated 256-byte (2048-bit) key used to encrypt notes and resources. This is also called the `Data Key` in [this document](https://github.com/laurent22/joplin/blob/dev/readme/dev/spec/e2ee/workflow.md).
- **Master Key Encryption/Decryption**: In these parts, the Master Key is encrypted or decrypted using the Master Password.
- **Data Encryption/Decryption**: In these parts, notes and resources are encrypted or decrypted using the Master Key.
The Master Password is stored locally in the database and is never updated to the Sync Target. As a result, third-party Sync Targets cannot decrypt the Master Key, notes or resources.
## 2. General Implementation for `encrypt()` and `decrypt()`
### 2.1. encrypt():
```mermaid
graph LR;
pwd[Password]
salt[Salt]
kdf((KDF))
key[Key]
pt_str["Plaintext<br/>(string)"]
pt_bin["Plaintext<br/>(binary)"]
ct_str["Ciphertext<br/>(string)"]
ct_bin["Ciphertext<br/>(binary)"]
cipher((Cipher))
codec_1((Encoder/<br/>Decoder))
codec_2((Encoder/<br/>Decoder))
pwd---salt
pt_str---salt
linkStyle 0,1 stroke-width:0px
pwd-->kdf
pt_str-->codec_1
subgraph sub_1 ["EncryptionService.encrypt()"]
direction LR
codec_1-->pt_bin
pt_bin-->cipher
salt-->kdf
kdf-->key
key-->cipher
cipher-->ct_bin
ct_bin-->codec_2
end
codec_2-->ct_str
```
### 2.2. decrypt():
```mermaid
graph LR;
pwd[Password]
salt[Salt]
kdf((KDF))
key[Key]
pt_str["Plaintext<br/>(string)"]
pt_bin["Plaintext<br/>(binary)"]
ct_str["Ciphertext<br/>(string)"]
ct_bin["Ciphertext<br/>(binary)"]
decipher((Decipher))
codec_1((Encoder/<br/>Decoder))
codec_2((Encoder/<br/>Decoder))
pwd---salt
ct_str---salt
linkStyle 0,1 stroke-width:0px
pwd-->kdf
ct_str-->codec_1
subgraph sub_1 ["EncryptionService.decrypt()"]
direction LR
codec_1-->ct_bin
ct_bin-->decipher
salt-->kdf
kdf-->key
key-->decipher
decipher-->pt_bin
pt_bin-->codec_2
end
codec_2-->pt_str
```
### 2.3. Explanations
(In this section, `encrypt()` and `decrypt()` refer to `EncryptionService.encrypt()` and `EncryptionService.decrypt()` respectively.)</br>
- **KDF**: Key Derivation Function, used to derive cryptographically strong keys from passwords.
- **Encoder/Decoder**: The `encrypt()` and `decrypt()` methods take strings as input and produce strings as output. Since the cipher operates on binary data and generates binary outputs, encoders and decoders are used to convert between strings and binary data.
- **KDF (Key Derivation Function)**: The `encrypt()` and `decrypt()` methods are designed for password-based encryption, but passwords may not meet cryptographic key strength requirements. The KDF addresses this by taking a password and a salt as inputs and generating a secure cryptographic key for the actual encryption process.
- **Salt**: The salt is managed internally by the `encrypt()` and `decrypt()` methods (or more generally, within the `EncryptionService`), so callers don't need to handle it explicitly.
## 3. Specifications for the New Native Encryption Methods
### 3.1. Encryption Flow
The expanded flow is shown below. Some of the elements have been replaced with the specific implementation compared with the general implementation graphs. Additionally, extra elements have been introduced for the chosen cipher.
```mermaid
graph LR;
pwd[Password]
salt[Salt]
kdf((PBKDF2))
key[Key]
pt_str["Plaintext<br/>(string)"]
pt_bin["Plaintext<br/>(binary)"]
ct_str["Ciphertext<br/>(string)"]
ct_bin["Ciphertext<br/>(binary)"]
iv[Initialization Vector]
adata[Associated Data]
atag[Authentication Tag]
cipher((AES-256-GCM))
codec((Encoder/<br/>Decoder))
b64enc((Base64<br/>Encoder))
pwd---salt
pt_str---salt
linkStyle 0,1 stroke-width:0px
pwd-->kdf
pt_str-->codec
subgraph sub_1 ["EncryptionService.encrypt()"]
direction LR
codec-->pt_bin
pt_bin-->cipher
salt-->kdf
kdf-->key
key-->cipher
iv-->cipher
adata-->cipher
cipher-->ct_bin
ct_bin-->b64enc
cipher-->atag
end
b64enc-->ct_str
```
### 3.2. Decryption Flow
The decryption flow is similar to the encryption flow.
```mermaid
graph LR;
pwd[Password]
salt[Salt]
kdf((PBKDF2))
key[Key]
pt_str["Plaintext<br/>(string)"]
pt_bin["Plaintext<br/>(binary)"]
ct_str["Ciphertext<br/>(string)"]
ct_bin["Ciphertext<br/>(binary)"]
iv[Initialization Vector]
adata[Associated Data]
atag[Authentication Tag]
decipher((AES-256-GCM))
codec((Encoder/<br/>Decoder))
b64dec((Base64<br/>Decoder))
pwd---salt
ct_str---salt
linkStyle 0,1 stroke-width:0px
pwd-->kdf
ct_str-->b64dec
subgraph sub_1 ["EncryptionService.decrypt()"]
direction LR
b64dec-->ct_bin
ct_bin-->decipher
salt-->kdf
kdf-->key
key-->decipher
iv-->decipher
adata-->decipher
atag-->decipher
decipher-->pt_bin
pt_bin-->codec
end
codec-->pt_str
```
### 3.3. Encoder/Decoder
Different encoders/decoders are used for different types of plaintext:
| Method Name | Plaintext Type | Encoder |
|---|---|---|
| KeyV1 | Master Key | Hex String Decoder |
| StringV1 | Note Content | UTF-16 Encoder |
| FileV1 | Resources (Files) | Base64 String Decoder |
- For `KeyV1` and `FileV1`, the plaintext is initially binary but is encoded as strings to work with `encrypt()` and `decrypt()`, which operate only on strings. Before encryption, a proper decoder converts these strings back to their original binary form. This resolves the double Base64 encoding issue in the old encryption methods, resulting in smaller ciphertext.
- For `StringV1`, UTF-16 encoding ensures compatibility with all possible JavaScript characters.
### 3.4. PBKDF2 Parameters
PBKDF2 is used as the KDF for its compatibility across all platforms. The parameters are listed below:
<table>
<tbody>
<tr>
<td>Method Name</td><td>Algorithm</td><td>Iteration Count</td><td>Salt</td><td>Output Key Length</td>
</tr>
<tr>
<td>KeyV1</td>
<td rowspan="3">PBKDF2-HMAC-SHA512</td>
<td>220000</td>
<td rowspan="3">256-bit Generated Salt</td>
<td rowspan="3">256 Bits</td>
</tr>
<tr>
<td>StringV1</td>
<td rowspan="2">3</td>
</tr>
<tr>
<td>FileV1</td>
</tr>
</tbody>
</table>
The iteration count of `KeyV1` follows [OWASP recommendations](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2). For `StringV1` and `FileV1`, the randomly generated Master Key already provides sufficient cryptographic strength so the low iteration count is acceptable.</br>
However, applying the KDF to the Master Key is still necessary to resolve the short IV problem in the AES-GCM cipher. The details are provided in [Section 3.6](#36-extended-equivalent-nonce).</br>
### 3.5. Cipher/Decipher Parameters
The parameters for the cipher `AES-256-GCM`, used in all three new encryption methods (`KeyV1`, `StringV1`, `FileV1`), are listed below:
| Parameter | Value |
|---|---|
| Cipher/Decipher | AES |
| Mode | GCM |
| Key Length | 256 Bits |
| Initialization Vector | 96-bit Random Bytes |
| Associated Data | (Empty) |
| Authentication Tag Length | 128 Bits |
The Initialization Vector (IV) length is set to 96 bits because extending it doesn't improve the cryptographic strength. Actually, the low-level implementation of AES-GCM always reduces longer IVs to 96 bits.
### 3.6. Extended Equivalent Nonce
Although AES-GCM has been used in TLS for years and has not shown significant vulnerabilities, there are still some security considerations:</br>
- The Galois Counter Mode (GCM) is vulnerable when the IV and key are reused.
- While a simple counter could serve as the IV, it's not easy to maintain a reliable monotonic counter across all clients.
- The AES-GCM cipher has a maximum IV length of 96 bits (as discussed in [Section 3.5](#35-cipherdecipher-parameters)), which is relatively short.
Although unlikely, a Joplin user could run into two pieces of ciphertext encrypted with the same IV even if the IV is randomly generated. This causes security vulnerabilities if the notes and resources are encrypted with the same Master Key directly. To resolve this, a 256-bit generated salt is used for each encryption to derive a new encryption key from the Master Key, so the key passed to the cipher changes every time. In theory, this approach provides an equivalent nonce with a length of (256+96) bits.
The salt is generated using the following formula:
```
encryptionNonce = concat(<168-bit Random Data>, <64-bit Timestamp in ms>, <56-bit Counter Value>)
salt = sha256(encryptionNonce)
```
- The `encryptionNonce` is generated when the app starts or when the 56-bit counter overflows.
- The counter increases with each encryption operation.
## 4. Dependencies
- **Desktop/CLI Client**: `Web Crypto API` provided by Node.js
- **Web Client**: `Web Crypto API` provided by the browsers
- **Mobile Client**: `react-native-quick-crypto`

查看文件

@@ -0,0 +1,23 @@
# E2EE workflow
## E2EE setup
This flowchart describes how E2EE is setup when the user enables it on a client (eg. desktop), and then what happens when they try to sync using a second client (eg. mobile).
<img alt="E2EE setup flowchart" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/e2ee/e2ee-setup.png" width="100%"/>
## E2EE synchronisation
This flowchart describes at a high level how synchronisation works while E2EE is enabled.
<img alt="E2EE synchronisation flowchart" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/e2ee/e2ee-sync.png" width="100%"/>
## E2EE notebook share
This flowchart describes how a user can share a notebook with another user.
<img alt="E2EE notebook share" src="https://raw.githubusercontent.com/laurent22/joplin/dev/Assets/WebsiteAssets/images/e2ee/e2ee-share.png" width="100%"/>
After the invitation has been received and the key exchanged, the process is the same as in a regular synchronisation operation.
Both users have the notebook key encrypted with their own master password. That key is uploaded to Joplin Cloud too to allow other clients to retrieve the encrypted data.