Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

All: Add new encryption methods based on native crypto libraries #10696

Merged
merged 78 commits into from
Oct 26, 2024

Conversation

wh201906
Copy link
Contributor

@wh201906 wh201906 commented Jul 7, 2024

This PR adds some new encryption methods for improved performance and security. Users need to turn on Use beta encryption in the sync settings to enable this feature.

Code Changes:

  • Dependency: Added react-native-quick-crypto to the app-mobile package.
  • Code: Introduced crypto module, including its type definition and implementations for both Node.js and React Native.
  • Code: Introduced some new encryption methods(StringV1, FileV1 and KeyV1) with AES-256-GCM cipher and better performance.
  • Code: Added a featureFlag in the sync settings for the new methods.
  • Code: Increased encryption chunk size from 5000 to 65536 for string and 131072 for files.
  • Code: Add chunk size map to apply different size to different encryption methods.
  • Code: Added defaultFileEncryptionMethod_ to handle JS strings and base64 file content separately.
  • Code: Updated the handlers in EncryptionService.encrypt() to support async functions.
  • Test: Modified EncryptionService.test.ts and Synchronizer.e2ee.test.ts to test the new encryption methods in some test cases.
  • Test: Added integration tests for testing encryption/decryption on different devices.
  • Test: Added performance tests in the integration tests.
  • Test: Added unit tests for the nonce generation and increasement.

Copy link
Collaborator

@personalizedrefrigerator personalizedrefrigerator left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this!

I've left a few comments.

packages/app-mobile/utils/shim-init-react.js Outdated Show resolved Hide resolved
@wh201906 wh201906 marked this pull request as draft July 7, 2024 16:25
import { promisify } from 'util';
import crypto = require('crypto');

class NativeEncryption implements NativeEncryptionInterface {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the use of a class makes sense here. Is it just to group these functions together? In that case a file crypto.ts might be preferable with plain functions inside.

Also the fact that it is "native" doesn't really matter, so it should not appear in the name.

Copy link
Contributor Author

@wh201906 wh201906 Jul 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also the fact that it is "native" doesn't really matter, so it should not appear in the name.

I will remove it.

Is it just to group these functions together? In that case a file crypto.ts might be preferable with plain functions inside.

I guess this is the grouped plain functions, right? If so I can rewrite these functions in that way.
https://github.com/laurent22/joplin/blob/v3.0.1/packages/lib/services/e2ee/RSA.node.ts#L11

Copy link
Contributor Author

@wh201906 wh201906 Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed the Native in the name and renamed these modules to Crypto in commit a2be9f1.
However, I just noticed that the name Crypto has been used in the Web Crypto API. I guess it's better to use a different name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed the class definition in commit 17aa0fd.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As @tessus suggested, I've replaced the name to Krypto

@wh201906 wh201906 marked this pull request as ready for review July 14, 2024 03:54
@wh201906
Copy link
Contributor Author

@laurent22 @personalizedrefrigerator I thinks it's ready to be merged.

Copy link
Owner

@laurent22 laurent22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the pull request. I understand this is preparation work, but have you been able to use this for E2EE?

import crypto from 'react-native-quick-crypto';
import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys';

const Krypto: KryptoInterface = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • In general we don't "brand" filenames to go around naming conflicts. TypeScript provides ways to deal with this when importing.

  • The name should be all lowercase because it's a library of functions (see coding style guide) and should be "crypto.ts"

  • It should default-export this constant. Name it cryptoLib if you want, and then export default cryptoLib)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Krypto was my suggestion, since I didn't know whether ts/js had namespaces. Sorry about that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I will rename them.

return crypto.getHashes();
},

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mentioned that the pull request is ready to be merged but surely we can't merge this. You're correct that all "any" should be replaced by proper types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's good to add react-native-quick-crypto(or craftzdog/react-native-buffer) in the dependencies of the whole project, so I can import the Buffer type in it as RNBuffer and make a new type NodeBuffer | RNBuffer for the interface.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and make a new type NodeBuffer | RNBuffer for the interface.

Ideally the interface would not mention any RN-specific type because it's going to be shared with non-RN code. Don't NodeBuffer and RNBuffer share the same interface? Are there differences? If it's the same, maybe create a new interface that declares just the methods and properties that you need

