• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

node-opcua / node-opcua / 23974043205

04 Apr 2026 07:17AM UTC coverage: 92.589% (+0.01%) from 92.576%
23974043205

push

github

erossignon
chore: fix Mocha.Suite.settimeout misused

18408 of 21832 branches covered (84.32%)

161708 of 174651 relevant lines covered (92.59%)

461089.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.38
/packages/node-opcua-common/source/opcua_secure_object.ts
1
/**
1✔
2
 * @module node-opcua-common
1✔
3
 */
1✔
4
import { EventEmitter } from "node:events";
1✔
5
import fs from "node:fs";
1✔
6
import { assert } from "node-opcua-assert";
1✔
7
import { readCertificateChain, readPrivateKey } from "node-opcua-crypto";
1✔
8
import { type Certificate, type PrivateKey, split_der } from "node-opcua-crypto/web";
1✔
9

1✔
10
import type { ICertificateChainProvider } from "./certificate_chain_provider";
1✔
11

1✔
12
export interface ICertificateKeyPairProvider {
1✔
13
    getCertificate(): Certificate;
1✔
14
    getCertificateChain(): Certificate[];
1✔
15
    getPrivateKey(): PrivateKey;
1✔
16
}
1✔
17

1✔
18
export interface IHasCertificateFile {
1✔
19
    readonly certificateFile: string;
1✔
20
    readonly privateKeyFile: string;
1✔
21
}
1✔
22

1✔
23
/**
1✔
24
 * Holds cryptographic secrets (certificate chain and private key) for a
1✔
25
 * certificate/key file pair. Secrets are lazily loaded from disk on first
1✔
26
 * access and kept in truly private `#`-fields so they never appear in
1✔
27
 * `JSON.stringify`, `console.log`, `Object.keys`, or `util.inspect`.
1✔
28
 */
