-
Notifications
You must be signed in to change notification settings - Fork 25
Port shared secret encryption from nucypher-core #646
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
base: epic-port-nucypher-core-into-ts
Are you sure you want to change the base?
Changes from 6 commits
9093538
dfb7647
acc7bd3
2198670
62b9958
6804223
9f0cd82
3961182
0bd45aa
74e6004
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| /** | ||
| * Errors during encryption. | ||
| */ | ||
| export class EncryptionError extends Error { | ||
| readonly code: EncryptionErrorCode; | ||
|
|
||
| constructor(code: EncryptionErrorCode) { | ||
| let message: string; | ||
| switch (code) { | ||
| case EncryptionErrorCode.PlaintextTooLarge: | ||
| message = 'Plaintext is too large to encrypt'; | ||
| break; | ||
| default: | ||
| message = 'Unknown encryption error'; | ||
| } | ||
| super(message); | ||
| this.name = 'EncryptionError'; | ||
| this.code = code; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Error codes for encryption operations. | ||
| */ | ||
| export enum EncryptionErrorCode { | ||
| PlaintextTooLarge = 'PlaintextTooLarge', | ||
| } | ||
|
|
||
| /** | ||
| * Errors during decryption. | ||
| */ | ||
| export class DecryptionError extends Error { | ||
| readonly code: DecryptionErrorCode; | ||
| readonly details?: string; | ||
|
|
||
| constructor(code: DecryptionErrorCode, details?: string) { | ||
| let message: string; | ||
| switch (code) { | ||
| case DecryptionErrorCode.CiphertextTooShort: | ||
| message = 'The ciphertext must include the nonce'; | ||
| break; | ||
| case DecryptionErrorCode.AuthenticationFailed: | ||
| message = 'Decryption of ciphertext failed: ' + | ||
| 'either someone tampered with the ciphertext or ' + | ||
| 'you are using an incorrect decryption key.'; | ||
| break; | ||
| case DecryptionErrorCode.DeserializationFailed: | ||
| message = details ? `deserialization failed: ${details}` : 'deserialization failed'; | ||
| break; | ||
| default: | ||
| message = 'Unknown decryption error'; | ||
| } | ||
| super(message); | ||
| this.name = 'DecryptionError'; | ||
| this.code = code; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Error codes for decryption operations. | ||
| */ | ||
| export enum DecryptionErrorCode { | ||
| CiphertextTooShort = 'CiphertextTooShort', | ||
| AuthenticationFailed = 'AuthenticationFailed', | ||
| DeserializationFailed = 'DeserializationFailed', | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './errors'; | ||
| export * from './shared-secret'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| import { chacha20poly1305 } from '@noble/ciphers/chacha'; | ||
| import { randomBytes } from '@noble/hashes/utils'; | ||
|
|
||
| import { | ||
| DecryptionError, | ||
| DecryptionErrorCode, | ||
| EncryptionError, | ||
| EncryptionErrorCode, | ||
| } from './errors'; | ||
|
|
||
| export function encryptWithSharedSecret( | ||
| sharedSecret: Uint8Array, | ||
| plaintext: Uint8Array, | ||
| ): Uint8Array { | ||
| const nonce = randomBytes(12); // Generate a 12-byte nonce | ||
| const cipher = chacha20poly1305(sharedSecret, nonce); // Use an object with key | ||
| try { | ||
| const ciphertext = cipher.encrypt(plaintext); | ||
| const result = new Uint8Array(nonce.length + ciphertext.length); | ||
| result.set(nonce); | ||
| result.set(ciphertext, nonce.length); | ||
| return result; | ||
| } catch (error) { | ||
| throw new EncryptionError(EncryptionErrorCode.PlaintextTooLarge); | ||
| } | ||
| } | ||
|
|
||
| export function decryptWithSharedSecret( | ||
| sharedSecret: Uint8Array, | ||
| ciphertext: Uint8Array, | ||
| ): Uint8Array { | ||
| const nonceLength = 12; // ChaCha20Poly1305 uses a 12-byte nonce | ||
| if (ciphertext.length < nonceLength) { | ||
| throw new DecryptionError(DecryptionErrorCode.CiphertextTooShort); | ||
| } | ||
| const nonce = ciphertext.slice(0, nonceLength); | ||
| const encryptedData = ciphertext.slice(nonceLength); | ||
| const cipher = chacha20poly1305(sharedSecret, nonce); // Use an object with key | ||
| try { | ||
| return cipher.decrypt(encryptedData); | ||
| } catch (error) { | ||
| throw new DecryptionError(DecryptionErrorCode.AuthenticationFailed); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| { | ||
| "test_vectors": [ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed the fixed nonce, specific shared secret - does it make sense to also have more random values in the test vector? Or is that something planned for later in the process?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The values are randomly generated at the Rust code and fed as constants to the compatibility tests. For values that are randomly generated in this project, there is the unit test in the other file.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Perhaps I'm misunderstanding. The values for the nonce, shared secret don't seem that "random"...? In one case the nonce is just 0s or 1s and the shared secret is just ints 0-31...?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, sorry my bad. I forgot that I updated this in Rust. Yes, the values are fixed. But since the values does not have special numerical meanings, in relation to the algorithm, they are still counted as random (or you may say simi-random). Hey @cygnusv, Could you please, provide your input if testing with new random values is needed for this cryptography validation.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Just to clarify: the test vectors should still contain specific values, but I expected each vector in the generated file to use different values for completeness. These values would originally have been generated randomly or pseudo-randomly. For example, to generate the test vector file, you could use a fixed seed. Then, using that seed, you can deterministically generate pseudo-random nonces, shared secrets, etc., rather than reusing the same values or incrementing bytes only sequentially. The seed approach ensures that the same file is generated every time (due to the fixed seed), but each test vector contains distinct, realistic inputs like unique nonces and shared secrets. This avoids test vectors that only differ trivially (e.g. same nonce in every case), which could miss edge cases or failures.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, in all cases, the typescript tests should run on any provided data in this json 😄 . So, whatever will be generated from nucypher-core, should be usable here regardless of what the values are.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. I'm referring to the strategy used in the code that generated the test vectors from
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would like continue this the conversation about the generated test vectors at nucypher/nucypher-core#105.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for being a bit late to the discussion. I've been following along while working on nucypher/nucypher-core#105 and I changed my mind a few times during this process. The production and consumption of test vectors are a bit more complicated than expected due to how our internal protocol objects work, since in some cases serialization/deserialization is more difficult or not possible without adding code. I'm still iterating a proper method for producing test vectors in my PR, but on the PRs in taco-web we should strive to consume test vectors without resorting to duplicated implementations.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With respect to the specific issue discussed here, the test vector ideally should be generated in a 100% deterministic way (i.e. using a seed). However, there's nothing wrong about adding some hard-coded, obviously not random examples, like |
||
| { | ||
| "id": "vector1", | ||
| "description": "Fixed nonce encryption with known plaintext", | ||
| "shared_secret": [ | ||
| 0, | ||
|
derekpierre marked this conversation as resolved.
Outdated
|
||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| 6, | ||
| 7, | ||
| 8, | ||
| 9, | ||
| 10, | ||
| 11, | ||
| 12, | ||
| 13, | ||
| 14, | ||
| 15, | ||
| 16, | ||
| 17, | ||
| 18, | ||
| 19, | ||
| 20, | ||
| 21, | ||
| 22, | ||
| 23, | ||
| 24, | ||
| 25, | ||
| 26, | ||
| 27, | ||
| 28, | ||
| 29, | ||
| 30, | ||
| 31 | ||
| ], | ||
| "plaintext": "This is a test message", | ||
| "fixed_nonce": [ | ||
| 0, | ||
| 0, | ||
| 0, | ||
| 0, | ||
| 0, | ||
| 0, | ||
| 0, | ||
| 0, | ||
| 0, | ||
| 0, | ||
| 0, | ||
| 0 | ||
| ], | ||
| "expected_ciphertext": "0000000000000000000000004cd02b428d8fd5f172412804dc376e4a9dc2809486c8b548d407dad8f75ee84d6d9e92a42c4a" | ||
| }, | ||
| { | ||
| "id": "vector2", | ||
| "description": "Fixed nonce encryption with alternative values", | ||
| "shared_secret": [ | ||
| 31, | ||
| 30, | ||
| 29, | ||
| 28, | ||
| 27, | ||
| 26, | ||
| 25, | ||
| 24, | ||
| 23, | ||
| 22, | ||
| 21, | ||
| 20, | ||
| 19, | ||
| 18, | ||
| 17, | ||
| 16, | ||
| 15, | ||
| 14, | ||
| 13, | ||
| 12, | ||
| 11, | ||
| 10, | ||
| 9, | ||
| 8, | ||
| 7, | ||
| 6, | ||
| 5, | ||
| 4, | ||
| 3, | ||
| 2, | ||
| 1, | ||
| 0 | ||
| ], | ||
| "plaintext": "", | ||
| "fixed_nonce": [ | ||
| 1, | ||
| 1, | ||
| 1, | ||
| 1, | ||
| 1, | ||
| 1, | ||
| 1, | ||
| 1, | ||
| 1, | ||
| 1, | ||
| 1, | ||
| 1 | ||
| ], | ||
| "expected_ciphertext": "0101010101010101010101018726c5f2d5b81872a86493ef5232aaa1" | ||
| }, | ||
| { | ||
| "id": "vector3", | ||
| "description": "Rust-generated ciphertext for TypeScript compatibility check", | ||
| "shared_secret": [ | ||
| 0, | ||
| 1, | ||
| 2, | ||
| 3, | ||
| 4, | ||
| 5, | ||
| 6, | ||
| 7, | ||
| 8, | ||
| 9, | ||
| 10, | ||
| 11, | ||
| 12, | ||
| 13, | ||
| 14, | ||
| 15, | ||
| 16, | ||
| 17, | ||
| 18, | ||
| 19, | ||
| 20, | ||
| 21, | ||
| 22, | ||
| 23, | ||
| 24, | ||
| 25, | ||
| 26, | ||
| 27, | ||
| 28, | ||
| 29, | ||
| 30, | ||
| 31 | ||
| ], | ||
| "rust_generated_ciphertext": [ | ||
| 87, | ||
| 119, | ||
| 14, | ||
| 255, | ||
| 68, | ||
| 116, | ||
| 249, | ||
| 152, | ||
| 58, | ||
| 121, | ||
| 245, | ||
| 185, | ||
| 157, | ||
| 62, | ||
| 23, | ||
| 3, | ||
| 50, | ||
| 163, | ||
| 150, | ||
| 212, | ||
| 167, | ||
| 157, | ||
| 244, | ||
| 227, | ||
| 159, | ||
| 130, | ||
| 110, | ||
| 26, | ||
| 30, | ||
| 161, | ||
| 173, | ||
| 70, | ||
| 232, | ||
| 207, | ||
| 110, | ||
| 161, | ||
| 252, | ||
| 228, | ||
| 149, | ||
| 42, | ||
| 173, | ||
| 185, | ||
| 61, | ||
| 157, | ||
| 144, | ||
| 163, | ||
| 128, | ||
| 200, | ||
| 87, | ||
| 80, | ||
| 172, | ||
| 63, | ||
| 235, | ||
| 241, | ||
| 169, | ||
| 124, | ||
| 248, | ||
| 158, | ||
| 213, | ||
| 62, | ||
| 241, | ||
| 209, | ||
| 117, | ||
| 216, | ||
| 202, | ||
| 230, | ||
| 106, | ||
| 248, | ||
| 170, | ||
| 150, | ||
| 126, | ||
| 40, | ||
| 203, | ||
| 34, | ||
| 14, | ||
| 44, | ||
| 71, | ||
| 186, | ||
| 234, | ||
| 92, | ||
| 13, | ||
| 248 | ||
| ], | ||
| "expected_plaintext": "This is a message encrypted by the Rust implementation" | ||
| } | ||
| ] | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.