Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
"dependencies": {
"@ethersproject/abi": "^5.8.0",
"@ethersproject/providers": "^5.8.0",
"@noble/ciphers": "^1.2.1",
"@noble/hashes": "^1.7.1",
"@nucypher/nucypher-contracts": "^0.23.0",
"@nucypher/nucypher-core": "^0.14.5",
"axios": "^1.8.4",
Expand Down
66 changes: 66 additions & 0 deletions packages/shared/src/encryption/errors.ts
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',
}
2 changes: 2 additions & 0 deletions packages/shared/src/encryption/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './errors';
export * from './shared-secret';
44 changes: 44 additions & 0 deletions packages/shared/src/encryption/shared-secret.ts
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
Comment thread
Muhammad-Altabba marked this conversation as resolved.
Outdated
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);
}
}
235 changes: 235 additions & 0 deletions packages/shared/test/fixtures/shared-secret-vectors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
{
"test_vectors": [
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

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...?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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).
However, we can generate different values every time. But it will make debugging harder in case of an issue.

Hey @cygnusv, Could you please, provide your input if testing with new random values is needed for this cryptography validation.

Copy link
Copy Markdown
Member

@derekpierre derekpierre Apr 16, 2025

Choose a reason for hiding this comment

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

Yes, the values are fixed.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 nucypher-core.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.
Thanks,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 [1, 1, 1, .....1].

{
"id": "vector1",
"description": "Fixed nonce encryption with known plaintext",
"shared_secret": [
0,
Comment thread
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"
}
]
}
Loading