Copy link
Contributor Author

@wh201906 wh201906 Jul 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created type CryptoBuffer for it.

@@ -26,6 +26,8 @@ function shimInit() {
shim.Geolocation = GeolocationReact;
shim.sjclModule = require('@joplin/lib/vendor/sjcl-rn.js');

shim.Krypto = require('../services/e2ee/Krypto.react-native').default;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For new code, all import statements should be at the top.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it.

@@ -25,3 +25,14 @@ export interface RSA {
publicKey(rsaKeyPair: RSAKeyPair): string;
privateKey(rsaKeyPair: RSAKeyPair): string;
}

export interface KryptoInterface {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please name it "Crypto". We know from the type that it's an interface so it's not necessary to append this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

@wh201906
Copy link
Contributor Author

@laurent22 @tessus @personalizedrefrigerator Please help review the changes. Thanks!

@wh201906 wh201906 force-pushed the wh201906/native_encryption branch from 7788bc0 to 832e589 Compare July 20, 2024 05:59
},

pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: string): Promise<CryptoBuffer> => {
const digestMap: { [key: string]: HashAlgorithm } = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again please read the coding style guide, we explicitly ask not to set types this way. See https://joplinapp.org/help/dev/coding_style#avoid-inline-types

I really shouldn't have to keep reminding you to read it. It's understandable to miss a few details here and there but for this PR that's what, the third or fourth time that I remind you to read it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I only checked the method declaration. I will change it later.

'sha512': 'SHA-512',
'ripemd160': 'RIPEMD-160',
};
const digestAlgorithm: string = digestMap[digest.toLowerCase()] || digest;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment on lines 25 to 33
const digestMap: { [key: string]: string } = {
'sha-1': 'sha1',
'sha-224': 'sha224',
'sha-256': 'sha256',
'sha-384': 'sha384',
'sha-512': 'sha512',
'ripemd-160': 'ripemd160',
};
const digestAlgorithm: string = digestMap[digest.toLowerCase()] || digest;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really get this part - are the digestMap the list of supported digests? In that case shouldn't that be exposed to the library user? In other words, it seems there should be an enum that tells the list of supported digests.

And digest should probably have this type.

Also please don't use toLowerCase() for this - the input digest should be strictly correct and checked at compile time using a type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The digestMap of pbkdf2Raw() is for mapping the name according to the implementations. For Node.js, the hash algorithms are named like sha1, sha256 while for react-native-quick-crypto they are named like SHA-1, SHA-256. I planned to use the string type for the name, just like how Node.js does. But now I will change it to a enum because react-native-quick-crypto only supports a limited set of them.

wh201906 added 2 commits July 21, 2024 19:12
Remove the return value type in the implementation
Rename some variables
Copy link
Owner

@laurent22 laurent22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, still a few details but we're getting there. In general please try to use consistent names for things.

For example a map of Cars is a CarMap, not a VehicleMap or an AutomobileMap. OR else you rename car to Vehicle or Automobile too.

Consistent names help with reading the code and searching for strings.

Comment on lines 41 to 47
export type HashAlgorithm =
| 'SHA-1'
| 'SHA-224'
| 'SHA-256'
| 'SHA-384'
| 'SHA-512'
| 'RIPEMD-160';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you mind using an actual enum for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to use enum but the enum member name cannot contain -.

Copy link
Contributor Author

@wh201906 wh201906 Jul 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are the enum member name styles I tried:
SHA-512: The style used in react-native-quick-crypto, but the enum member name cannot contain -.
sha512: The style used in node:crypto, but it is not PascalCase so the linter fails.
Sha512: Valid enum member name and follows linter rules, but not used by node:crypto or react-native-quick-crypto and looks strange.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use the type used in node:crypto and add the exceptions to eslintrc.js, in the "ENUM" section

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finished in commit f2ed473.

pbkdf2 as nodePbkdf2,
} from 'crypto';

type NodeDigestNameMap = Record<HashAlgorithm, string>;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is it actually? A digest or a hash algorithm? Please pick one name. Also everything is "NodeSomething" in this file but that doesn't mean we should prefix all types and variables. It's implied that it's NodeSomething.