1✔
29
export class SecretHolder implements ICertificateChainProvider {
2,115✔
30
    #certificateChain: Certificate[] | null = null;
2,115✔
31
    #privateKey: PrivateKey | null = null;
2,115✔
32
    #obj: IHasCertificateFile;
2,115✔
33

2,115✔
34
    constructor(obj: IHasCertificateFile) {
2,115✔
35
        this.#obj = obj;
2,115✔
36
    }
2,115✔
37

2,115✔
38
    public getCertificate(): Certificate {
2,115✔
39
        // Ensure the chain is loaded before accessing [0]
5,456✔
40
        const chain = this.getCertificateChain();
5,456✔
41
        return chain[0];
5,456✔
42
    }
5,456✔
43

2,115✔
44
    public getCertificateChain(): Certificate[] {
2,115✔
45
        if (!this.#certificateChain) {
25,656✔
46
            const file = this.#obj.certificateFile;
1,806✔
47
            if (!fs.existsSync(file)) {
1,806!
48
                throw new Error(`Certificate file must exist: ${file}`);
×
49
            }
×
50
            const chain = readCertificateChain(file);
1,806✔
51
            if (!chain || chain.length === 0) {
1,806!
52
                throw new Error(`Invalid certificate chain (length=0) ${file}`);
×
53
            }
×
54
            this.#certificateChain = chain;
1,806✔
55
        }
1,806✔
56
        return this.#certificateChain;
25,656✔
57
    }
25,656✔
58

2,115✔
59
    public getPrivateKey(): PrivateKey {
2,115✔
60
        if (!this.#privateKey) {
7,930✔
61
            const file = this.#obj.privateKeyFile;
1,965✔
62
            if (!fs.existsSync(file)) {
1,965!
63
                throw new Error(`Private key file must exist: ${file}`);
×
64
            }
×
65
            const key = readPrivateKey(file);
1,965✔
66
            if (key instanceof Buffer) {
1,965!
67
                throw new Error(`Invalid private key ${file}. Should not be a buffer`);
×
68
            }
×
69
            this.#privateKey = key;
1,965✔
70
        }
1,965✔
71
        return this.#privateKey;
7,930✔
72
    }
7,930✔
73

2,115✔
74
    /**
2,115✔
75
     * Clears cached secrets so the GC can reclaim sensitive material.
2,115✔
76
     * After calling dispose the holder will re-read from disk on next access.
2,115✔
77
     */
2,115✔
78
    public dispose(): void {
2,115✔
79
        this.#certificateChain = null;
32✔
80
        this.#privateKey = null;
32✔
81
    }
32✔
82

2,115✔
83
    /**
2,115✔
84
     * Alias for {@link dispose}.
2,115✔
85
     * Implements `ICertificateChainProvider.invalidate()`.
2,115✔
86
     */
2,115✔
87
    public invalidate(): void {
2,115✔
88
        this.dispose();
11✔
89
    }
11✔
90

2,115✔
91
    // Prevent secrets from leaking through JSON serialization
2,115✔
92
    public toJSON(): Record<string, string> {
2,115✔
93
        return { certificateFile: this.#obj.certificateFile, privateKeyFile: this.#obj.privateKeyFile };
×
94
    }
×
95

2,115✔
96
    // Prevent secrets from leaking through console.log / util.inspect
2,115✔
97
    public [Symbol.for("nodejs.util.inspect.custom")](): string {
2,115✔
98
        return `SecretHolder { certificateFile: "${this.#obj.certificateFile}", privateKeyFile: "${this.#obj.privateKeyFile}" }`;
×
99
    }
×
100
}
2,115✔
101

1✔
102
/**
1✔
103
 * Module-private WeakMap that associates an ICertificateKeyPairProvider
1✔
104
 * with its SecretHolder. Using a WeakMap means:
1✔
105
 * - The secret holder is invisible from the outside (no enumerable property)
1✔
106
 * - If the owning object is GC'd, the SecretHolder is automatically collected
1✔
107
 */
1✔
108
const secretHolders = new WeakMap<object, SecretHolder>();
1✔
109

1✔
110
function getSecretHolder(obj: ICertificateKeyPairProvider & IHasCertificateFile): SecretHolder {
13,851✔
111
    let holder = secretHolders.get(obj);
13,851✔
112
    if (!holder) {
13,851✔
113
        holder = new SecretHolder(obj);
1,815✔
114
        secretHolders.set(obj, holder);
1,815✔
115
    }
1,815✔
116
    return holder;
13,851✔
117
}
13,851✔
118

1✔
119
/**
1✔
120
 * Invalidate any cached certificate chain and private key for the given
1✔
121
 * provider so that the next `getCertificate()` / `getPrivateKey()` call
1✔
122
 * re-reads from disk.
1✔
123
 *
1✔
124
 * This is the public replacement for the old `$$certificateChain = null`
1✔
125
 * / `$$privateKey = null` pattern.
1✔
126
 */
1✔
127
export function invalidateCachedSecrets(obj: ICertificateKeyPairProvider): void {
21✔
128
    const holder = secretHolders.get(obj);
21✔
129
    if (holder) {
21✔
130
        holder.dispose();
21✔
131
    }
21✔
132
}
21✔
133

1✔
134
/**
1✔
135
 * Extract a partial certificate chain from a certificate chain so that the
1✔
136
 * total size of the chain does not exceed maxSize.
1✔
137
 * If maxSize is not provided, the full certificate chain is returned.
1✔
138
 * If the first certificate in the chain already exceeds maxSize, an error is thrown.
1✔
139
 *
1✔
140
 * @param certificateChain - full certificate chain (single DER buffer or array)
1✔
141
 * @param maxSize          - optional byte budget
1✔
142
 * @returns the truncated chain as an array of individual certificates
1✔
143
 */
1✔
144
export function getPartialCertificateChain(certificateChain?: Certificate | Certificate[] | null, maxSize?: number): Certificate[] {
1,275✔
145
    if (
1,275✔
146
        !certificateChain ||
1,275✔
147
        (Array.isArray(certificateChain) && certificateChain.length === 0) ||
1,275✔
148
        (certificateChain instanceof Buffer && certificateChain.length === 0)
1,275!
149
    ) {
1,275!
150
        return [];
×
151
    }
×
152
    const certificates = Array.isArray(certificateChain) ? certificateChain : split_der(certificateChain);
1,275!
153
    if (maxSize === undefined) {
1,275✔
154
        return certificates;
629✔
155
    }
629✔
156
    // at least include first certificate
646✔
157
    const chainToReturn: Certificate[] = [certificates[0]];
646✔
158
    let cumulatedLength = certificates[0].length;
646✔
159
    // Throw if first certificate already exceed maxSize
646✔
160
    if (cumulatedLength > maxSize) {
1,275!
161
        throw new Error(`getPartialCertificateChain not enough space for leaf certificate ${maxSize} < ${cumulatedLength}`);
×
162
    }
×
163
    let index = 1;
646✔
164
    while (index < certificates.length && cumulatedLength + certificates[index].length <= maxSize) {
1,275✔
165
        chainToReturn.push(certificates[index]);
265✔
166
        cumulatedLength += certificates[index].length;
265✔
167
        index++;
265✔
168
    }
265✔
169
    return chainToReturn;
646✔
170
}
646✔
171

1✔
172
export interface IOPCUASecureObjectOptions {
1✔
173
    certificateFile?: string;
1✔
174
    privateKeyFile?: string;
1✔
175
}
1✔
176

1✔
177
/**
1✔
178
 * An object that provides a certificate and a privateKey.
1✔
179
 * Secrets are loaded lazily and stored in a module-private WeakMap
1✔
180
 * so they never appear on the instance.
1✔
181
 */
1✔
182

1✔
183
// biome-ignore lint/suspicious/noExplicitAny: EventEmitter use any
1✔
184
export class OPCUASecureObject<T extends Record<string | symbol, any> = any>
1,988✔
185
    extends EventEmitter<T>
1,988✔
186
    implements ICertificateKeyPairProvider, IHasCertificateFile
1,988✔
187
{
1,988✔
188
    public readonly certificateFile: string;
1,988✔
189
    public readonly privateKeyFile: string;
1,988✔
190

1,988✔
191
    constructor(options: IOPCUASecureObjectOptions) {
1,988✔
192
        super();
1,988✔
193
        assert(typeof options.certificateFile === "string");
1,988✔
194
        assert(typeof options.privateKeyFile === "string");
1,988✔
195
        this.certificateFile = options.certificateFile || "invalid certificate file";
1,988!
196
        this.privateKeyFile = options.privateKeyFile || "invalid private key file";
1,988!
197
    }
1,988✔
198

1,988✔
199
    public getCertificate(): Certificate {
1,988✔
200
        return getSecretHolder(this).getCertificate();
5,456✔
201
    }
5,456✔
202

1,988✔
203
    public getCertificateChain(): Certificate[] {
1,988✔
204
        return getSecretHolder(this).getCertificateChain();
2,011✔
205
    }
2,011✔
206

1,988✔
207
    public getPrivateKey(): PrivateKey {
1,988✔
208
        return getSecretHolder(this).getPrivateKey();
6,384✔
209
    }
6,384✔
210
}
1,988✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc