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.
@@ -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
- export interface ICertificateKeyPairProviderPriv extends ICertificateKeyPairProvider {
12
- $$certificate: null | Certificate;
13
- $$certificateChain: null | Certificate;
14
- $$privateKey: null | PrivateKey;
11
+ interface IHasCertificateFile {
12
+ readonly certificateFile: string;
13
+ readonly privateKeyFile: string;
15
14
  }
16
- export declare function getPartialCertificateChain1(certificateChain?: Buffer | null, maxSize?: number): Buffer | undefined;
17
- export declare function getPartialCertificateChain(certificateChain?: Buffer | null, maxSize?: number): Buffer | undefined;
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
- * an object that provides a certificate and a privateKey
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.getPartialCertificateChain1 = getPartialCertificateChain1;
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 events_1 = require("events");
13
- const fs_1 = __importDefault(require("fs"));
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
- function _load_certificate(certificateFilename) {
18
- const der = (0, node_opcua_crypto_1.readCertificate)(certificateFilename);
19
- return der;
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
- function _load_private_key(privateKeyFilename) {
22
- return (0, node_opcua_crypto_1.readPrivateKey)(privateKeyFilename);
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
- function getPartialCertificateChain1(certificateChain, maxSize) {
25
- return certificateChain || undefined;
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 || certificateChain.length === 0) {
29
- return undefined;
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 certificateChain;
128
+ return certificates;
33
129
  }
34
- const certificates = (0, web_1.split_der)(certificateChain);
35
130
  // at least include first certificate
36
- let buffer = certificates.length == 1 ? certificateChain : Buffer.from(certificates[0]);
131
+ const chainToReturn = [certificates[0]];
132
+ let cumulatedLength = certificates[0].length;
37
133
  // Throw if first certificate already exceed maxSize
38
- if (buffer.length > maxSize) {
39
- throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${buffer.length}`);
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 && buffer.length + certificates[index].length < maxSize) {
43
- buffer = Buffer.concat([buffer, certificates[index]]);
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 buffer;
143
+ return chainToReturn;
47
144
  }
48
145
  /**
49
- * an object that provides a certificate and a privateKey
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
- class OPCUASecureObject extends events_1.EventEmitter {
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
- const priv = this;
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
- const priv = this;
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
- const priv = this;
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":"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"}
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.165.0",
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.0",
16
- "node-opcua-types": "2.165.0"
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": "fe53ac03427fcb223996446c991f7496635ba193",
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 { Certificate, PrivateKey, split_der } from "node-opcua-crypto/web";
9
- import { readCertificate, readPrivateKey } from "node-opcua-crypto";
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
- export interface ICertificateKeyPairProviderPriv extends ICertificateKeyPairProvider {
16
- $$certificate: null | Certificate;
17
- $$certificateChain: null | Certificate;
18
- $$privateKey: null | PrivateKey;
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
- function _load_private_key(privateKeyFilename: string): PrivateKey {
26
- return readPrivateKey(privateKeyFilename);
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
- export function getPartialCertificateChain1(certificateChain?: Buffer| null, maxSize?: number ): Buffer| undefined {
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
- return certificateChain || undefined;
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
- export function getPartialCertificateChain(certificateChain?: Buffer | null, maxSize?: number): Buffer | undefined {
34
-
35
- if (!certificateChain || certificateChain.length === 0) {
36
- return undefined;
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 certificateChain;
144
+ return certificates;
40
145
  }
41
- const certificates = split_der(certificateChain);
42
146
  // at least include first certificate
43
- let buffer = certificates.length == 1 ? certificateChain : Buffer.from(certificates[0]);
147
+ const chainToReturn: Certificate[] = [certificates[0]];
148
+ let cumulatedLength = certificates[0].length;
44
149
  // Throw if first certificate already exceed maxSize
45
- if (buffer.length> maxSize) {
46
- throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${buffer.length}`);
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 && buffer.length + certificates[index].length < maxSize) {
50
- buffer = Buffer.concat([buffer, certificates[index]]);
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 buffer;
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
- * an object that provides a certificate and a privateKey
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
- export class OPCUASecureObject extends EventEmitter implements ICertificateKeyPairProvider {
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
- const priv = this as unknown as ICertificateKeyPairProviderPriv;
80
- if (!priv.$$certificate) {
81
- const certChain = this.getCertificateChain();
82
- priv.$$certificate = split_der(certChain)[0] as Certificate;
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
- const priv = this as unknown as ICertificateKeyPairProviderPriv;
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
  }