Copy link
Contributor Author

@wh201906 wh201906 Jul 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is it actually? A digest or a hash algorithm? Please pick one name.

They are almost identical in this context. For node:crypto and react-native-quick-crypto, the argument name in pbkdf2() is digest but the type of it is HashAlgorithm. Considering the digest is the result of a hash algorithm I will use hashxx for all cases except the argument name in pbkdf2Raw() (to match the argument name in node:crypto and react-native-quick-crypto). I will use digest for all.

Also everything is "NodeSomething" in this file but that doesn't mean we should prefix all types and variables. It's implied that it's NodeSomething.

I use the prefix node to indicate the function is from the node:crypto rather than my own implementation. If I remove the prefix, the name getCiphers could refers to the one in node:crypto and the one in my interface Crypto. In the upcoming draft I didn't add the node prefix to functions like createCipheriv because I won't implement a wrapper of it in the interface Crypto.
Is this acceptable?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finished in commit e4eb58c.

@wh201906
Copy link
Contributor Author

but have you been able to use this for E2EE?

Yes I did.

Fix description in crypto.test.ts
Remove featureFlag.useBetaEncryptionMethod.advanced because it is set to
true elsewhere
@wh201906
Copy link
Contributor Author

wh201906 commented Sep 6, 2024

@laurent22 I think we have shown that these new encryption methods (StringV1, FileV1 and KeyV1) can be implemented by Web Crypto API, node:crypto or react-native-quick-crypto. Would you consider putting the new methods to the dev branch now so users can try them? If we proved the Web Crypto API implementation with IPC is better on mobile platforms, we can switch to it without modifying any existing ciphertext encrypted with StringV1/FileV1/KeyV1.

@wh201906
Copy link
Contributor Author

wh201906 commented Sep 9, 2024

@personalizedrefrigerator Hi. I just found that I cannot build the project after merging the current dev branch (commit ec36847). It shows some errors like this:

➤ YN0000: [@joplin/app-mobile]: ../lib/services/e2ee/crypto.ts(12,30): error TS2339: Property 'subtle' does not exist on type 'typeof webcrypto'.
➤ YN0000: [@joplin/app-mobile]: ../lib/services/e2ee/crypto.ts(15,19): error TS2339: Property 'subtle' does not exist on type 'typeof webcrypto'.
➤ YN0000: [@joplin/app-mobile]: ../lib/services/e2ee/crypto.ts(21,37): error TS2339: Property 'subtle' does not exist on type 'typeof webcrypto'.
➤ YN0000: [@joplin/app-mobile]: ../lib/services/e2ee/crypto.ts(30,37): error TS2339: Property 'subtle' does not exist on type 'typeof webcrypto'.
➤ YN0000: [@joplin/app-mobile]: ../lib/services/e2ee/crypto.ts(46,14): error TS2339: Property 'getRandomValues' does not exist on type 'typeof webcrypto'.
➤ YN0000: [@joplin/app-mobile]: ../lib/services/e2ee/crypto.ts(53,15): error TS2339: Property 'getRandomValues' does not exist on type 'typeof webcrypto'.
➤ YN0000: [@joplin/app-mobile]: ../lib/services/e2ee/crypto.ts(58,33): error TS2339: Property 'getRandomValues' does not exist on type 'typeof webcrypto'.
➤ YN0000: [@joplin/app-mobile]: ../lib/services/e2ee/crypto.ts(66,38): error TS2339: Property 'subtle' does not exist on type 'typeof webcrypto'.

I tried merging several commits in the dev branch and it seems to be caused by commit 7e9c7d7. I guess it's something wrong about the Web Crypto API in the Node.js but I have no idea how to resolve it. Could you please take a look when you have time?

@personalizedrefrigerator
Copy link
Collaborator

personalizedrefrigerator commented Sep 10, 2024

I tried merging several commits in the dev branch and it seems to be caused by commit 7e9c7d7. I guess it's something wrong about the Web Crypto API in the Node.js but I have no idea how to resolve it. Could you please take a look when you have time?

