Skip to content

Commit adeb734

Browse files
authored
deterministic key generation using pseudo randomness (Hopding#1033)
* deterministic key generation using pseudo randomness * linter on the psuedo random commit
1 parent 07bf247 commit adeb734

14 files changed

+76
-62
lines changed

README.md

+8-8
Original file line numberDiff line numberDiff line change
@@ -493,15 +493,15 @@ const secondDonorPdfBytes = ...
493493
const firstDonorPdfDoc = await PDFDocument.load(firstDonorPdfBytes)
494494
const secondDonorPdfDoc = await PDFDocument.load(secondDonorPdfBytes)
495495

496-
// Copy the 1st page from the first donor document, and
496+
// Copy the 1st page from the first donor document, and
497497
// the 743rd page from the second donor document
498498
const [firstDonorPage] = await pdfDoc.copyPages(firstDonorPdfDoc, [0])
499499
const [secondDonorPage] = await pdfDoc.copyPages(secondDonorPdfDoc, [742])
500500

501501
// Add the first copied page
502502
pdfDoc.addPage(firstDonorPage)
503503

504-
// Insert the second copied page to index 0, so it will be the
504+
// Insert the second copied page to index 0, so it will be the
505505
// first page in `pdfDoc`
506506
pdfDoc.insertPage(0, secondDonorPage)
507507

@@ -606,11 +606,11 @@ const preamble = await pdfDoc.embedPage(usConstitutionPdf.getPages()[1], {
606606
top: 575,
607607
})
608608

609-
// Get the width/height of the American flag PDF scaled down to 30% of
609+
// Get the width/height of the American flag PDF scaled down to 30% of
610610
// its original size
611611
const americanFlagDims = americanFlag.scale(0.3)
612612

613-
// Get the width/height of the preamble clipping scaled up to 225% of
613+
// Get the width/height of the preamble clipping scaled up to 225% of
614614
// its original size
615615
const preambleDims = preamble.scale(2.25)
616616

@@ -813,8 +813,8 @@ import { PDFDocument } from 'pdf-lib'
813813
const existingPdfBytes = ...
814814

815815
// Load a PDFDocument without updating its existing metadata
816-
const pdfDoc = await PDFDocument.load(existingPdfBytes, {
817-
updateMetadata: false
816+
const pdfDoc = await PDFDocument.load(existingPdfBytes, {
817+
updateMetadata: false
818818
})
819819

820820
// Print all available metadata fields
@@ -1228,7 +1228,7 @@ When working with PDFs, you will frequently come across the terms "character enc
12281228
const pdfDoc = await PDFDocument.create()
12291229
const courierFont = await pdfDoc.embedFont(StandardFonts.Courier)
12301230
const page = pdfDoc.addPage()
1231-
page.drawText('Some boring latin text in the Courier font', {
1231+
page.drawText('Some boring latin text in the Courier font', {
12321232
font: courierFont,
12331233
})
12341234
```
@@ -1248,7 +1248,7 @@ When working with PDFs, you will frequently come across the terms "character enc
12481248
const ubuntuFont = await pdfDoc.embedFont(fontBytes)
12491249

12501250
const page = pdfDoc.addPage()
1251-
page.drawText('Some fancy Unicode text in the ŪЬȕǹƚü font', {
1251+
page.drawText('Some fancy Unicode text in the ŪЬȕǹƚü font', {
12521252
font: ubuntuFont,
12531253
})
12541254
```

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"DkDavid (https://github.com/DkDavid)",
2929
"Bj Tecu (https://github.com/btecu)",
3030
"Brent McSharry (https://github.com/mcshaz)",
31-
"Tim Knapp (https://github.com/duffyd)"
31+
"Tim Knapp (https://github.com/duffyd)",
32+
"Ching Chang (https://github.com/ChingChang9)"
3233
],
3334
"scripts": {
3435
"release:latest": "yarn publish --tag latest && yarn pack && yarn release:tag",

src/api/PDFPage.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ import {
4444
PDFArray,
4545
} from 'src/core';
4646
import {
47-
addRandomSuffix,
4847
assertEachIs,
4948
assertIs,
5049
assertMultiple,
@@ -700,7 +699,7 @@ export default class PDFPage {
700699
// TODO: Reuse image Font name if we've already added this image to Resources.Fonts
701700
assertIs(font, 'font', [[PDFFont, 'PDFFont']]);
702701
this.font = font;
703-
this.fontKey = addRandomSuffix(this.font.name);
702+
this.fontKey = this.doc.context.addRandomSuffix(this.font.name);
704703
this.node.setFontDictionary(PDFName.of(this.fontKey), this.font.ref);
705704
}
706705

@@ -1060,8 +1059,7 @@ export default class PDFPage {
10601059
assertRangeOrUndefined(options.opacity, 'opacity.opacity', 0, 1);
10611060
assertIsOneOfOrUndefined(options.blendMode, 'options.blendMode', BlendMode);
10621061

1063-
const xObjectKey = addRandomSuffix('Image', 10);
1064-
this.node.setXObject(PDFName.of(xObjectKey), image.ref);
1062+
const xObjectKey = this.doc.context.addRandomSuffix('Image', 10);
10651063

10661064
const graphicsStateKey = this.maybeEmbedGraphicsState({
10671065
opacity: options.opacity,
@@ -1135,7 +1133,7 @@ export default class PDFPage {
11351133
assertRangeOrUndefined(options.opacity, 'opacity.opacity', 0, 1);
11361134
assertIsOneOfOrUndefined(options.blendMode, 'options.blendMode', BlendMode);
11371135

1138-
const xObjectKey = addRandomSuffix('EmbeddedPdfPage', 10);
1136+
const xObjectKey = this.doc.context.addRandomSuffix('EmbeddedPdfPage', 10);
11391137
this.node.setXObject(PDFName.of(xObjectKey), embeddedPage.ref);
11401138

11411139
const graphicsStateKey = this.maybeEmbedGraphicsState({
@@ -1592,7 +1590,7 @@ export default class PDFPage {
15921590
return undefined;
15931591
}
15941592

1595-
const key = addRandomSuffix('GS', 10);
1593+
const key = this.doc.context.addRandomSuffix('GS', 10);
15961594

15971595
const graphicsState = this.doc.context.obj({
15981596
Type: 'ExtGState',

src/api/form/PDFField.ts

+3-8
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,7 @@ import {
2222
PDFAcroTerminal,
2323
AnnotationFlags,
2424
} from 'src/core';
25-
import {
26-
addRandomSuffix,
27-
assertIs,
28-
assertMultiple,
29-
assertOrUndefined,
30-
} from 'src/utils';
25+
import { assertIs, assertMultiple, assertOrUndefined } from 'src/utils';
3126
import { ImageAlignment } from '../image';
3227
import PDFImage from '../PDFImage';
3328
import { drawImage, rotateInPlace } from '../operations';
@@ -321,7 +316,7 @@ export default class PDFField {
321316
);
322317
widget.setRectangle(rect);
323318

324-
if(typeof pageRef !== 'undefined'){
319+
if (typeof pageRef !== 'undefined') {
325320
widget.setP(pageRef);
326321
}
327322

@@ -495,7 +490,7 @@ export default class PDFField {
495490
options.y = adj.height - borderWidth - imageDims.height;
496491
}
497492

498-
const imageName = addRandomSuffix('Image', 10);
493+
const imageName = this.doc.context.addRandomSuffix('Image', 10);
499494
const appearance = [...rotate, ...drawImage(imageName, options)];
500495
////////////
501496

src/api/form/PDFForm.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
PDFName,
4242
PDFWidgetAnnotation,
4343
} from 'src/core';
44-
import { addRandomSuffix, assertIs, Cache, assertOrUndefined } from 'src/utils';
44+
import { assertIs, Cache, assertOrUndefined } from 'src/utils';
4545

4646
export interface FlattenOptions {
4747
updateFieldAppearances: boolean;
@@ -550,7 +550,7 @@ export default class PDFForm {
550550
const page = this.findWidgetPage(widget);
551551
const widgetRef = this.findWidgetAppearanceRef(field, widget);
552552

553-
const xObjectKey = addRandomSuffix('FlatWidget', 10);
553+
const xObjectKey = this.doc.context.addRandomSuffix('FlatWidget', 10);
554554
page.node.setXObject(PDFName.of(xObjectKey), widgetRef);
555555

556556
const rectangle = widget.getRectangle();

src/api/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export * from 'src/api/form';
22
export * from 'src/api/text';
33
export * from 'src/api/colors';
44
export * from 'src/api/errors';
5-
export * from "src/api/image";
5+
export * from 'src/api/image';
66
export * from 'src/api/objects';
77
export * from 'src/api/operations';
88
export * from 'src/api/operators';

src/api/operations.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -443,19 +443,19 @@ export const rotateInPlace = (options: {
443443
rotation: 0 | 90 | 180 | 270;
444444
}) =>
445445
options.rotation === 0 ? [
446-
translate(0, 0),
447-
rotateDegrees(0)
446+
translate(0, 0),
447+
rotateDegrees(0)
448448
]
449449
: options.rotation === 90 ? [
450-
translate(options.width, 0),
450+
translate(options.width, 0),
451451
rotateDegrees(90)
452452
]
453453
: options.rotation === 180 ? [
454-
translate(options.width, options.height),
454+
translate(options.width, options.height),
455455
rotateDegrees(180)
456456
]
457457
: options.rotation === 270 ? [
458-
translate(0, options.height),
458+
translate(0, options.height),
459459
rotateDegrees(270)
460460
]
461461
: []; // Invalid rotation - noop

src/core/PDFContext.ts

+6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import PDFOperator from 'src/core/operators/PDFOperator';
1818
import Ops from 'src/core/operators/PDFOperatorNames';
1919
import PDFContentStream from 'src/core/structures/PDFContentStream';
2020
import { typedArrayFor } from 'src/utils';
21+
import { SimpleRNG } from 'src/utils/rng';
2122

2223
type LookupKey = PDFRef | PDFObject | undefined;
2324

@@ -54,6 +55,7 @@ class PDFContext {
5455
Info?: PDFObject;
5556
ID?: PDFObject;
5657
};
58+
rng: SimpleRNG;
5759

5860
private readonly indirectObjects: Map<PDFRef, PDFObject>;
5961

@@ -66,6 +68,7 @@ class PDFContext {
6668
this.trailerInfo = {};
6769

6870
this.indirectObjects = new Map();
71+
this.rng = new SimpleRNG(1);
6972
}
7073

7174
assign(ref: PDFRef, object: PDFObject): void {
@@ -287,6 +290,9 @@ class PDFContext {
287290
this.popGraphicsStateContentStreamRef = this.register(stream);
288291
return this.popGraphicsStateContentStreamRef;
289292
}
293+
294+
addRandomSuffix = (prefix: string, suffixLength = 4) =>
295+
`${prefix}-${Math.floor(this.rng.nextInt() * 10 ** suffixLength)}`;
290296
}
291297

292298
export default PDFContext;

src/core/annotation/PDFWidgetAnnotation.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ class PDFWidgetAnnotation extends PDFAnnotation {
4646
return undefined;
4747
}
4848

49-
setP(page: PDFRef){
50-
this.dict.set(PDFName.of('P'),page);
49+
setP(page: PDFRef) {
50+
this.dict.set(PDFName.of('P'), page);
5151
}
5252

5353
setDefaultAppearance(appearance: string) {

src/core/embedders/CustomFontEmbedder.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import PDFRef from 'src/core/objects/PDFRef';
77
import PDFString from 'src/core/objects/PDFString';
88
import PDFContext from 'src/core/PDFContext';
99
import {
10-
addRandomSuffix,
1110
byAscendingId,
1211
Cache,
1312
sortedUniq,
@@ -106,7 +105,8 @@ class CustomFontEmbedder {
106105
}
107106

108107
embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> {
109-
this.baseFontName = this.customName || addRandomSuffix(this.fontName);
108+
this.baseFontName =
109+
this.customName || context.addRandomSuffix(this.fontName);
110110
return this.embedFontDict(context, ref);
111111
}
112112

src/utils/rng.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Generates a pseudo random number. Although it is not cryptographically secure
3+
* and uniformly distributed, it is not a concern for the intended use-case,
4+
* which is to generate distinct numbers.
5+
*
6+
* Credit: https://stackoverflow.com/a/19303725/10254049
7+
*/
8+
export class SimpleRNG {
9+
private seed = 1;
10+
11+
constructor(seed: number) {
12+
this.seed = seed;
13+
}
14+
15+
nextInt = () => {
16+
const x = Math.sin(this.seed++) * 10000;
17+
return x - Math.floor(x);
18+
};
19+
}

src/utils/strings.ts

-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ export const copyStringIntoBuffer = (
3131
return length;
3232
};
3333

34-
export const addRandomSuffix = (prefix: string, suffixLength = 4) =>
35-
`${prefix}-${Math.floor(Math.random() * 10 ** suffixLength)}`;
36-
3734
export const escapeRegExp = (str: string) =>
3835
str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
3936

tests/api/PDFDocument.spec.ts

+8-24
Original file line numberDiff line numberDiff line change
@@ -142,39 +142,21 @@ describe(`PDFDocument`, () => {
142142
});
143143

144144
describe(`embedFont() method`, () => {
145-
it(`serializes the same value on every save when using a custom font name`, async () => {
145+
it(`serializes the same value on every save`, async () => {
146146
const customFont = fs.readFileSync('assets/fonts/ubuntu/Ubuntu-B.ttf');
147-
const customName = 'Custom-Font-Name';
148147
const pdfDoc1 = await PDFDocument.create({ updateMetadata: false });
149148
const pdfDoc2 = await PDFDocument.create({ updateMetadata: false });
150149

151150
pdfDoc1.registerFontkit(fontkit);
152151
pdfDoc2.registerFontkit(fontkit);
153152

154-
await pdfDoc1.embedFont(customFont, { customName });
155-
await pdfDoc2.embedFont(customFont, { customName });
156-
157-
const savedDoc1 = await pdfDoc1.save();
158-
const savedDoc2 = await pdfDoc2.save();
159-
160-
expect(savedDoc1).toEqual(savedDoc2);
161-
});
162-
163-
it(`does not serialize the same on save when not using a custom font name`, async () => {
164-
const customFont = fs.readFileSync('assets/fonts/ubuntu/Ubuntu-B.ttf');
165-
const pdfDoc1 = await PDFDocument.create();
166-
const pdfDoc2 = await PDFDocument.create();
167-
168-
pdfDoc1.registerFontkit(fontkit);
169-
pdfDoc2.registerFontkit(fontkit);
170-
171153
await pdfDoc1.embedFont(customFont);
172154
await pdfDoc2.embedFont(customFont);
173155

174156
const savedDoc1 = await pdfDoc1.save();
175157
const savedDoc2 = await pdfDoc2.save();
176158

177-
expect(savedDoc1).not.toEqual(savedDoc2);
159+
expect(savedDoc1).toEqual(savedDoc2);
178160
});
179161
});
180162

@@ -516,19 +498,21 @@ describe(`copy() method`, () => {
516498
srcDoc.setModificationDate(modificationDate);
517499
pdfDoc = await srcDoc.copy();
518500
});
519-
501+
520502
it(`Returns a pdf with the same number of pages`, async () => {
521503
expect(pdfDoc.getPageCount()).toBe(srcDoc.getPageCount());
522504
});
523-
505+
524506
it(`Can copy author, creationDate, creator, producer, subject, title, defaultWordBreaks`, async () => {
525507
expect(pdfDoc.getAuthor()).toBe(srcDoc.getAuthor());
526508
expect(pdfDoc.getCreationDate()).toStrictEqual(srcDoc.getCreationDate());
527509
expect(pdfDoc.getCreator()).toBe(srcDoc.getCreator());
528-
expect(pdfDoc.getModificationDate()).toStrictEqual(srcDoc.getModificationDate());
510+
expect(pdfDoc.getModificationDate()).toStrictEqual(
511+
srcDoc.getModificationDate(),
512+
);
529513
expect(pdfDoc.getProducer()).toBe(srcDoc.getProducer());
530514
expect(pdfDoc.getSubject()).toBe(srcDoc.getSubject());
531515
expect(pdfDoc.getTitle()).toBe(srcDoc.getTitle());
532516
expect(pdfDoc.defaultWordBreaks).toEqual(srcDoc.defaultWordBreaks);
533517
});
534-
});
518+
});

tests/utils/rng.spec.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { SimpleRNG } from 'src/utils/rng';
2+
3+
describe(`psuedo random numbers`, () => {
4+
it(`generates distinct numbers`, () => {
5+
const rng = new SimpleRNG(1);
6+
expect(rng.nextInt()).not.toEqual(rng.nextInt());
7+
});
8+
9+
it(`generates the same number across different SimpleRNG`, () => {
10+
const rng = new SimpleRNG(1);
11+
expect(rng.nextInt()).toEqual(0.7098480789645691);
12+
expect(rng.nextInt()).toEqual(0.9742682568175951);
13+
});
14+
});

0 commit comments

Comments
 (0)