node-opcua-common 2.165.0 → 2.167.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/opcua_secure_object.d.ts +51 -12
- package/dist/opcua_secure_object.js +127 -48
- package/dist/opcua_secure_object.js.map +1 -1
- package/package.json +5 -4
- package/source/opcua_secure_object.ts +148 -61
|
@@ -1,32 +1,71 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module node-opcua-common
|
|
3
3
|
*/
|
|
4
|
-
import { EventEmitter } from "events";
|
|
5
|
-
import { Certificate, PrivateKey } from "node-opcua-crypto/web";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import { type Certificate, type PrivateKey } from "node-opcua-crypto/web";
|
|
6
6
|
export interface ICertificateKeyPairProvider {
|
|
7
7
|
getCertificate(): Certificate;
|
|
8
|
-
getCertificateChain(): Certificate;
|
|
8
|
+
getCertificateChain(): Certificate[];
|
|
9
9
|
getPrivateKey(): PrivateKey;
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
$$privateKey: null | PrivateKey;
|
|
11
|
+
interface IHasCertificateFile {
|
|
12
|
+
readonly certificateFile: string;
|
|
13
|
+
readonly privateKeyFile: string;
|
|
15
14
|
}
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Holds cryptographic secrets (certificate chain and private key) for a
|
|
17
|
+
* certificate/key file pair. Secrets are lazily loaded from disk on first
|
|
18
|
+
* access and kept in truly private `#`-fields so they never appear in
|
|
19
|
+
* `JSON.stringify`, `console.log`, `Object.keys`, or `util.inspect`.
|
|
20
|
+
*/
|
|
21
|
+
export declare class SecretHolder {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(obj: IHasCertificateFile);
|
|
24
|
+
getCertificate(): Certificate;
|
|
25
|
+
getCertificateChain(): Certificate[];
|
|
26
|
+
getPrivateKey(): PrivateKey;
|
|
27
|
+
/**
|
|
28
|
+
* Clears cached secrets so the GC can reclaim sensitive material.
|
|
29
|
+
* After calling dispose the holder will re-read from disk on next access.
|
|
30
|
+
*/
|
|
31
|
+
dispose(): void;
|
|
32
|
+
toJSON(): Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Invalidate any cached certificate chain and private key for the given
|
|
36
|
+
* provider so that the next `getCertificate()` / `getPrivateKey()` call
|
|
37
|
+
* re-reads from disk.
|
|
38
|
+
*
|
|
39
|
+
* This is the public replacement for the old `$$certificateChain = null`
|
|
40
|
+
* / `$$privateKey = null` pattern.
|
|
41
|
+
*/
|
|
42
|
+
export declare function invalidateCachedSecrets(obj: ICertificateKeyPairProvider): void;
|
|
43
|
+
/**
|
|
44
|
+
* Extract a partial certificate chain from a certificate chain so that the
|
|
45
|
+
* total size of the chain does not exceed maxSize.
|
|
46
|
+
* If maxSize is not provided, the full certificate chain is returned.
|
|
47
|
+
* If the first certificate in the chain already exceeds maxSize, an error is thrown.
|
|
48
|
+
*
|
|
49
|
+
* @param certificateChain - full certificate chain (single DER buffer or array)
|
|
50
|
+
* @param maxSize - optional byte budget
|
|
51
|
+
* @returns the truncated chain as an array of individual certificates
|
|
52
|
+
*/
|
|
53
|
+
export declare function getPartialCertificateChain(certificateChain?: Certificate | Certificate[] | null, maxSize?: number): Certificate[];
|
|
18
54
|
export interface IOPCUASecureObjectOptions {
|
|
19
55
|
certificateFile?: string;
|
|
20
56
|
privateKeyFile?: string;
|
|
21
57
|
}
|
|
22
58
|
/**
|
|
23
|
-
*
|
|
59
|
+
* An object that provides a certificate and a privateKey.
|
|
60
|
+
* Secrets are loaded lazily and stored in a module-private WeakMap
|
|
61
|
+
* so they never appear on the instance.
|
|
24
62
|
*/
|
|
25
|
-
export declare class OPCUASecureObject extends EventEmitter implements ICertificateKeyPairProvider {
|
|
63
|
+
export declare class OPCUASecureObject<T extends Record<string | symbol, any> = any> extends EventEmitter<T> implements ICertificateKeyPairProvider, IHasCertificateFile {
|
|
26
64
|
readonly certificateFile: string;
|
|
27
65
|
readonly privateKeyFile: string;
|
|
28
66
|
constructor(options: IOPCUASecureObjectOptions);
|
|
29
67
|
getCertificate(): Certificate;
|
|
30
|
-
getCertificateChain(): Certificate;
|
|
68
|
+
getCertificateChain(): Certificate[];
|
|
31
69
|
getPrivateKey(): PrivateKey;
|
|
32
70
|
}
|
|
71
|
+
export {};
|
|
@@ -3,52 +3,152 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.OPCUASecureObject = void 0;
|
|
7
|
-
exports.
|
|
6
|
+
exports.OPCUASecureObject = exports.SecretHolder = void 0;
|
|
7
|
+
exports.invalidateCachedSecrets = invalidateCachedSecrets;
|
|
8
8
|
exports.getPartialCertificateChain = getPartialCertificateChain;
|
|
9
9
|
/**
|
|
10
10
|
* @module node-opcua-common
|
|
11
11
|
*/
|
|
12
|
-
const
|
|
13
|
-
const
|
|
12
|
+
const node_events_1 = require("node:events");
|
|
13
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
14
14
|
const node_opcua_assert_1 = require("node-opcua-assert");
|
|
15
|
-
const web_1 = require("node-opcua-crypto/web");
|
|
16
15
|
const node_opcua_crypto_1 = require("node-opcua-crypto");
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const web_1 = require("node-opcua-crypto/web");
|
|
17
|
+
/**
|
|
18
|
+
* Holds cryptographic secrets (certificate chain and private key) for a
|
|
19
|
+
* certificate/key file pair. Secrets are lazily loaded from disk on first
|
|
20
|
+
* access and kept in truly private `#`-fields so they never appear in
|
|
21
|
+
* `JSON.stringify`, `console.log`, `Object.keys`, or `util.inspect`.
|
|
22
|
+
*/
|
|
23
|
+
class SecretHolder {
|
|
24
|
+
#certificateChain = null;
|
|
25
|
+
#privateKey = null;
|
|
26
|
+
#obj;
|
|
27
|
+
constructor(obj) {
|
|
28
|
+
this.#obj = obj;
|
|
29
|
+
}
|
|
30
|
+
getCertificate() {
|
|
31
|
+
// Ensure the chain is loaded before accessing [0]
|
|
32
|
+
const chain = this.getCertificateChain();
|
|
33
|
+
return chain[0];
|
|
34
|
+
}
|
|
35
|
+
getCertificateChain() {
|
|
36
|
+
if (!this.#certificateChain) {
|
|
37
|
+
const file = this.#obj.certificateFile;
|
|
38
|
+
if (!node_fs_1.default.existsSync(file)) {
|
|
39
|
+
throw new Error(`Certificate file must exist: ${file}`);
|
|
40
|
+
}
|
|
41
|
+
const chain = (0, node_opcua_crypto_1.readCertificateChain)(file);
|
|
42
|
+
if (!chain || chain.length === 0) {
|
|
43
|
+
throw new Error(`Invalid certificate chain (length=0) ${file}`);
|
|
44
|
+
}
|
|
45
|
+
this.#certificateChain = chain;
|
|
46
|
+
}
|
|
47
|
+
return this.#certificateChain;
|
|
48
|
+
}
|
|
49
|
+
getPrivateKey() {
|
|
50
|
+
if (!this.#privateKey) {
|
|
51
|
+
const file = this.#obj.privateKeyFile;
|
|
52
|
+
if (!node_fs_1.default.existsSync(file)) {
|
|
53
|
+
throw new Error(`Private key file must exist: ${file}`);
|
|
54
|
+
}
|
|
55
|
+
const key = (0, node_opcua_crypto_1.readPrivateKey)(file);
|
|
56
|
+
if (key instanceof Buffer) {
|
|
57
|
+
throw new Error(`Invalid private key ${file}. Should not be a buffer`);
|
|
58
|
+
}
|
|
59
|
+
this.#privateKey = key;
|
|
60
|
+
}
|
|
61
|
+
return this.#privateKey;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Clears cached secrets so the GC can reclaim sensitive material.
|
|
65
|
+
* After calling dispose the holder will re-read from disk on next access.
|
|
66
|
+
*/
|
|
67
|
+
dispose() {
|
|
68
|
+
this.#certificateChain = null;
|
|
69
|
+
this.#privateKey = null;
|
|
70
|
+
}
|
|
71
|
+
// Prevent secrets from leaking through JSON serialization
|
|
72
|
+
toJSON() {
|
|
73
|
+
return { certificateFile: this.#obj.certificateFile, privateKeyFile: this.#obj.privateKeyFile };
|
|
74
|
+
}
|
|
75
|
+
// Prevent secrets from leaking through console.log / util.inspect
|
|
76
|
+
[Symbol.for("nodejs.util.inspect.custom")]() {
|
|
77
|
+
return `SecretHolder { certificateFile: "${this.#obj.certificateFile}", privateKeyFile: "${this.#obj.privateKeyFile}" }`;
|
|
78
|
+
}
|
|
20
79
|
}
|
|
21
|
-
|
|
22
|
-
|
|
80
|
+
exports.SecretHolder = SecretHolder;
|
|
81
|
+
/**
|
|
82
|
+
* Module-private WeakMap that associates an ICertificateKeyPairProvider
|
|
83
|
+
* with its SecretHolder. Using a WeakMap means:
|
|
84
|
+
* - The secret holder is invisible from the outside (no enumerable property)
|
|
85
|
+
* - If the owning object is GC'd, the SecretHolder is automatically collected
|
|
86
|
+
*/
|
|
87
|
+
const secretHolders = new WeakMap();
|
|
88
|
+
function getSecretHolder(obj) {
|
|
89
|
+
let holder = secretHolders.get(obj);
|
|
90
|
+
if (!holder) {
|
|
91
|
+
holder = new SecretHolder(obj);
|
|
92
|
+
secretHolders.set(obj, holder);
|
|
93
|
+
}
|
|
94
|
+
return holder;
|
|
23
95
|
}
|
|
24
|
-
|
|
25
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Invalidate any cached certificate chain and private key for the given
|
|
98
|
+
* provider so that the next `getCertificate()` / `getPrivateKey()` call
|
|
99
|
+
* re-reads from disk.
|
|
100
|
+
*
|
|
101
|
+
* This is the public replacement for the old `$$certificateChain = null`
|
|
102
|
+
* / `$$privateKey = null` pattern.
|
|
103
|
+
*/
|
|
104
|
+
function invalidateCachedSecrets(obj) {
|
|
105
|
+
const holder = secretHolders.get(obj);
|
|
106
|
+
if (holder) {
|
|
107
|
+
holder.dispose();
|
|
108
|
+
}
|
|
26
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Extract a partial certificate chain from a certificate chain so that the
|
|
112
|
+
* total size of the chain does not exceed maxSize.
|
|
113
|
+
* If maxSize is not provided, the full certificate chain is returned.
|
|
114
|
+
* If the first certificate in the chain already exceeds maxSize, an error is thrown.
|
|
115
|
+
*
|
|
116
|
+
* @param certificateChain - full certificate chain (single DER buffer or array)
|
|
117
|
+
* @param maxSize - optional byte budget
|
|
118
|
+
* @returns the truncated chain as an array of individual certificates
|
|
119
|
+
*/
|
|
27
120
|
function getPartialCertificateChain(certificateChain, maxSize) {
|
|
28
|
-
if (!certificateChain ||
|
|
29
|
-
|
|
121
|
+
if (!certificateChain ||
|
|
122
|
+
(Array.isArray(certificateChain) && certificateChain.length === 0) ||
|
|
123
|
+
(certificateChain instanceof Buffer && certificateChain.length === 0)) {
|
|
124
|
+
return [];
|
|
30
125
|
}
|
|
126
|
+
const certificates = Array.isArray(certificateChain) ? certificateChain : (0, web_1.split_der)(certificateChain);
|
|
31
127
|
if (maxSize === undefined) {
|
|
32
|
-
return
|
|
128
|
+
return certificates;
|
|
33
129
|
}
|
|
34
|
-
const certificates = (0, web_1.split_der)(certificateChain);
|
|
35
130
|
// at least include first certificate
|
|
36
|
-
|
|
131
|
+
const chainToReturn = [certificates[0]];
|
|
132
|
+
let cumulatedLength = certificates[0].length;
|
|
37
133
|
// Throw if first certificate already exceed maxSize
|
|
38
|
-
if (
|
|
39
|
-
throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${
|
|
134
|
+
if (cumulatedLength > maxSize) {
|
|
135
|
+
throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${cumulatedLength}`);
|
|
40
136
|
}
|
|
41
137
|
let index = 1;
|
|
42
|
-
while (index < certificates.length &&
|
|
43
|
-
|
|
138
|
+
while (index < certificates.length && cumulatedLength + certificates[index].length <= maxSize) {
|
|
139
|
+
chainToReturn.push(certificates[index]);
|
|
140
|
+
cumulatedLength += certificates[index].length;
|
|
44
141
|
index++;
|
|
45
142
|
}
|
|
46
|
-
return
|
|
143
|
+
return chainToReturn;
|
|
47
144
|
}
|
|
48
145
|
/**
|
|
49
|
-
*
|
|
146
|
+
* An object that provides a certificate and a privateKey.
|
|
147
|
+
* Secrets are loaded lazily and stored in a module-private WeakMap
|
|
148
|
+
* so they never appear on the instance.
|
|
50
149
|
*/
|
|
51
|
-
|
|
150
|
+
// biome-ignore lint/suspicious/noExplicitAny: EventEmitter use any
|
|
151
|
+
class OPCUASecureObject extends node_events_1.EventEmitter {
|
|
52
152
|
certificateFile;
|
|
53
153
|
privateKeyFile;
|
|
54
154
|
constructor(options) {
|
|
@@ -59,34 +159,13 @@ class OPCUASecureObject extends events_1.EventEmitter {
|
|
|
59
159
|
this.privateKeyFile = options.privateKeyFile || "invalid private key file";
|
|
60
160
|
}
|
|
61
161
|
getCertificate() {
|
|
62
|
-
|
|
63
|
-
if (!priv.$$certificate) {
|
|
64
|
-
const certChain = this.getCertificateChain();
|
|
65
|
-
priv.$$certificate = (0, web_1.split_der)(certChain)[0];
|
|
66
|
-
}
|
|
67
|
-
return priv.$$certificate;
|
|
162
|
+
return getSecretHolder(this).getCertificate();
|
|
68
163
|
}
|
|
69
164
|
getCertificateChain() {
|
|
70
|
-
|
|
71
|
-
if (!priv.$$certificateChain) {
|
|
72
|
-
(0, node_opcua_assert_1.assert)(fs_1.default.existsSync(this.certificateFile), "Certificate file must exist :" + this.certificateFile);
|
|
73
|
-
priv.$$certificateChain = _load_certificate(this.certificateFile);
|
|
74
|
-
if (priv.$$certificateChain && priv.$$certificateChain.length === 0) {
|
|
75
|
-
// do it again for debug purposes
|
|
76
|
-
priv.$$certificateChain = _load_certificate(this.certificateFile);
|
|
77
|
-
throw new Error("Invalid certificate length = 0 " + this.certificateFile);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return priv.$$certificateChain;
|
|
165
|
+
return getSecretHolder(this).getCertificateChain();
|
|
81
166
|
}
|
|
82
167
|
getPrivateKey() {
|
|
83
|
-
|
|
84
|
-
if (!priv.$$privateKey) {
|
|
85
|
-
(0, node_opcua_assert_1.assert)(fs_1.default.existsSync(this.privateKeyFile), "private file must exist :" + this.privateKeyFile);
|
|
86
|
-
priv.$$privateKey = _load_private_key(this.privateKeyFile);
|
|
87
|
-
}
|
|
88
|
-
(0, node_opcua_assert_1.assert)(!(priv.$$privateKey instanceof Buffer), "should not be a buffer");
|
|
89
|
-
return priv.$$privateKey;
|
|
168
|
+
return getSecretHolder(this).getPrivateKey();
|
|
90
169
|
}
|
|
91
170
|
}
|
|
92
171
|
exports.OPCUASecureObject = OPCUASecureObject;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opcua_secure_object.js","sourceRoot":"","sources":["../source/opcua_secure_object.ts"],"names":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"opcua_secure_object.js","sourceRoot":"","sources":["../source/opcua_secure_object.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-opcua-common",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.167.0",
|
|
4
4
|
"description": "pure nodejs OPCUA SDK - module common",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "mocha",
|
|
7
|
+
"test:check": "tsc --noEmit -p test/tsconfig.json",
|
|
7
8
|
"clean": "npx rimraf -g node_modules dist *.tsbuildinfo",
|
|
8
9
|
"build": "tsc -b",
|
|
9
10
|
"lint": "eslint source/**/*.ts"
|
|
@@ -12,8 +13,8 @@
|
|
|
12
13
|
"types": "./dist/index.d.ts",
|
|
13
14
|
"dependencies": {
|
|
14
15
|
"node-opcua-assert": "2.164.0",
|
|
15
|
-
"node-opcua-crypto": "5.3.
|
|
16
|
-
"node-opcua-types": "2.
|
|
16
|
+
"node-opcua-crypto": "5.3.3",
|
|
17
|
+
"node-opcua-types": "2.167.0"
|
|
17
18
|
},
|
|
18
19
|
"author": "Etienne Rossignon",
|
|
19
20
|
"license": "MIT",
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"internet of things"
|
|
31
32
|
],
|
|
32
33
|
"homepage": "http://node-opcua.github.io/",
|
|
33
|
-
"gitHead": "
|
|
34
|
+
"gitHead": "5decfa86ee53a36ecd3bb454e7bf6e3dd27c7a4e",
|
|
34
35
|
"files": [
|
|
35
36
|
"dist",
|
|
36
37
|
"source"
|
|
@@ -1,57 +1,162 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module node-opcua-common
|
|
3
3
|
*/
|
|
4
|
-
import { EventEmitter } from "events";
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import fs from "node:fs";
|
|
7
6
|
import { assert } from "node-opcua-assert";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { readCertificateChain, readPrivateKey } from "node-opcua-crypto";
|
|
8
|
+
import { type Certificate, type PrivateKey, split_der } from "node-opcua-crypto/web";
|
|
9
|
+
|
|
10
10
|
export interface ICertificateKeyPairProvider {
|
|
11
11
|
getCertificate(): Certificate;
|
|
12
|
-
getCertificateChain(): Certificate;
|
|
12
|
+
getCertificateChain(): Certificate[];
|
|
13
13
|
getPrivateKey(): PrivateKey;
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
function _load_certificate(certificateFilename: string): Certificate {
|
|
21
|
-
const der = readCertificate(certificateFilename);
|
|
22
|
-
return der;
|
|
15
|
+
|
|
16
|
+
interface IHasCertificateFile {
|
|
17
|
+
readonly certificateFile: string;
|
|
18
|
+
readonly privateKeyFile: string;
|
|
23
19
|
}
|
|
24
20
|
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Holds cryptographic secrets (certificate chain and private key) for a
|
|
23
|
+
* certificate/key file pair. Secrets are lazily loaded from disk on first
|
|
24
|
+
* access and kept in truly private `#`-fields so they never appear in
|
|
25
|
+
* `JSON.stringify`, `console.log`, `Object.keys`, or `util.inspect`.
|
|
26
|
+
*/
|
|
27
|
+
export class SecretHolder {
|
|
28
|
+
#certificateChain: Certificate[] | null = null;
|
|
29
|
+
#privateKey: PrivateKey | null = null;
|
|
30
|
+
#obj: IHasCertificateFile;
|
|
31
|
+
|
|
32
|
+
constructor(obj: IHasCertificateFile) {
|
|
33
|
+
this.#obj = obj;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public getCertificate(): Certificate {
|
|
37
|
+
// Ensure the chain is loaded before accessing [0]
|
|
38
|
+
const chain = this.getCertificateChain();
|
|
39
|
+
return chain[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public getCertificateChain(): Certificate[] {
|
|
43
|
+
if (!this.#certificateChain) {
|
|
44
|
+
const file = this.#obj.certificateFile;
|
|
45
|
+
if (!fs.existsSync(file)) {
|
|
46
|
+
throw new Error(`Certificate file must exist: ${file}`);
|
|
47
|
+
}
|
|
48
|
+
const chain = readCertificateChain(file);
|
|
49
|
+
if (!chain || chain.length === 0) {
|
|
50
|
+
throw new Error(`Invalid certificate chain (length=0) ${file}`);
|
|
51
|
+
}
|
|
52
|
+
this.#certificateChain = chain;
|
|
53
|
+
}
|
|
54
|
+
return this.#certificateChain;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public getPrivateKey(): PrivateKey {
|
|
58
|
+
if (!this.#privateKey) {
|
|
59
|
+
const file = this.#obj.privateKeyFile;
|
|
60
|
+
if (!fs.existsSync(file)) {
|
|
61
|
+
throw new Error(`Private key file must exist: ${file}`);
|
|
62
|
+
}
|
|
63
|
+
const key = readPrivateKey(file);
|
|
64
|
+
if (key instanceof Buffer) {
|
|
65
|
+
throw new Error(`Invalid private key ${file}. Should not be a buffer`);
|
|
66
|
+
}
|
|
67
|
+
this.#privateKey = key;
|
|
68
|
+
}
|
|
69
|
+
return this.#privateKey;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Clears cached secrets so the GC can reclaim sensitive material.
|
|
74
|
+
* After calling dispose the holder will re-read from disk on next access.
|
|
75
|
+
*/
|
|
76
|
+
public dispose(): void {
|
|
77
|
+
this.#certificateChain = null;
|
|
78
|
+
this.#privateKey = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Prevent secrets from leaking through JSON serialization
|
|
82
|
+
public toJSON(): Record<string, string> {
|
|
83
|
+
return { certificateFile: this.#obj.certificateFile, privateKeyFile: this.#obj.privateKeyFile };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Prevent secrets from leaking through console.log / util.inspect
|
|
87
|
+
public [Symbol.for("nodejs.util.inspect.custom")](): string {
|
|
88
|
+
return `SecretHolder { certificateFile: "${this.#obj.certificateFile}", privateKeyFile: "${this.#obj.privateKeyFile}" }`;
|
|
89
|
+
}
|
|
27
90
|
}
|
|
28
91
|
|
|
29
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Module-private WeakMap that associates an ICertificateKeyPairProvider
|
|
94
|
+
* with its SecretHolder. Using a WeakMap means:
|
|
95
|
+
* - The secret holder is invisible from the outside (no enumerable property)
|
|
96
|
+
* - If the owning object is GC'd, the SecretHolder is automatically collected
|
|
97
|
+
*/
|
|
98
|
+
const secretHolders = new WeakMap<object, SecretHolder>();
|
|
99
|
+
|
|
100
|
+
function getSecretHolder(obj: ICertificateKeyPairProvider & IHasCertificateFile): SecretHolder {
|
|
101
|
+
let holder = secretHolders.get(obj);
|
|
102
|
+
if (!holder) {
|
|
103
|
+
holder = new SecretHolder(obj);
|
|
104
|
+
secretHolders.set(obj, holder);
|
|
105
|
+
}
|
|
106
|
+
return holder;
|
|
107
|
+
}
|
|
30
108
|
|
|
31
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Invalidate any cached certificate chain and private key for the given
|
|
111
|
+
* provider so that the next `getCertificate()` / `getPrivateKey()` call
|
|
112
|
+
* re-reads from disk.
|
|
113
|
+
*
|
|
114
|
+
* This is the public replacement for the old `$$certificateChain = null`
|
|
115
|
+
* / `$$privateKey = null` pattern.
|
|
116
|
+
*/
|
|
117
|
+
export function invalidateCachedSecrets(obj: ICertificateKeyPairProvider): void {
|
|
118
|
+
const holder = secretHolders.get(obj);
|
|
119
|
+
if (holder) {
|
|
120
|
+
holder.dispose();
|
|
121
|
+
}
|
|
32
122
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract a partial certificate chain from a certificate chain so that the
|
|
126
|
+
* total size of the chain does not exceed maxSize.
|
|
127
|
+
* If maxSize is not provided, the full certificate chain is returned.
|
|
128
|
+
* If the first certificate in the chain already exceeds maxSize, an error is thrown.
|
|
129
|
+
*
|
|
130
|
+
* @param certificateChain - full certificate chain (single DER buffer or array)
|
|
131
|
+
* @param maxSize - optional byte budget
|
|
132
|
+
* @returns the truncated chain as an array of individual certificates
|
|
133
|
+
*/
|
|
134
|
+
export function getPartialCertificateChain(certificateChain?: Certificate | Certificate[] | null, maxSize?: number): Certificate[] {
|
|
135
|
+
if (
|
|
136
|
+
!certificateChain ||
|
|
137
|
+
(Array.isArray(certificateChain) && certificateChain.length === 0) ||
|
|
138
|
+
(certificateChain instanceof Buffer && certificateChain.length === 0)
|
|
139
|
+
) {
|
|
140
|
+
return [];
|
|
37
141
|
}
|
|
142
|
+
const certificates = Array.isArray(certificateChain) ? certificateChain : split_der(certificateChain);
|
|
38
143
|
if (maxSize === undefined) {
|
|
39
|
-
return
|
|
144
|
+
return certificates;
|
|
40
145
|
}
|
|
41
|
-
const certificates = split_der(certificateChain);
|
|
42
146
|
// at least include first certificate
|
|
43
|
-
|
|
147
|
+
const chainToReturn: Certificate[] = [certificates[0]];
|
|
148
|
+
let cumulatedLength = certificates[0].length;
|
|
44
149
|
// Throw if first certificate already exceed maxSize
|
|
45
|
-
if (
|
|
46
|
-
throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${
|
|
150
|
+
if (cumulatedLength > maxSize) {
|
|
151
|
+
throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${cumulatedLength}`);
|
|
47
152
|
}
|
|
48
153
|
let index = 1;
|
|
49
|
-
while (index < certificates.length &&
|
|
50
|
-
|
|
154
|
+
while (index < certificates.length && cumulatedLength + certificates[index].length <= maxSize) {
|
|
155
|
+
chainToReturn.push(certificates[index]);
|
|
156
|
+
cumulatedLength += certificates[index].length;
|
|
51
157
|
index++;
|
|
52
158
|
}
|
|
53
|
-
return
|
|
54
|
-
|
|
159
|
+
return chainToReturn;
|
|
55
160
|
}
|
|
56
161
|
|
|
57
162
|
export interface IOPCUASecureObjectOptions {
|
|
@@ -60,9 +165,13 @@ export interface IOPCUASecureObjectOptions {
|
|
|
60
165
|
}
|
|
61
166
|
|
|
62
167
|
/**
|
|
63
|
-
*
|
|
168
|
+
* An object that provides a certificate and a privateKey.
|
|
169
|
+
* Secrets are loaded lazily and stored in a module-private WeakMap
|
|
170
|
+
* so they never appear on the instance.
|
|
64
171
|
*/
|
|
65
|
-
|
|
172
|
+
|
|
173
|
+
// biome-ignore lint/suspicious/noExplicitAny: EventEmitter use any
|
|
174
|
+
export class OPCUASecureObject<T extends Record<string | symbol, any> = any> extends EventEmitter<T> implements ICertificateKeyPairProvider, IHasCertificateFile {
|
|
66
175
|
public readonly certificateFile: string;
|
|
67
176
|
public readonly privateKeyFile: string;
|
|
68
177
|
|
|
@@ -70,41 +179,19 @@ export class OPCUASecureObject extends EventEmitter implements ICertificateKeyPa
|
|
|
70
179
|
super();
|
|
71
180
|
assert(typeof options.certificateFile === "string");
|
|
72
181
|
assert(typeof options.privateKeyFile === "string");
|
|
73
|
-
|
|
74
182
|
this.certificateFile = options.certificateFile || "invalid certificate file";
|
|
75
183
|
this.privateKeyFile = options.privateKeyFile || "invalid private key file";
|
|
76
184
|
}
|
|
77
185
|
|
|
78
186
|
public getCertificate(): Certificate {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return priv.$$certificate;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
public getCertificateChain(): Certificate {
|
|
88
|
-
const priv = this as unknown as ICertificateKeyPairProviderPriv;
|
|
89
|
-
if (!priv.$$certificateChain) {
|
|
90
|
-
assert(fs.existsSync(this.certificateFile), "Certificate file must exist :" + this.certificateFile);
|
|
91
|
-
priv.$$certificateChain = _load_certificate(this.certificateFile);
|
|
92
|
-
if (priv.$$certificateChain && priv.$$certificateChain.length === 0) {
|
|
93
|
-
// do it again for debug purposes
|
|
94
|
-
priv.$$certificateChain = _load_certificate(this.certificateFile);
|
|
95
|
-
throw new Error("Invalid certificate length = 0 " + this.certificateFile);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return priv.$$certificateChain;
|
|
187
|
+
return getSecretHolder(this).getCertificate();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public getCertificateChain(): Certificate[] {
|
|
191
|
+
return getSecretHolder(this).getCertificateChain();
|
|
99
192
|
}
|
|
100
193
|
|
|
101
194
|
public getPrivateKey(): PrivateKey {
|
|
102
|
-
|
|
103
|
-
if (!priv.$$privateKey) {
|
|
104
|
-
assert(fs.existsSync(this.privateKeyFile), "private file must exist :" + this.privateKeyFile);
|
|
105
|
-
priv.$$privateKey = _load_private_key(this.privateKeyFile);
|
|
106
|
-
}
|
|
107
|
-
assert(!(priv.$$privateKey instanceof Buffer), "should not be a buffer");
|
|
108
|
-
return priv.$$privateKey;
|
|
195
|
+
return getSecretHolder(this).getPrivateKey();
|
|
109
196
|
}
|
|
110
197
|
}
|