Skip to content

Commit 1788d25

Browse files
authored
fix: plaintextLength must be enforced (#213)
If the user has expressed a plaintext length, this value MUST not be exceed.
1 parent a466851 commit 1788d25

File tree

5 files changed

+46
-6
lines changed

5 files changed

+46
-6
lines changed

modules/encrypt-node/src/encrypt.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@ export async function encrypt (
2626
plaintext: Buffer|Uint8Array|Readable|string|NodeJS.ReadableStream,
2727
op: EncryptInput = {}
2828
): Promise<EncryptOutput> {
29-
const stream = encryptStream(cmm, op)
3029
const { encoding } = op
30+
if (plaintext instanceof Uint8Array) {
31+
op.plaintextLength = plaintext.byteLength
32+
} else if (typeof plaintext === 'string') {
33+
plaintext = Buffer.from(plaintext, encoding)
34+
op.plaintextLength = plaintext.byteLength
35+
}
3136

37+
const stream = encryptStream(cmm, op)
3238
const result: Buffer[] = []
3339
let messageHeader: MessageHeader|false = false
3440
stream
@@ -38,8 +44,6 @@ export async function encrypt (
3844
// This will check both Uint8Array|Buffer
3945
if (plaintext instanceof Uint8Array) {
4046
stream.end(plaintext)
41-
} else if (typeof plaintext === 'string') {
42-
stream.end(Buffer.from(plaintext, encoding))
4347
} else if (plaintext.readable) {
4448
plaintext.pipe(stream)
4549
} else {

modules/encrypt-node/src/encrypt_stream.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function encryptStream (
7878

7979
wrappingStream.emit('MessageHeader', messageHeader)
8080

81-
const encryptStream = getFramedEncryptStream(getCipher, messageHeader, dispose)
81+
const encryptStream = getFramedEncryptStream(getCipher, messageHeader, dispose, plaintextLength)
8282
const signatureStream = new SignatureStream(getSigner)
8383

8484
pipeline(encryptStream, signatureStream)

modules/encrypt-node/src/framed_encrypt_stream.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515

1616
import {
1717
serializeFactory, aadFactory,
18-
MessageHeader // eslint-disable-line no-unused-vars
18+
MessageHeader, // eslint-disable-line no-unused-vars
19+
Maximum
1920
} from '@aws-crypto/serialize'
2021
// @ts-ignore
2122
import { Transform as PortableTransform } from 'readable-stream'
@@ -49,11 +50,17 @@ const ioTick = () => new Promise(resolve => setImmediate(resolve))
4950
const noop = () => {}
5051
type ErrBack = (err?: Error) => void
5152

52-
export function getFramedEncryptStream (getCipher: GetCipher, messageHeader: MessageHeader, dispose: Function) {
53+
export function getFramedEncryptStream (getCipher: GetCipher, messageHeader: MessageHeader, dispose: Function, plaintextLength?: number) {
5354
let accumulatingFrame: AccumulatingFrame = { contentLength: 0, content: [], sequenceNumber: 1 }
5455
let pathologicalDrain: Function = noop
5556
const { frameLength } = messageHeader
5657

58+
/* Precondition: plaintextLength must be within bounds.
59+
* The Maximum.BYTES_PER_MESSAGE is set to be within Number.MAX_SAFE_INTEGER
60+
* See serialize/identifiers.ts enum Maximum for more details.
61+
*/
62+
needs(!plaintextLength || (plaintextLength >= 0 && Maximum.BYTES_PER_MESSAGE >= plaintextLength), 'plaintextLength out of bounds.')
63+
5764
/* Keeping the messageHeader, accumulatingFrame and pathologicalDrain private is the intention here.
5865
* It is already unlikely that these values could be touched in the current composition of streams,
5966
* but a different composition may change this.
@@ -63,6 +70,11 @@ export function getFramedEncryptStream (getCipher: GetCipher, messageHeader: Mes
6370
_transform (chunk: Buffer, encoding: string, callback: ErrBack) {
6471
const contentLeft = frameLength - accumulatingFrame.contentLength
6572

73+
/* Precondition: Must not process more than plaintextLength.
74+
* The plaintextLength is the MAXIMUM value that can be encrypted.
75+
*/
76+
needs(!plaintextLength || (plaintextLength -= chunk.length) >= 0, 'Encrypted data exceeded plaintextLength.')
77+
6678
/* Check for early return (Postcondition): Have not accumulated a frame. */
6779
if (contentLeft > chunk.length) {
6880
// eat more

modules/encrypt-node/test/framed_encrypt_stream.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,24 @@ describe('getFramedEncryptStream', () => {
3030
expect(test._transform).is.a('function')
3131
})
3232

33+
it('Precondition: plaintextLength must be within bounds.', () => {
34+
const getCipher: any = () => {}
35+
expect(() => getFramedEncryptStream(getCipher, {} as any, () => {}, -1)).to.throw(Error, 'plaintextLength out of bounds.')
36+
expect(() => getFramedEncryptStream(getCipher, {} as any, () => {}, Number.MAX_SAFE_INTEGER + 1)).to.throw(Error, 'plaintextLength out of bounds.')
37+
38+
/* Math is hard.
39+
* I want to make sure that I don't have an errant off by 1 error.
40+
*/
41+
expect(() => getFramedEncryptStream(getCipher, {} as any, () => {}, Number.MAX_SAFE_INTEGER)).to.not.throw(Error)
42+
})
43+
44+
it('Precondition: Must not process more than plaintextLength.', () => {
45+
const getCipher: any = () => {}
46+
const test = getFramedEncryptStream(getCipher, { } as any, () => {}, 8)
47+
48+
expect(() => test._transform(Buffer.from(Array(9)), 'binary', () => {})).to.throw(Error, 'Encrypted data exceeded plaintextLength.')
49+
})
50+
3351
it('Check for early return (Postcondition): Have not accumulated a frame.', () => {
3452
const getCipher: any = () => {}
3553
const frameLength = 10

modules/serialize/src/identifiers.ts

+6
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ export enum Maximum {
8080
* or some value larger 2 ** 63.
8181
*/
8282
BYTES_PER_CACHED_KEY_LIMIT = 2 ** 53 - 1, // eslint-disable-line no-unused-vars
83+
/* This value should be Maximum.FRAME_COUNT * Maximum.FRAME_SIZE.
84+
* However this would be ~ 2 ** 64, much larger than Number.MAX_SAFE_INTEGER.
85+
* For the same reasons outlined above in BYTES_PER_CACHED_KEY_LIMIT
86+
* this value is set to 2 ** 53 - 1.
87+
*/
88+
BYTES_PER_MESSAGE = 2 ** 53 - 1, // eslint-disable-line no-unused-vars
8389
// Maximum number of frames allowed in one message as defined in specification
8490
FRAME_COUNT = 2 ** 32 - 1, // eslint-disable-line no-unused-vars
8591
// Maximum bytes allowed in a single frame as defined in specification

0 commit comments

Comments
 (0)