The error seems to be coming from the buildInjectedJs step of the mobile app build. I notice that 7e9c7d7 causes @types/node:^18.0.0 to resolve to a different version than is used by @joplin/lib...

Does changing these lines in yarn.lock from

"@types/node@npm:^18.0.0":
  version: 18.19.33
  resolution: "@types/node@npm:18.19.33"

to

"@types/node@npm:^18.0.0":
  version: 18.19.34
  resolution: "@types/node@npm:18.19.34"

then re-running yarn install help?

@wh201906
Copy link
Contributor Author

Does changing these lines in yarn.lock from

"@types/node@npm:^18.0.0":
  version: 18.19.33
  resolution: "@types/node@npm:18.19.33"

to

"@types/node@npm:^18.0.0":
  version: 18.19.34
  resolution: "@types/node@npm:18.19.34"

then re-running yarn install help?

It works. Thanks

@wh201906
Copy link
Contributor Author

wh201906 commented Sep 12, 2024

  • Do tests exist for or would it make sense to add tests for the following cases?
    • Using the Synchronizer with EncryptionService with the new encryption methods (a full sync):
      • Notes with resources.
      • Plain text notes.
      • Empty notes.
      • Multiple master keys.
      • Invalid data (checking that Synchronizer and related logic handles exceptions correctly).
      • Android/iOS: Encrypting/decrypting data that contains null characters (\0).
        • In the past, we've had trouble with null characters and SQLite storage due to how a library was sending information to native code.

I think for Case 1~4 they have nothing to do with a specific encryption method. They might be tested elsewhere, but I'm not sure about it.
For Case 5, I added some unit test cases in commit b329c0c, and the Synchronizer related logic test is added in commit edd2a9002a.
For Case 6, I didn't test it with the Synchronizer, but the null characters test is added in commit 26faf260cf. This test case covers plaintext with null character in it and it's working. As for the ciphertext, it is always base64 encoded so a null character won't appear in it.

@personalizedrefrigerator I guess I've checked the cases you mentioned.

@wh201906 wh201906 changed the title All: Replace sjcl with native crypto implementations All: Add new encryption methods based on native crypto libraries Sep 14, 2024
@wh201906
Copy link
Contributor Author

@laurent22 @personalizedrefrigerator I thinks it's ready to be merged.

@wh201906 wh201906 requested a review from tessus September 22, 2024 07:59
[EncryptionMethod.SJCL4]: 5000,
[EncryptionMethod.Custom]: 5000,
[EncryptionMethod.KeyV1]: 5000, // Master key is not encrypted by chunks so this value will not be used.
[EncryptionMethod.FileV1]: 131072, // 128k
Copy link

@ahxxm ahxxm Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(comment because of #9852, my joplin-desktop now contains 4G data + 8G encrypted data, feel free to ignore!)

for such large chunk size, do you think the encrypted bytes might benefit from a quick zstd compression? i.e. tentatively compress the plaintext before encryptRaw then set result.compressed = true; result.ct = encryptedCompressed if smaller, the same for decrypt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the extra compression step will slow down the encryption/decryption speed. The E2EE on mobile platform is not very fast, so the speed should be taken into consideration.
With that said, even if we don't use compression now, the encrypted file size is decreased in the new encryption methods. (report).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zstd can reach around 500mb/s with default compression level on desktop, while I can't find a benchmark on mobile, its overhead(a slight regression over huge improvements you brought) should remain manageable

I'm still reading the full report, overhead decrease in file size and master key are promising, thank you!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The speed overhead could be much greater than expected.
For example, the raw AES encryption speed on desktop is over a few GB/s
https://discourse.joplinapp.org/t/performance-test-of-node-js/38762#aes-encryptiondecryption-performance-2
But in the final implementation, the encryption speed is less than 100MB/s
https://discourse.joplinapp.org/t/final-report-of-the-native-encryption-project/40171#p-128759-based-on-commit-edd2a9002a31b0fee4f11ded0a37951543e289ab-release-build-7

Things could be worse on React Native. The raw AES encryption speed could reach 100MB/s. But in the final implementation, the encryption speed is less than 2MB/s
https://discourse.joplinapp.org/t/performance-test-of-react-native-quick-crypto/38622#aes-encryptiondecryption-performance-2

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I almost never edit notes on my phone, but the decryption slowness on first sync was already concerningly impressive..

@laurent22 laurent22 merged commit aa6348c into laurent22:dev Oct 26, 2024
10 checks passed
@laurent22
Copy link
Owner

Ok I think we can merge this then. Many thanks @wh201906 for implementing this important feature, and thanks @personalizedrefrigerator for reviewing and providing support!

@wh201906 wh201906 deleted the wh201906/native_encryption branch October 27, 2024 03:38
@laurent22
Copy link
Owner

laurent22 commented Nov 13, 2024

@wh201906, we're getting this random test error on CI:

➤ YN0000: [@joplin/lib]: FAIL services/e2ee/crypto.test.js
➤ YN0000: [@joplin/lib]:   ● e2ee/crypto › should generate new nonce if counter overflow
➤ YN0000: [@joplin/lib]: 
➤ YN0000: [@joplin/lib]:     expect(received).toBe(expected) // Object.is equality
➤ YN0000: [@joplin/lib]: 
➤ YN0000: [@joplin/lib]:     Expected: 257
➤ YN0000: [@joplin/lib]:     Received: 1
➤ YN0000: [@joplin/lib]: 
➤ YN0000: [@joplin/lib]:        97 | 		expect(nonce.subarray(0, 21)).not.toEqual(randomPart);
➤ YN0000: [@joplin/lib]:        98 | 		// Timestamp part should have expected value
➤ YN0000: [@joplin/lib]:     >  99 | 		expect(nonce[21]).toBe(timestampPart[0] + 2);
➤ YN0000: [@joplin/lib]:           | 		                  ^
➤ YN0000: [@joplin/lib]:       100 | 		expect(nonce.subarray(22, 28)).toEqual(timestampPart.subarray(1));
➤ YN0000: [@joplin/lib]:       101 | 	}));
➤ YN0000: [@joplin/lib]:       102 |
➤ YN0000: [@joplin/lib]: 
➤ YN0000: [@joplin/lib]:       at Object.toBe (services/e2ee/crypto.test.ts:99:21)

Do you know what could be the reason? It certainly looks like a value somewhere is stored as a byte, but it's checked here as if it was something else, an integer maybe.

What I'm wondering though is could it mean that encryption is faulty due to an integer overflow error? Or is that just the test that is not correct? And I wonder in terms of security if there could be a flaw - for example a number is expected to be a large integer, but it's in fact just a byte.

@wh201906
Copy link
Contributor Author

wh201906 commented Nov 13, 2024

@laurent22 I will check it right away.

@wh201906
Copy link
Contributor Author

wh201906 commented Nov 13, 2024

Do you know what could be the reason? It certainly looks like a value somewhere is stored as a byte, but it's checked here as if it was something else, an integer maybe.

The implementation of crypto.increaseNonce() is correct, but my test code is wrong. I only checked the least significant byte of the timestamp part in the nonce, which can overflow after adding 1 to 255, causing a carry-over to the next byte. I mistakenly thought the least significant byte should always increase and wrote the wrong code in the test cases. This issue can only occur when the test is running with timestamps ending in 0xFE or 0xFF, so it has a 2/256 chance of being triggered.

What I'm wondering though is could it mean that encryption is faulty due to an integer overflow error? Or is that just the test that is not correct?

It’s just an issue with the test. I'll submit a PR to fix it and add test cases for specific timestamps.

And I wonder in terms of security if there could be a flaw - for example a number is expected to be a large integer, but it's in fact just a byte.

This could happen when dealing with TypedArray because elements in it have a fixed range. For the implementations of new crypto methods I think they should be fine because I did consider the type and overflow for the nonce.

With that said, even in the worst case — the counter part and the timestamp part in the nonce are set to a constant value, the 21 bytes (168 bits) of random number still make the nonce quite unique. I just realized the random number part is updated only when the counter part overflows, so a fixed counter would result in a fixed nonce (salt) there. But we still have a 96 bits random IV which is updated for each chunk. Plus, the worst case I mentioned there is almost impossible to happen unless the Joplin app itself is malformed. Just forget about it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants