@fin.cx/skr 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/index.d.ts +6 -0
- package/dist_ts/index.js +7 -1
- package/dist_ts/plugins.d.ts +8 -1
- package/dist_ts/plugins.js +10 -2
- package/dist_ts/skr.api.d.ts +70 -0
- package/dist_ts/skr.api.js +348 -1
- package/dist_ts/skr.export.accounts.d.ts +53 -0
- package/dist_ts/skr.export.accounts.js +111 -0
- package/dist_ts/skr.export.balances.d.ts +59 -0
- package/dist_ts/skr.export.balances.js +205 -0
- package/dist_ts/skr.export.d.ts +110 -0
- package/dist_ts/skr.export.js +315 -0
- package/dist_ts/skr.export.ledger.d.ts +95 -0
- package/dist_ts/skr.export.ledger.js +164 -0
- package/dist_ts/skr.export.pdf.d.ts +82 -0
- package/dist_ts/skr.export.pdf.js +548 -0
- package/dist_ts/skr.invoice.adapter.d.ts +98 -0
- package/dist_ts/skr.invoice.adapter.js +476 -0
- package/dist_ts/skr.invoice.booking.d.ts +102 -0
- package/dist_ts/skr.invoice.booking.js +556 -0
- package/dist_ts/skr.invoice.entity.d.ts +287 -0
- package/dist_ts/skr.invoice.entity.js +2 -0
- package/dist_ts/skr.invoice.mapper.d.ts +69 -0
- package/dist_ts/skr.invoice.mapper.js +401 -0
- package/dist_ts/skr.invoice.storage.d.ts +140 -0
- package/dist_ts/skr.invoice.storage.js +529 -0
- package/dist_ts/skr.security.d.ts +65 -0
- package/dist_ts/skr.security.js +319 -0
- package/dist_ts/skr.types.d.ts +1 -0
- package/package.json +17 -12
- package/readme.md +207 -16
- package/ts/index.ts +6 -0
- package/ts/plugins.ts +22 -1
- package/ts/skr.api.ts +485 -0
- package/ts/skr.export.accounts.ts +154 -0
- package/ts/skr.export.balances.ts +270 -0
- package/ts/skr.export.ledger.ts +249 -0
- package/ts/skr.export.pdf.ts +601 -0
- package/ts/skr.export.ts +443 -0
- package/ts/skr.invoice.adapter.ts +581 -0
- package/ts/skr.invoice.booking.ts +738 -0
- package/ts/skr.invoice.entity.ts +351 -0
- package/ts/skr.invoice.mapper.ts +486 -0
- package/ts/skr.invoice.storage.ts +710 -0
- package/ts/skr.security.ts +405 -0
- package/ts/skr.types.ts +1 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
4
|
+
import * as https from 'https';
|
|
5
|
+
|
|
6
|
+
export interface ISigningOptions {
|
|
7
|
+
certificatePem?: string;
|
|
8
|
+
privateKeyPem?: string;
|
|
9
|
+
privateKeyPassphrase?: string;
|
|
10
|
+
timestampServerUrl?: string;
|
|
11
|
+
includeTimestamp?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ISignatureResult {
|
|
15
|
+
signature: string;
|
|
16
|
+
signatureFormat: 'CAdES-B' | 'CAdES-T' | 'CAdES-LT';
|
|
17
|
+
signingTime: string;
|
|
18
|
+
certificateChain?: string[];
|
|
19
|
+
timestampToken?: string;
|
|
20
|
+
timestampTime?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ITimestampResponse {
|
|
24
|
+
token: string;
|
|
25
|
+
time: string;
|
|
26
|
+
serverUrl: string;
|
|
27
|
+
hashAlgorithm: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SecurityManager {
|
|
31
|
+
private options: ISigningOptions;
|
|
32
|
+
private logger: plugins.smartlog.ConsoleLog;
|
|
33
|
+
|
|
34
|
+
constructor(options: ISigningOptions = {}) {
|
|
35
|
+
this.options = {
|
|
36
|
+
timestampServerUrl: options.timestampServerUrl || 'http://timestamp.digicert.com',
|
|
37
|
+
includeTimestamp: options.includeTimestamp !== false,
|
|
38
|
+
...options
|
|
39
|
+
};
|
|
40
|
+
this.logger = new plugins.smartlog.ConsoleLog();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a CAdES-B (Basic) signature for data
|
|
45
|
+
*/
|
|
46
|
+
public async createCadesSignature(
|
|
47
|
+
data: Buffer | string,
|
|
48
|
+
certificatePem?: string,
|
|
49
|
+
privateKeyPem?: string
|
|
50
|
+
): Promise<ISignatureResult> {
|
|
51
|
+
const cert = certificatePem || this.options.certificatePem;
|
|
52
|
+
const key = privateKeyPem || this.options.privateKeyPem;
|
|
53
|
+
|
|
54
|
+
if (!cert || !key) {
|
|
55
|
+
throw new Error('Certificate and private key are required for signing');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Parse certificate and key
|
|
60
|
+
const certificate = plugins.nodeForge.pki.certificateFromPem(cert);
|
|
61
|
+
const privateKey = this.options.privateKeyPassphrase
|
|
62
|
+
? plugins.nodeForge.pki.decryptRsaPrivateKey(key, this.options.privateKeyPassphrase)
|
|
63
|
+
: plugins.nodeForge.pki.privateKeyFromPem(key);
|
|
64
|
+
|
|
65
|
+
// Create PKCS#7 signed data (CMS)
|
|
66
|
+
const p7 = plugins.nodeForge.pkcs7.createSignedData();
|
|
67
|
+
|
|
68
|
+
// Add content
|
|
69
|
+
if (typeof data === 'string') {
|
|
70
|
+
p7.content = plugins.nodeForge.util.createBuffer(data, 'utf8');
|
|
71
|
+
} else {
|
|
72
|
+
p7.content = plugins.nodeForge.util.createBuffer(data.toString('latin1'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Add certificate
|
|
76
|
+
p7.addCertificate(certificate);
|
|
77
|
+
|
|
78
|
+
// Add signer
|
|
79
|
+
p7.addSigner({
|
|
80
|
+
key: privateKey,
|
|
81
|
+
certificate: certificate,
|
|
82
|
+
digestAlgorithm: plugins.nodeForge.pki.oids.sha256,
|
|
83
|
+
authenticatedAttributes: [
|
|
84
|
+
{
|
|
85
|
+
type: plugins.nodeForge.pki.oids.contentType,
|
|
86
|
+
value: plugins.nodeForge.pki.oids.data
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: plugins.nodeForge.pki.oids.messageDigest
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: plugins.nodeForge.pki.oids.signingTime,
|
|
93
|
+
value: new Date().toISOString()
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Sign the data
|
|
99
|
+
p7.sign({ detached: true });
|
|
100
|
+
|
|
101
|
+
// Convert to PEM
|
|
102
|
+
const pem = plugins.nodeForge.pkcs7.messageToPem(p7);
|
|
103
|
+
|
|
104
|
+
// Extract base64 signature
|
|
105
|
+
const signature = pem
|
|
106
|
+
.replace(/-----BEGIN PKCS7-----/, '')
|
|
107
|
+
.replace(/-----END PKCS7-----/, '')
|
|
108
|
+
.replace(/\r?\n/g, '');
|
|
109
|
+
|
|
110
|
+
const result: ISignatureResult = {
|
|
111
|
+
signature: signature,
|
|
112
|
+
signatureFormat: 'CAdES-B',
|
|
113
|
+
signingTime: new Date().toISOString(),
|
|
114
|
+
certificateChain: [cert]
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Add timestamp if requested
|
|
118
|
+
if (this.options.includeTimestamp && this.options.timestampServerUrl) {
|
|
119
|
+
try {
|
|
120
|
+
const timestampResponse = await this.requestTimestamp(signature);
|
|
121
|
+
result.timestampToken = timestampResponse.token;
|
|
122
|
+
result.timestampTime = timestampResponse.time;
|
|
123
|
+
result.signatureFormat = 'CAdES-T';
|
|
124
|
+
} catch (error) {
|
|
125
|
+
this.logger.log('warn', `Failed to obtain timestamp: ${error}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new Error(`Failed to create CAdES signature: ${error}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Requests an RFC 3161 timestamp from a TSA
|
|
137
|
+
*/
|
|
138
|
+
public async requestTimestamp(dataHash: string | Buffer): Promise<ITimestampResponse> {
|
|
139
|
+
try {
|
|
140
|
+
// Create hash of the data
|
|
141
|
+
let hash: Buffer;
|
|
142
|
+
if (typeof dataHash === 'string') {
|
|
143
|
+
hash = crypto.createHash('sha256').update(dataHash).digest();
|
|
144
|
+
} else {
|
|
145
|
+
hash = crypto.createHash('sha256').update(dataHash).digest();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Create timestamp request (simplified - in production use proper ASN.1 encoding)
|
|
149
|
+
const tsRequest = this.createTimestampRequest(hash);
|
|
150
|
+
|
|
151
|
+
// Send request to TSA
|
|
152
|
+
const response = await this.sendTimestampRequest(tsRequest);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
token: response.toString('base64'),
|
|
156
|
+
time: new Date().toISOString(),
|
|
157
|
+
serverUrl: this.options.timestampServerUrl!,
|
|
158
|
+
hashAlgorithm: 'sha256'
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new Error(`Failed to obtain timestamp: ${error}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Creates a timestamp request (simplified version)
|
|
167
|
+
*/
|
|
168
|
+
private createTimestampRequest(hash: Buffer): Buffer {
|
|
169
|
+
// In production, use proper ASN.1 encoding library
|
|
170
|
+
// This is a simplified placeholder
|
|
171
|
+
const request = {
|
|
172
|
+
version: 1,
|
|
173
|
+
messageImprint: {
|
|
174
|
+
hashAlgorithm: { algorithm: '2.16.840.1.101.3.4.2.1' }, // SHA-256 OID
|
|
175
|
+
hashedMessage: hash
|
|
176
|
+
},
|
|
177
|
+
reqPolicy: null,
|
|
178
|
+
nonce: crypto.randomBytes(8),
|
|
179
|
+
certReq: true
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Convert to DER-encoded ASN.1 (simplified)
|
|
183
|
+
return Buffer.from(JSON.stringify(request));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Sends timestamp request to TSA server
|
|
188
|
+
*/
|
|
189
|
+
private async sendTimestampRequest(request: Buffer): Promise<Buffer> {
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
const url = new URL(this.options.timestampServerUrl!);
|
|
192
|
+
|
|
193
|
+
const options = {
|
|
194
|
+
hostname: url.hostname,
|
|
195
|
+
port: url.port || 443,
|
|
196
|
+
path: url.pathname,
|
|
197
|
+
method: 'POST',
|
|
198
|
+
headers: {
|
|
199
|
+
'Content-Type': 'application/timestamp-query',
|
|
200
|
+
'Content-Length': request.length
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const req = https.request(options, (res) => {
|
|
205
|
+
const chunks: Buffer[] = [];
|
|
206
|
+
|
|
207
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
208
|
+
res.on('end', () => {
|
|
209
|
+
const response = Buffer.concat(chunks);
|
|
210
|
+
if (res.statusCode === 200) {
|
|
211
|
+
resolve(response);
|
|
212
|
+
} else {
|
|
213
|
+
reject(new Error(`TSA server returned status ${res.statusCode}`));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
req.on('error', reject);
|
|
219
|
+
req.write(request);
|
|
220
|
+
req.end();
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Verifies a CAdES signature
|
|
226
|
+
*/
|
|
227
|
+
public async verifyCadesSignature(
|
|
228
|
+
data: Buffer | string,
|
|
229
|
+
signature: string,
|
|
230
|
+
certificatePem?: string
|
|
231
|
+
): Promise<boolean> {
|
|
232
|
+
try {
|
|
233
|
+
// Add PEM headers if not present
|
|
234
|
+
let pemSignature = signature;
|
|
235
|
+
if (!signature.includes('BEGIN PKCS7')) {
|
|
236
|
+
pemSignature = `-----BEGIN PKCS7-----\n${signature}\n-----END PKCS7-----`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Parse the PKCS#7 message
|
|
240
|
+
const p7 = plugins.nodeForge.pkcs7.messageFromPem(pemSignature);
|
|
241
|
+
|
|
242
|
+
// Prepare content for verification
|
|
243
|
+
let content: plugins.nodeForge.util.ByteStringBuffer;
|
|
244
|
+
if (typeof data === 'string') {
|
|
245
|
+
content = plugins.nodeForge.util.createBuffer(data, 'utf8');
|
|
246
|
+
} else {
|
|
247
|
+
content = plugins.nodeForge.util.createBuffer(data.toString('latin1'));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Verify the signature
|
|
251
|
+
const verified = (p7 as any).verify({
|
|
252
|
+
content: content,
|
|
253
|
+
detached: true
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return verified;
|
|
257
|
+
} catch (error) {
|
|
258
|
+
this.logger.log('error', `Signature verification failed: ${error}`);
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Generates a self-signed certificate for testing
|
|
265
|
+
*/
|
|
266
|
+
public async generateSelfSignedCertificate(
|
|
267
|
+
commonName: string = 'SKR Export System',
|
|
268
|
+
validDays: number = 365
|
|
269
|
+
): Promise<{ certificate: string; privateKey: string }> {
|
|
270
|
+
const keys = plugins.nodeForge.pki.rsa.generateKeyPair(2048);
|
|
271
|
+
const cert = plugins.nodeForge.pki.createCertificate();
|
|
272
|
+
|
|
273
|
+
cert.publicKey = keys.publicKey;
|
|
274
|
+
cert.serialNumber = '01';
|
|
275
|
+
cert.validity.notBefore = new Date();
|
|
276
|
+
cert.validity.notAfter = new Date();
|
|
277
|
+
cert.validity.notAfter.setDate(cert.validity.notAfter.getDate() + validDays);
|
|
278
|
+
|
|
279
|
+
const attrs = [
|
|
280
|
+
{ name: 'commonName', value: commonName },
|
|
281
|
+
{ name: 'countryName', value: 'DE' },
|
|
282
|
+
{ name: 'organizationName', value: 'SKR Export System' },
|
|
283
|
+
{ shortName: 'OU', value: 'Accounting' }
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
cert.setSubject(attrs);
|
|
287
|
+
cert.setIssuer(attrs);
|
|
288
|
+
|
|
289
|
+
cert.setExtensions([
|
|
290
|
+
{
|
|
291
|
+
name: 'basicConstraints',
|
|
292
|
+
cA: true
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: 'keyUsage',
|
|
296
|
+
keyCertSign: true,
|
|
297
|
+
digitalSignature: true,
|
|
298
|
+
nonRepudiation: true,
|
|
299
|
+
keyEncipherment: true,
|
|
300
|
+
dataEncipherment: true
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
name: 'extKeyUsage',
|
|
304
|
+
serverAuth: true,
|
|
305
|
+
clientAuth: true,
|
|
306
|
+
codeSigning: true,
|
|
307
|
+
emailProtection: true,
|
|
308
|
+
timeStamping: true
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'nsCertType',
|
|
312
|
+
client: true,
|
|
313
|
+
server: true,
|
|
314
|
+
email: true,
|
|
315
|
+
objsign: true,
|
|
316
|
+
sslCA: true,
|
|
317
|
+
emailCA: true,
|
|
318
|
+
objCA: true
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: 'subjectAltName',
|
|
322
|
+
altNames: [
|
|
323
|
+
{ type: 2, value: commonName }
|
|
324
|
+
]
|
|
325
|
+
}
|
|
326
|
+
]);
|
|
327
|
+
|
|
328
|
+
// Self-sign certificate
|
|
329
|
+
cert.sign(keys.privateKey, plugins.nodeForge.md.sha256.create());
|
|
330
|
+
|
|
331
|
+
// Convert to PEM
|
|
332
|
+
const certificatePem = plugins.nodeForge.pki.certificateToPem(cert);
|
|
333
|
+
const privateKeyPem = plugins.nodeForge.pki.privateKeyToPem(keys.privateKey);
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
certificate: certificatePem,
|
|
337
|
+
privateKey: privateKeyPem
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Creates a detached signature file
|
|
343
|
+
*/
|
|
344
|
+
public async createDetachedSignature(
|
|
345
|
+
dataPath: string,
|
|
346
|
+
outputPath: string
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
const data = await plugins.smartfile.fs.toBuffer(dataPath);
|
|
349
|
+
const signature = await this.createCadesSignature(data);
|
|
350
|
+
|
|
351
|
+
const signatureData = {
|
|
352
|
+
signature: signature.signature,
|
|
353
|
+
format: signature.signatureFormat,
|
|
354
|
+
signingTime: signature.signingTime,
|
|
355
|
+
timestamp: signature.timestampToken,
|
|
356
|
+
timestampTime: signature.timestampTime,
|
|
357
|
+
algorithm: 'SHA256withRSA',
|
|
358
|
+
signedFile: path.basename(dataPath)
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
await plugins.smartfile.memory.toFs(
|
|
362
|
+
JSON.stringify(signatureData, null, 2),
|
|
363
|
+
outputPath
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Verifies a detached signature file
|
|
369
|
+
*/
|
|
370
|
+
public async verifyDetachedSignature(
|
|
371
|
+
dataPath: string,
|
|
372
|
+
signaturePath: string
|
|
373
|
+
): Promise<boolean> {
|
|
374
|
+
try {
|
|
375
|
+
const data = await plugins.smartfile.fs.toBuffer(dataPath);
|
|
376
|
+
const signatureJson = await plugins.smartfile.fs.toStringSync(signaturePath);
|
|
377
|
+
const signatureData = JSON.parse(signatureJson);
|
|
378
|
+
|
|
379
|
+
return await this.verifyCadesSignature(data, signatureData.signature);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
this.logger.log('error', `Failed to verify detached signature: ${error}`);
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Adds Long-Term Validation (LTV) information
|
|
388
|
+
*/
|
|
389
|
+
public async addLtvInformation(
|
|
390
|
+
signature: ISignatureResult,
|
|
391
|
+
ocspResponse?: Buffer,
|
|
392
|
+
crlData?: Buffer
|
|
393
|
+
): Promise<ISignatureResult> {
|
|
394
|
+
// Add OCSP response and CRL data for long-term validation
|
|
395
|
+
const ltv = {
|
|
396
|
+
...signature,
|
|
397
|
+
signatureFormat: 'CAdES-LT' as const,
|
|
398
|
+
ocsp: ocspResponse?.toString('base64'),
|
|
399
|
+
crl: crlData?.toString('base64'),
|
|
400
|
+
ltvTime: new Date().toISOString()
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return ltv;
|
|
404
|
+
}
|
|
405
|
+
}
|
package/ts/skr.types.ts
CHANGED