-
-
Notifications
You must be signed in to change notification settings - Fork 9
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
Issure with isAligned32 in getDst with shared ArrayBuffer #47
Comments
Could you provide a test vector? |
const data = new Uint8Array(41).fill(5);
const key = new Uint8Array(32).fill(2);
const nonce = new Uint8Array(24).fill(3);
const chacha = xchacha20poly1305(key, nonce)
console.log('initial', data.subarray(0, 8));
const output = data.subarray(8, 32);
const ciphertext = chacha.encrypt(data.subarray(0, 8), output);
console.log('encrypted', ciphertext, output);
chacha.decrypt(ciphertext, data.subarray(32, 40));
console.log('decrypted', data.subarray(32, 40));
deepStrictEqual(data.subarray(0, 8), data.subarray(32, 40)) This works properly. |
Fixed in dfa5ba3 |
I have changed your example to illustrate the issue. const data = new Uint8Array(41).fill(5);
const key = new Uint8Array(32).fill(2);
const nonce = new Uint8Array(24).fill(3);
const chacha = xchacha20poly1305(key, nonce);
console.log("initial", data.subarray(0, 8));
const output = data.subarray(8, 32);
const ciphertext = chacha.encrypt(data.subarray(0, 8), output);
console.log("encrypted", ciphertext, output);
// Later on the receiver side receives the ciphertext in a buffer at an arbitrary offset
// In my use case this is a CBOR-decoded byte array backed by a large buffer
const buffer = new Uint8Array(256).fill(0);
const offset = 31; // <== this offset must be aligned in 1.1.0 (so it will fail here). It works just fine with 1.0.0
buffer.set(ciphertext, offset);
{
const ciphertext = buffer.subarray(offset, offset + 24); // 8 bytes + 16 bytes tag
const output = buffer.subarray(offset, offset + 24 - 16); // 8 bytes
chacha.decrypt(ciphertext, output);
console.log("decrypted", output);
expect(data.subarray(0, 8)).toEqual(output);
} Haven't tried the change in dfa5ba3 yet. |
Tested the modified example above with 1.1.1, and the failure is now "Invalid tag". export function getOutput(expectedLength: number, out?: Uint8Array, onlyAligned = true) {
if (out === undefined) return new Uint8Array(expectedLength);
if (out.length !== expectedLength)
throw new Error(
'invalid output length, expected at least ' + expectedLength + ', got: ' + out.length
);
if (onlyAligned && !isAligned32(out)) throw new Error('invalid output, must be aligned');
return out.subarray(0, expectedLength).fill(0);
} in the last line creates a subarray of "out" of the same length and fills it with zero. Removing the I guess |
@atomictag removing |
7e444bf ensures
Reusing input + output makes sense for stream ciphers (plain salsa, plain chacha, plain ctr), however, it's much harder to get correctly working for everyone else. |
As far as I can tell (just ran a quick test with 1.1.2) the sample code in the README "Reuse array for input and output" fails import { chacha20poly1305 } from '@noble/ciphers/chacha';
import { utf8ToBytes } from '@noble/ciphers/utils';
import { randomBytes } from '@noble/ciphers/webcrypto';
const key = randomBytes(32);
const nonce = randomBytes(12);
const inputLength = 12;
const tagLength = 16;
const buf = new Uint8Array(inputLength + tagLength);
const _data = utf8ToBytes('hello, noble'); // length == 12
buf.set(_data, 0); // first inputLength bytes
const _start = buf.subarray(0, inputLength);
const chacha = chacha20poly1305(key, nonce);
chacha.encrypt(_start, buf);
chacha.decrypt(buf, _start); // _start now same as _data This was also failing with 1.1.1 with the same The way 1.0.0 worked was perfect - if ciphertext is in 1Mb I don't need to allocate another 1M to decrypt. May I ask what requirements triggered this change in behavior between 1.0.0 and 1.1.0? Many thanks |
It only worked in ONE cipher. If it works in one case and not the other then it is unreliable. Why can’t you use one big array with separate parts for input and output? |
what are params of your system? |
The docs weren't very clear on which cypher supported the "Reuse array for input and output" feature, but the example in the README is using Anyways, AFAICT the exact code in the README (which uses
It's a bit complicated but in a nutshell imagine a pipeline of thousands of messages ranging from a few hundred bytes to megabytes streamed as CBOR envelopes containing encrypted bytes - from response buffer to decoded output not a single additional byte is allocated (CBOR decoding and decryption are both "in-place").
Of course I could arrange a scratch buffer for decryption but that would impact some aspects of the system and break some other parts - so I am wondering if it is worth taking the risk for the sake of staying up to date with my preferred cipher toolkit, or whether I should stick to 1.0.0 which served me well so far until I fully understand what has changed between 1.0.0 and 1.1.x to break this Note that I totally understand that the Many thanks |
readme example fixed |
Thank you for the awesome library - and glad it got an official audit!
Using
xchacha20poly1305
here.The
getDst
utility introduced in 1.1.0 now performs the checkisAligned32(dst)
which checks the alignment on the byteOffset on the underlying ArrayBuffer (not exactly sure why on the byteOffset - see below).
This breaks in 1.1.0 if the backing buffer contains more data than just the ciphertext, if it happens to be misaligned.
No issue with 1.0.0.
Say the raw byte array input for decryption contains concatenated metadata + nonce + data.
Now extract 3 subarrays "metadata", "nonce", "data", where the "data" subarray contains ciphertext and tag, so
However now
output.byteOffset
depends on whatever was in the raw byte array input (incl. "metadata"), so it is not guaranteed to be aligned at all, therefore theisAligned32
check fails.The text was updated successfully, but these errors were encountered: