-
Notifications
You must be signed in to change notification settings - Fork 2
/
script.js
371 lines (320 loc) · 10.6 KB
/
script.js
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
// credits https://github.com/vbuch/node-signpdf/blob/master/src/signpdf.test.js
// credits https://stackoverflow.com/questions/15969733/verify-pkcs7-pem-signature-unpack-data-in-node-js/16148331#16148331
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const forge = require('node-forge');
const PDFDocument = require('pdfkit');
const DEFAULT_BYTE_RANGE_PLACEHOLDER = '**********';
const certPath = path.join(__dirname, 'certs', 'certificate.p12');
const PKCS12_CERT_BAG = '1.2.840.113549.1.12.10.1.3';
const PKCS12_KEY_BAG = '1.2.840.113549.1.12.10.1.2';
let signedPDFFilePath = path.join(__dirname, 'output', `signed.pdf`);
const addSignaturePlaceholder = ({
pdf,
reason,
signatureLength = 4096
}) => {
/* eslint-disable no-underscore-dangle,no-param-reassign */
// Generate the signature placeholder
const signature = pdf.ref({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
SubFilter: 'adbe.pkcs7.detached',
ByteRange: [
0,
DEFAULT_BYTE_RANGE_PLACEHOLDER,
DEFAULT_BYTE_RANGE_PLACEHOLDER,
DEFAULT_BYTE_RANGE_PLACEHOLDER,
],
Contents: Buffer.from(String.fromCharCode(0).repeat(signatureLength)),
Reason: new String(reason),
M: new Date(),
});
// Generate signature annotation widget
const widget = pdf.ref({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Rect: [0, 0, 0, 0],
V: signature,
T: new String('Signature1'), // eslint-disable-line no-new-wrappers
F: 4,
P: pdf._root.data.Pages.data.Kids[0], // eslint-disable-line no-underscore-dangle
});
// Include the widget in a page
pdf._root.data.Pages.data.Kids[0].data.Annots = [widget];
// Create a form (with the widget) and link in the _root
const form = pdf.ref({
SigFlags: 3,
Fields: [widget],
});
pdf._root.data.AcroForm = form;
return {
signature,
form,
widget,
};
/* eslint-enable no-underscore-dangle,no-param-reassign */
};
const creatPDF = (invoiceObj) => new Promise((resolve) => {
const doc = new PDFDocument({
autoFirstPage: true,
size: 'A4',
layout: 'portrait',
bufferPages: true,
});
doc.info = {
Title: 'E-Invoice',
Author: 'author',
Subject: invoiceObj.invoiceId,
CreationDate: new Date(),
ModDate: new Date()
}
doc.fillColor('black')
doc.fontSize(15)
.text('E-Invoice Egypt', 300, 40, {
align: 'right',
})
doc.fillColor('gray')
doc.fontSize(10)
.text(`Seller: Apple Egypt`, 30, 100, {
align: 'left',
})
doc.fontSize(10)
.text(`Customer: Sony Egypt`, 30, 115, {
align: 'left',
})
doc.fontSize(10)
.text(`Invoice ID: 12jhasjdh-834jjsjad-2384masdn`, 30, 130, {
align: 'left',
})
doc.fillColor('black')
doc.fontSize(10)
.text(`Item Name Item Quantity Item Price Item Category Total Price`, 130, 180, {
align: 'justify',
underline: true
})
//for loop to render items here
doc.fillColor('blue')
doc.fontSize(10)
.text(`f7 12 3000 It 23894`, 150, 200, {
align: 'justify',
})
doc.fillColor('blue')
doc.fontSize(10)
.text(`f7 12 3000 It 23894`, 150, 220, {
align: 'justify',
})
doc.fillColor('gray')
doc.fontSize(10)
.text(`Total Price Without VAT: 32323`, 30, 500, {
align: 'justify',
})
doc.fillColor('gray')
doc.fontSize(10)
.text(`Total Price With VAT: 3232234`, 30, 515, {
align: 'justify',
})
doc.fillColor('gray')
doc.fontSize(10)
.text(`Total VAT: 23423`, 30, 530, {
align: 'justify',
})
doc.fillColor('red')
doc.moveDown()
doc.text('This Document Is Signed And Certified By company', 100, 750, {
align: 'right'
})
doc.image('certified.png', 20, 700, {
width: 130,
height: 80,
align: 'left'
})
const pdfChunks = [];
doc.on('data', (data) => {
pdfChunks.push(data);
});
doc.on('end', () => {
resolve(Buffer.concat(pdfChunks));
});
const refs = addSignaturePlaceholder({
pdf: doc,
reason: 'I am the author',
signatureLength: 4096,
});
Object.keys(refs).forEach(key => refs[key].end());
doc.end();
});
const signPDF = (pdfBuffer, p12Buffer) => {
if (!(pdfBuffer instanceof Buffer)) {
throw new Error(
'PDF expected as Buffer.'
);
}
if (!(p12Buffer instanceof Buffer)) {
throw new Error(
'p12 certificate expected as Buffer.'
);
}
let pdf = pdfBuffer;
const lastChar = pdfBuffer.slice(pdfBuffer.length - 1).toString();
if (lastChar === '\n') {
// remove the trailing new line
pdf = pdf.slice(0, pdf.length - 1);
}
const byteRangePlaceholder = [
0,
`/${DEFAULT_BYTE_RANGE_PLACEHOLDER}`,
`/${DEFAULT_BYTE_RANGE_PLACEHOLDER}`,
`/${DEFAULT_BYTE_RANGE_PLACEHOLDER}`,
];
const byteRangeString = `/ByteRange [${byteRangePlaceholder.join(' ')}]`;
const byteRangePos = pdf.indexOf(byteRangeString);
if (byteRangePos === -1) {
throw new Error(
`Could not find ByteRange placeholder: ${byteRangeString}`
);
}
const byteRangeEnd = byteRangePos + byteRangeString.length;
const contentsTagPos = pdf.indexOf('/Contents ', byteRangeEnd);
const placeholderPos = pdf.indexOf('<', contentsTagPos);
const placeholderEnd = pdf.indexOf('>', placeholderPos);
const placeholderLengthWithBrackets = (placeholderEnd + 1) - placeholderPos;
const placeholderLength = placeholderLengthWithBrackets - 2;
const byteRange = [0, 0, 0, 0];
byteRange[1] = placeholderPos;
byteRange[2] = byteRange[1] + placeholderLengthWithBrackets;
byteRange[3] = pdf.length - byteRange[2];
let actualByteRange = `/ByteRange [${byteRange.join(' ')}]`;
actualByteRange += ' '.repeat(byteRangeString.length - actualByteRange.length);
// Replace the /ByteRange placeholder with the actual ByteRange
pdf = Buffer.concat([
pdf.slice(0, byteRangePos),
Buffer.from(actualByteRange),
pdf.slice(byteRangeEnd),
]);
// Remove the placeholder signature
pdf = Buffer.concat([
pdf.slice(0, byteRange[1]),
pdf.slice(byteRange[2], byteRange[2] + byteRange[3]),
]);
const forgeCert = forge.util.createBuffer(p12Buffer.toString('binary'));
const p12Asn1 = forge.asn1.fromDer(forgeCert);
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, '');
// get bags by type
const certBags = p12.getBags({bagType: PKCS12_CERT_BAG})[PKCS12_CERT_BAG];
const keyBags = p12.getBags({bagType: PKCS12_KEY_BAG})[PKCS12_KEY_BAG];
const p7 = forge.pkcs7.createSignedData();
p7.content = forge.util.createBuffer(pdf.toString('binary'));
p7.addCertificate(certBags[0].cert);
p7.addSigner({
key: keyBags[0].key,
certificate: certBags[0].cert,
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [
{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data,
}, {
type: forge.pki.oids.messageDigest,
// value will be auto-populated at signing time
}, {
type: forge.pki.oids.signingTime,
// value can also be auto-populated at signing time
value: new Date(),
},
],
});
p7.sign({detached: true});
const raw = forge.asn1.toDer(p7.toAsn1()).getBytes();
if (raw.length > placeholderLength) {
throw new Error(
`Signature exceeds placeholder length: ${raw.length} > ${placeholderLength}`
);
}
let signature = Buffer.from(raw, 'binary').toString('hex');
// placeholderLength is for the HEX symbols and we need the raw char length
const placeholderCharCount = placeholderLength / 2;
// Pad with zeroes so the output signature is the same length as the placeholder
signature += Buffer
.from(String.fromCharCode(0).repeat(placeholderCharCount - raw.length))
.toString('hex');
pdf = Buffer.concat([
pdf.slice(0, byteRange[1]),
Buffer.from(`<${signature}>`),
pdf.slice(byteRange[1]),
]);
return pdf;
};
const signFunc = async () => {
const pdfBuffer = await creatPDF({
invoiceId: 'asdasdjkayduweydwuedfhwu3dhweudhwd'
});
const signedPdf = signPDF(
pdfBuffer,
fs.readFileSync(certPath)
);
fs.writeFileSync(signedPDFFilePath, signedPdf);
console.log(`**** signedpdf written in ${signedPDFFilePath} ****`);
}
const extractSignature = (pdf) => {
const byteRangePos = pdf.indexOf('/ByteRange [');
if (byteRangePos === -1) {
throw new Error('Failed to locate ByteRange.');
}
const byteRangeEnd = pdf.indexOf(']', byteRangePos);
if (byteRangeEnd === -1) {
throw new Error('Failed to locate the end of the ByteRange.');
}
const byteRange = pdf.slice(byteRangePos, byteRangeEnd + 1).toString();
const matches = (/\/ByteRange \[(\d+) +(\d+) +(\d+) +(\d+)\]/).exec(byteRange);
let signedData = Buffer.concat([
pdf.slice(
parseInt(matches[1]),
parseInt(matches[1]) + parseInt(matches[2]),
),
pdf.slice(
parseInt(matches[3]),
parseInt(matches[3]) + parseInt(matches[4]),
)
])
let signatureHex = pdf.slice(
parseInt(matches[1]) + parseInt(matches[2]) + 1,
parseInt(matches[3]) - 1,
).toString('binary');
signatureHex = signatureHex.replace(/(?:00)*$/, '');
const signature = Buffer.from(signatureHex, 'hex').toString('binary');
return {signature, signedData};
};
function verify() {
const pdf = fs.readFileSync(signedPDFFilePath);
console.log(`**** validating file ${signedPDFFilePath} ****`);
const extractedData = extractSignature(pdf);
const p7Asn1 = forge.asn1.fromDer(extractedData.signature);
const message = forge.pkcs7.messageFromAsn1(p7Asn1);
const sig = message.rawCapture.signature;
const attrs = message.rawCapture.authenticatedAttributes;
const set = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SET, true, attrs);
const buf = Buffer.from(forge.asn1.toDer(set).data, 'binary');
const cert = forge.pki.certificateToPem(message.certificates[0]);
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(buf);
const validAuthenticatedAttributes = verifier.verify(cert, sig, 'binary')
if(!validAuthenticatedAttributes) throw new Error("Wrong authenticated attributes");
const oids = forge.pki.oids;
const hash = crypto.createHash('SHA256');
const data = extractedData.signedData;
hash.update(data);
const fullAttrDigest = attrs.find(attr=> forge.asn1.derToOid(attr.value[0].value) === oids.messageDigest);
const attrDigest = fullAttrDigest.value[1].value[0].value;
const dataDigest = hash.digest();
const validContentDigest = dataDigest.toString('binary') === attrDigest;
if(!validContentDigest) throw new Error('Wrong content digest');
console.log('**** FILE VALID ****');
}
async function main () {
await signFunc();
verify();
}
main();