@zetra/citrineos-util 1.8.3-fork.1

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.
Files changed (142) hide show
  1. package/dist/authorization/ApiAuthPlugin.d.ts +52 -0
  2. package/dist/authorization/ApiAuthPlugin.js +122 -0
  3. package/dist/authorization/ApiAuthPlugin.js.map +1 -0
  4. package/dist/authorization/OidcTokenProvider.d.ts +15 -0
  5. package/dist/authorization/OidcTokenProvider.js +47 -0
  6. package/dist/authorization/OidcTokenProvider.js.map +1 -0
  7. package/dist/authorization/index.d.ts +4 -0
  8. package/dist/authorization/index.js +8 -0
  9. package/dist/authorization/index.js.map +1 -0
  10. package/dist/authorization/provider/LocalByPassAuthProvider.d.ts +34 -0
  11. package/dist/authorization/provider/LocalByPassAuthProvider.js +62 -0
  12. package/dist/authorization/provider/LocalByPassAuthProvider.js.map +1 -0
  13. package/dist/authorization/provider/OIDCAuthProvider.d.ts +62 -0
  14. package/dist/authorization/provider/OIDCAuthProvider.js +173 -0
  15. package/dist/authorization/provider/OIDCAuthProvider.js.map +1 -0
  16. package/dist/authorization/rbac/RbacRulesLoader.d.ts +32 -0
  17. package/dist/authorization/rbac/RbacRulesLoader.js +105 -0
  18. package/dist/authorization/rbac/RbacRulesLoader.js.map +1 -0
  19. package/dist/authorization/rbac/UrlMatcher.d.ts +14 -0
  20. package/dist/authorization/rbac/UrlMatcher.js +44 -0
  21. package/dist/authorization/rbac/UrlMatcher.js.map +1 -0
  22. package/dist/authorizer/RealTimeAuthorizer.d.ts +28 -0
  23. package/dist/authorizer/RealTimeAuthorizer.js +152 -0
  24. package/dist/authorizer/RealTimeAuthorizer.js.map +1 -0
  25. package/dist/authorizer/index.d.ts +1 -0
  26. package/dist/authorizer/index.js +5 -0
  27. package/dist/authorizer/index.js.map +1 -0
  28. package/dist/cache/memory.d.ts +19 -0
  29. package/dist/cache/memory.js +147 -0
  30. package/dist/cache/memory.js.map +1 -0
  31. package/dist/cache/redis.d.ts +16 -0
  32. package/dist/cache/redis.js +120 -0
  33. package/dist/cache/redis.js.map +1 -0
  34. package/dist/certificate/CertificateAuthority.d.ts +38 -0
  35. package/dist/certificate/CertificateAuthority.js +233 -0
  36. package/dist/certificate/CertificateAuthority.js.map +1 -0
  37. package/dist/certificate/CertificateUtil.d.ts +60 -0
  38. package/dist/certificate/CertificateUtil.js +317 -0
  39. package/dist/certificate/CertificateUtil.js.map +1 -0
  40. package/dist/certificate/client/acme.d.ts +37 -0
  41. package/dist/certificate/client/acme.js +138 -0
  42. package/dist/certificate/client/acme.js.map +1 -0
  43. package/dist/certificate/client/hubject.d.ts +41 -0
  44. package/dist/certificate/client/hubject.js +221 -0
  45. package/dist/certificate/client/hubject.js.map +1 -0
  46. package/dist/certificate/client/interface.d.ts +12 -0
  47. package/dist/certificate/client/interface.js +5 -0
  48. package/dist/certificate/client/interface.js.map +1 -0
  49. package/dist/certificate/index.d.ts +2 -0
  50. package/dist/certificate/index.js +6 -0
  51. package/dist/certificate/index.js.map +1 -0
  52. package/dist/files/ftpServer.d.ts +4 -0
  53. package/dist/files/ftpServer.js +9 -0
  54. package/dist/files/ftpServer.js.map +1 -0
  55. package/dist/files/gcpCloudStorage.d.ts +39 -0
  56. package/dist/files/gcpCloudStorage.js +130 -0
  57. package/dist/files/gcpCloudStorage.js.map +1 -0
  58. package/dist/files/localStorage.d.ts +14 -0
  59. package/dist/files/localStorage.js +57 -0
  60. package/dist/files/localStorage.js.map +1 -0
  61. package/dist/files/s3Storage.d.ts +17 -0
  62. package/dist/files/s3Storage.js +118 -0
  63. package/dist/files/s3Storage.js.map +1 -0
  64. package/dist/index.d.ts +21 -0
  65. package/dist/index.js +25 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/networkconnection/WebsocketNetworkConnection.d.ts +135 -0
  68. package/dist/networkconnection/WebsocketNetworkConnection.js +474 -0
  69. package/dist/networkconnection/WebsocketNetworkConnection.js.map +1 -0
  70. package/dist/networkconnection/authenticator/Authenticator.d.ts +20 -0
  71. package/dist/networkconnection/authenticator/Authenticator.js +39 -0
  72. package/dist/networkconnection/authenticator/Authenticator.js.map +1 -0
  73. package/dist/networkconnection/authenticator/AuthenticatorFilter.d.ts +11 -0
  74. package/dist/networkconnection/authenticator/AuthenticatorFilter.js +30 -0
  75. package/dist/networkconnection/authenticator/AuthenticatorFilter.js.map +1 -0
  76. package/dist/networkconnection/authenticator/BasicAuthenticationFilter.d.ts +17 -0
  77. package/dist/networkconnection/authenticator/BasicAuthenticationFilter.js +51 -0
  78. package/dist/networkconnection/authenticator/BasicAuthenticationFilter.js.map +1 -0
  79. package/dist/networkconnection/authenticator/ConnectedStationFilter.d.ts +14 -0
  80. package/dist/networkconnection/authenticator/ConnectedStationFilter.js +25 -0
  81. package/dist/networkconnection/authenticator/ConnectedStationFilter.js.map +1 -0
  82. package/dist/networkconnection/authenticator/NetworkProfileFilter.d.ts +16 -0
  83. package/dist/networkconnection/authenticator/NetworkProfileFilter.js +84 -0
  84. package/dist/networkconnection/authenticator/NetworkProfileFilter.js.map +1 -0
  85. package/dist/networkconnection/authenticator/UnknownStationFilter.d.ts +16 -0
  86. package/dist/networkconnection/authenticator/UnknownStationFilter.js +25 -0
  87. package/dist/networkconnection/authenticator/UnknownStationFilter.js.map +1 -0
  88. package/dist/networkconnection/authenticator/errors/AuthenticationError.d.ts +6 -0
  89. package/dist/networkconnection/authenticator/errors/AuthenticationError.js +25 -0
  90. package/dist/networkconnection/authenticator/errors/AuthenticationError.js.map +1 -0
  91. package/dist/networkconnection/authenticator/errors/IUpgradeError.d.ts +9 -0
  92. package/dist/networkconnection/authenticator/errors/IUpgradeError.js +5 -0
  93. package/dist/networkconnection/authenticator/errors/IUpgradeError.js.map +1 -0
  94. package/dist/networkconnection/authenticator/errors/UnknownError.d.ts +6 -0
  95. package/dist/networkconnection/authenticator/errors/UnknownError.js +24 -0
  96. package/dist/networkconnection/authenticator/errors/UnknownError.js.map +1 -0
  97. package/dist/networkconnection/index.d.ts +5 -0
  98. package/dist/networkconnection/index.js +9 -0
  99. package/dist/networkconnection/index.js.map +1 -0
  100. package/dist/queue/index.d.ts +4 -0
  101. package/dist/queue/index.js +8 -0
  102. package/dist/queue/index.js.map +1 -0
  103. package/dist/queue/kafka/receiver.d.ts +35 -0
  104. package/dist/queue/kafka/receiver.js +179 -0
  105. package/dist/queue/kafka/receiver.js.map +1 -0
  106. package/dist/queue/kafka/sender.d.ts +53 -0
  107. package/dist/queue/kafka/sender.js +189 -0
  108. package/dist/queue/kafka/sender.js.map +1 -0
  109. package/dist/queue/rabbit-mq/receiver.d.ts +89 -0
  110. package/dist/queue/rabbit-mq/receiver.js +472 -0
  111. package/dist/queue/rabbit-mq/receiver.js.map +1 -0
  112. package/dist/queue/rabbit-mq/sender.d.ts +90 -0
  113. package/dist/queue/rabbit-mq/sender.js +251 -0
  114. package/dist/queue/rabbit-mq/sender.js.map +1 -0
  115. package/dist/security/SignedMeterValuesUtil.d.ts +44 -0
  116. package/dist/security/SignedMeterValuesUtil.js +135 -0
  117. package/dist/security/SignedMeterValuesUtil.js.map +1 -0
  118. package/dist/security/authentication.d.ts +2 -0
  119. package/dist/security/authentication.js +26 -0
  120. package/dist/security/authentication.js.map +1 -0
  121. package/dist/util/RequestOperations.d.ts +14 -0
  122. package/dist/util/RequestOperations.js +25 -0
  123. package/dist/util/RequestOperations.js.map +1 -0
  124. package/dist/util/StringOperations.d.ts +1 -0
  125. package/dist/util/StringOperations.js +8 -0
  126. package/dist/util/StringOperations.js.map +1 -0
  127. package/dist/util/emaidCheckDigitCalculator.d.ts +15 -0
  128. package/dist/util/emaidCheckDigitCalculator.js +179 -0
  129. package/dist/util/emaidCheckDigitCalculator.js.map +1 -0
  130. package/dist/util/idGenerator.d.ts +7 -0
  131. package/dist/util/idGenerator.js +10 -0
  132. package/dist/util/idGenerator.js.map +1 -0
  133. package/dist/util/parser.d.ts +31 -0
  134. package/dist/util/parser.js +60 -0
  135. package/dist/util/parser.js.map +1 -0
  136. package/dist/util/swagger.d.ts +5 -0
  137. package/dist/util/swagger.js +154 -0
  138. package/dist/util/swagger.js.map +1 -0
  139. package/dist/util/validator.d.ts +110 -0
  140. package/dist/util/validator.js +534 -0
  141. package/dist/util/validator.js.map +1 -0
  142. package/package.json +46 -0
@@ -0,0 +1,251 @@
1
+ import { AbstractMessageSender, CircuitBreaker, MessageState, OcppError } from '@citrineos/base';
2
+ import * as amqplib from 'amqplib';
3
+ import { instanceToPlain } from 'class-transformer';
4
+ import { Logger } from 'tslog';
5
+ /**
6
+ * Implementation of a {@link IMessageSender} using RabbitMQ as the underlying transport.
7
+ */
8
+ export class RabbitMqSender extends AbstractMessageSender {
9
+ /**
10
+ * Constants
11
+ */
12
+ static QUEUE_PREFIX = 'amqp_queue_';
13
+ static RECONNECT_DELAY = 5000;
14
+ /**
15
+ * Fields
16
+ */
17
+ _connection;
18
+ _channel;
19
+ _reconnecting = false;
20
+ _abortReconnectController;
21
+ _circuitBreaker;
22
+ _reconnectInterval;
23
+ /**
24
+ * Constructor for the class.
25
+ *
26
+ * @param {SystemConfig} config - The system configuration.
27
+ * @param {Logger<ILogObj>} [logger] - The logger object.
28
+ */
29
+ constructor(config, logger, circuitBreaker) {
30
+ super(config, logger);
31
+ this._circuitBreaker = circuitBreaker ?? new CircuitBreaker();
32
+ this._circuitBreaker.onStateChange(this._onCircuitBreakerStateChange.bind(this));
33
+ this._connectWithRetry()
34
+ .then((channel) => {
35
+ this._channel = channel;
36
+ })
37
+ .catch((error) => {
38
+ this._logger.error('Failed to connect to RabbitMQ', error);
39
+ });
40
+ }
41
+ /**
42
+ * Methods
43
+ */
44
+ /**
45
+ * Sends a request message with an optional payload and returns a promise that resolves to the confirmation message.
46
+ *
47
+ * @param {IMessage<OcppRequest>} message - The message to be sent.
48
+ * @param {OcppRequest | undefined} payload - The optional payload to be sent with the message.
49
+ * @return {Promise<IMessageConfirmation>} A promise that resolves to the confirmation message.
50
+ */
51
+ sendRequest(message, payload) {
52
+ return this.send(message, payload, MessageState.Request);
53
+ }
54
+ /**
55
+ * Sends a response message and returns a promise of the message confirmation.
56
+ *
57
+ * @param {IMessage<OcppResponse | OcppError>} message - The message to send.
58
+ * @param {OcppResponse | OcppError} payload - The payload to include in the response.
59
+ * @return {Promise<IMessageConfirmation>} - A promise that resolves to the message confirmation.
60
+ */
61
+ sendResponse(message, payload) {
62
+ return this.send(message, payload, MessageState.Response);
63
+ }
64
+ /**
65
+ * Sends a message and returns a promise that resolves to a message confirmation.
66
+ *
67
+ * @param {IMessage<OcppRequest | OcppResponse | OcppError>} message - The message to be sent.
68
+ * @param {OcppRequest | OcppResponse | OcppError} [payload] - The payload to be included in the message.
69
+ * @param {MessageState} [state] - The state of the message.
70
+ * @return {Promise<IMessageConfirmation>} - A promise that resolves to a message confirmation.
71
+ */
72
+ async send(message, payload, state) {
73
+ if (this._circuitBreaker.state === 'CLOSED') {
74
+ return { success: false, payload: 'Circuit breaker is CLOSED. Cannot send message.' };
75
+ }
76
+ if (payload) {
77
+ message.payload = payload;
78
+ }
79
+ if (state) {
80
+ message.state = state;
81
+ }
82
+ if (!message.state) {
83
+ return { success: false, payload: 'Message state must be set' };
84
+ }
85
+ if (!message.payload) {
86
+ return { success: false, payload: 'Message payload must be set' };
87
+ }
88
+ const exchange = this._config.util.messageBroker.amqp?.exchange;
89
+ if (!this._channel) {
90
+ throw new Error('RabbitMQ is down: cannot unsubscribe.');
91
+ }
92
+ const channel = this._channel;
93
+ this._logger.debug(`Publishing to ${exchange}:`, message);
94
+ const success = channel.publish(exchange || '', '', Buffer.from(JSON.stringify(instanceToPlain(message)), 'utf-8'), {
95
+ contentEncoding: 'utf-8',
96
+ contentType: 'application/json',
97
+ headers: {
98
+ origin: message.origin.toString(),
99
+ eventGroup: message.eventGroup.toString(),
100
+ action: message.action.toString(),
101
+ state: message.state.toString(),
102
+ ...message.context,
103
+ tenantId: message.context.tenantId.toString(),
104
+ },
105
+ });
106
+ return { success };
107
+ }
108
+ /**
109
+ * Shuts down the sender by closing the client.
110
+ *
111
+ * @return {Promise<void>} A promise that resolves when the client is closed.
112
+ */
113
+ shutdown() {
114
+ this._abortReconnectController?.abort();
115
+ return Promise.resolve();
116
+ }
117
+ /**
118
+ * Protected Methods
119
+ */
120
+ /**
121
+ * Connect to RabbitMQ with retry logic.
122
+ * This method will keep trying to connect until successful, unless aborted.
123
+ *
124
+ * @param {AbortSignal} [abortSignal] - Optional abort signal to stop retrying.
125
+ * @return {Promise<amqplib.Channel>} A promise that resolves to the AMQP channel.
126
+ */
127
+ async _connectWithRetry(abortSignal) {
128
+ const url = this._config.util.messageBroker.amqp?.url;
129
+ if (!url) {
130
+ throw new Error('RabbitMQ URL is not configured');
131
+ }
132
+ while (true) {
133
+ if (abortSignal?.aborted) {
134
+ this._logger.warn('RabbitMQ reconnect aborted by signal.');
135
+ throw new Error('RabbitMQ reconnect aborted');
136
+ }
137
+ if (this._circuitBreaker.state === 'CLOSED') {
138
+ throw new Error('Circuit breaker is CLOSED. Cannot connect to RabbitMQ.');
139
+ }
140
+ try {
141
+ const connection = await amqplib.connect(url);
142
+ this._connection = connection;
143
+ const channel = await connection.createChannel();
144
+ channel.on('error', (err) => {
145
+ this._logger.error('AMQP channel error', err);
146
+ });
147
+ this._setupConnectionListeners();
148
+ this._circuitBreaker.triggerSuccess();
149
+ return channel;
150
+ }
151
+ catch (err) {
152
+ this._logger.error('RabbitMQ connect failed, triggering circuit breaker failure', err);
153
+ this._circuitBreaker.triggerFailure(err?.message);
154
+ await new Promise((res) => setTimeout(res, 1000));
155
+ }
156
+ }
157
+ }
158
+ _startReconnectInterval() {
159
+ if (this._reconnectInterval) {
160
+ clearInterval(this._reconnectInterval);
161
+ }
162
+ const delay = (this._config.maxReconnectDelay || 30) * 1000;
163
+ this._logger.warn(`Starting continuous reconnect attempts every ${delay / 1000} seconds while circuit breaker is CLOSED.`);
164
+ this._reconnectInterval = setInterval(() => {
165
+ this._logger.info('Attempting RabbitMQ reconnect due to circuit breaker CLOSED...');
166
+ this._connectWithRetry()
167
+ .then((channel) => {
168
+ this._logger.info('RabbitMQ reconnect attempt succeeded.');
169
+ this._channel = channel;
170
+ this._circuitBreaker.triggerSuccess();
171
+ })
172
+ .catch((err) => {
173
+ this._logger.error('RabbitMQ reconnect attempt failed.', err);
174
+ });
175
+ }, delay);
176
+ }
177
+ _onCircuitBreakerStateChange(state, reason) {
178
+ this._logger.info(`[CircuitBreaker] State changed to ${state}${reason ? `: ${reason}` : ''}`);
179
+ switch (state) {
180
+ case 'CLOSED': {
181
+ this._logger.error('Circuit breaker CLOSED: shutting down RabbitMQ sender. Reason:', reason);
182
+ void this.shutdown();
183
+ if (this._reconnectInterval) {
184
+ this._logger.info('Clearing reconnect interval as circuit breaker is now CLOSED.');
185
+ clearInterval(this._reconnectInterval);
186
+ this._reconnectInterval = undefined;
187
+ }
188
+ break;
189
+ }
190
+ case 'OPEN': {
191
+ this._logger.info('Circuit breaker is OPEN. Will attempt to (re)initialize RabbitMQ connection.');
192
+ if (this._reconnectInterval) {
193
+ this._logger.info('Clearing reconnect interval as circuit breaker is now OPEN.');
194
+ clearInterval(this._reconnectInterval);
195
+ this._reconnectInterval = undefined;
196
+ }
197
+ this._connectWithRetry()
198
+ .then((channel) => {
199
+ this._logger.info('RabbitMQ connection (re)initialized.');
200
+ this._channel = channel;
201
+ this._circuitBreaker.triggerSuccess();
202
+ })
203
+ .catch((err) => {
204
+ this._logger.error('RabbitMQ (re)init failed.', err);
205
+ });
206
+ break;
207
+ }
208
+ case 'FAILING': {
209
+ this._logger.warn('Circuit breaker is FAILING. RabbitMQ sender will not send messages until recovery. Reason:', reason);
210
+ this._logger.info('Attempting to start reconnect interval after circuit breaker FAILING.');
211
+ this._startReconnectInterval();
212
+ break;
213
+ }
214
+ default:
215
+ this._logger.warn('Unknown circuit breaker state:', state);
216
+ break;
217
+ }
218
+ }
219
+ /**
220
+ * Setup listeners for connection and channel events.
221
+ * This will handle disconnections and errors.
222
+ * Ensures listeners are not attached multiple times to the same connection.
223
+ */
224
+ _setupConnectionListeners() {
225
+ if (this._connection) {
226
+ // Only attach listeners if not already attached to this connection
227
+ if (this._connection._listenersAttached)
228
+ return;
229
+ this._connection.removeAllListeners('close');
230
+ this._connection.removeAllListeners('error');
231
+ this._connection.on('close', () => this._handleDisconnect());
232
+ this._connection.on('error', () => this._handleDisconnect());
233
+ this._connection._listenersAttached = true;
234
+ }
235
+ }
236
+ /**
237
+ * Handle RabbitMQ disconnection.
238
+ * This method will attempt to reconnect to RabbitMQ when the connection is lost.
239
+ * Debounces concurrent reconnects.
240
+ */
241
+ async _handleDisconnect() {
242
+ this._logger.warn('RabbitMQ connection lost. Triggering circuit breaker failure.');
243
+ this._connection = undefined;
244
+ this._channel = undefined;
245
+ this._circuitBreaker.triggerFailure('RabbitMQ connection lost');
246
+ if (this._circuitBreaker.state === 'CLOSED' || this._circuitBreaker.state === 'FAILING') {
247
+ this._startReconnectInterval();
248
+ }
249
+ }
250
+ }
251
+ //# sourceMappingURL=sender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sender.js","sourceRoot":"","sources":["../../../src/queue/rabbit-mq/sender.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ import type { BootstrapConfig, IFileStorage, SystemConfig } from '@citrineos/base';
2
+ import { OCPP2_0_1 } from '@citrineos/base';
3
+ import type { ILogObj } from 'tslog';
4
+ import { Logger } from 'tslog';
5
+ /**
6
+ * Util to process and validate signed meter values.
7
+ */
8
+ export declare class SignedMeterValuesUtil {
9
+ private readonly _fileStorage;
10
+ private readonly _logger;
11
+ private readonly _chargingStationSecurityInfoRepository;
12
+ private readonly _signedMeterValuesConfiguration;
13
+ /**
14
+ * @param {IFileStorage} [fileStorage] - The `fileStorage` allows access to the configured file storage.
15
+ *
16
+ * @param {BootstrapConfig & SystemConfig} config - The `config` contains the current system configuration settings.
17
+ *
18
+ * @param {Logger<ILogObj>} [logger] - The `logger` represents an instance of {@link Logger<ILogObj>}.
19
+ *
20
+ */
21
+ constructor(fileStorage: IFileStorage, config: BootstrapConfig & SystemConfig, logger: Logger<ILogObj>);
22
+ /**
23
+ * Checks the validity of a meter value.
24
+ *
25
+ * If a meter value is unsigned, it is valid.
26
+ *
27
+ * If a meter value is signed, it is valid if:
28
+ * - SignedMeterValuesConfig is configured
29
+ * AND
30
+ * - The incoming signed meter value's signing method matches the configured signing method
31
+ * AND
32
+ * - The incoming signed meter value's public key is empty but there is a public key stored for that charging station
33
+ * OR
34
+ * - The incoming signed meter value's public key isn't empty and it matches the configured public key
35
+ *
36
+ * @param stationId - The charging station the meter values belong to
37
+ * @param meterValues - The list of meter values
38
+ */
39
+ validateMeterValues(tenantId: number, stationId: string, meterValues: [OCPP2_0_1.MeterValueType, ...OCPP2_0_1.MeterValueType[]]): Promise<boolean>;
40
+ private validateSignedSampledValue;
41
+ private validateSignedMeterValueSignature;
42
+ private validateRsaSignature;
43
+ private formatKey;
44
+ }
@@ -0,0 +1,135 @@
1
+ import { sequelize } from '@citrineos/data';
2
+ import { OCPP2_0_1, SignedMeterValuesConfig } from '@citrineos/base';
3
+ import { Logger } from 'tslog';
4
+ import * as crypto from 'node:crypto';
5
+ import { stringToArrayBuffer } from 'pvutils';
6
+ /**
7
+ * Util to process and validate signed meter values.
8
+ */
9
+ export class SignedMeterValuesUtil {
10
+ _fileStorage;
11
+ _logger;
12
+ _chargingStationSecurityInfoRepository;
13
+ _signedMeterValuesConfiguration;
14
+ /**
15
+ * @param {IFileStorage} [fileStorage] - The `fileStorage` allows access to the configured file storage.
16
+ *
17
+ * @param {BootstrapConfig & SystemConfig} config - The `config` contains the current system configuration settings.
18
+ *
19
+ * @param {Logger<ILogObj>} [logger] - The `logger` represents an instance of {@link Logger<ILogObj>}.
20
+ *
21
+ */
22
+ constructor(fileStorage, config, logger) {
23
+ this._fileStorage = fileStorage;
24
+ this._logger = logger;
25
+ this._chargingStationSecurityInfoRepository =
26
+ new sequelize.SequelizeChargingStationSecurityInfoRepository(config, logger);
27
+ this._signedMeterValuesConfiguration =
28
+ config.modules.transactions.signedMeterValuesConfiguration;
29
+ }
30
+ /**
31
+ * Checks the validity of a meter value.
32
+ *
33
+ * If a meter value is unsigned, it is valid.
34
+ *
35
+ * If a meter value is signed, it is valid if:
36
+ * - SignedMeterValuesConfig is configured
37
+ * AND
38
+ * - The incoming signed meter value's signing method matches the configured signing method
39
+ * AND
40
+ * - The incoming signed meter value's public key is empty but there is a public key stored for that charging station
41
+ * OR
42
+ * - The incoming signed meter value's public key isn't empty and it matches the configured public key
43
+ *
44
+ * @param stationId - The charging station the meter values belong to
45
+ * @param meterValues - The list of meter values
46
+ */
47
+ async validateMeterValues(tenantId, stationId, meterValues) {
48
+ for (const meterValue of meterValues) {
49
+ for (const sampledValue of meterValue.sampledValue) {
50
+ if (sampledValue.signedMeterValue) {
51
+ const validMeterValues = await this.validateSignedSampledValue(tenantId, stationId, sampledValue.signedMeterValue);
52
+ if (!validMeterValues) {
53
+ return false;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ return true;
59
+ }
60
+ async validateSignedSampledValue(tenantId, stationId, signedMeterValue) {
61
+ if (signedMeterValue.publicKey && signedMeterValue.publicKey.length > 0) {
62
+ const incomingPublicKeyIsValid = await this.validateSignedMeterValueSignature(signedMeterValue);
63
+ if (this._signedMeterValuesConfiguration && incomingPublicKeyIsValid) {
64
+ await this._chargingStationSecurityInfoRepository.readOrCreateChargingStationInfo(tenantId, stationId, this._signedMeterValuesConfiguration.publicKeyFileId);
65
+ return true;
66
+ }
67
+ else {
68
+ return false;
69
+ }
70
+ }
71
+ else {
72
+ const chargingStationPublicKeyFileId = await this._chargingStationSecurityInfoRepository.readChargingStationPublicKeyFileId(tenantId, stationId);
73
+ return await this.validateSignedMeterValueSignature(signedMeterValue, chargingStationPublicKeyFileId);
74
+ }
75
+ }
76
+ async validateSignedMeterValueSignature(signedMeterValue, publicKeyFileId) {
77
+ const incomingPublicKeyString = signedMeterValue.publicKey;
78
+ const signingMethod = signedMeterValue.signingMethod;
79
+ if (!this._signedMeterValuesConfiguration?.publicKeyFileId) {
80
+ this._logger.warn('Invalid signature because public key is missing from system config.');
81
+ return false;
82
+ }
83
+ if (publicKeyFileId &&
84
+ publicKeyFileId !== this._signedMeterValuesConfiguration?.publicKeyFileId) {
85
+ this._logger.warn('Invalid signature because incoming public key does not match configured public key.');
86
+ return false;
87
+ }
88
+ if (!publicKeyFileId && incomingPublicKeyString.length === 0) {
89
+ this._logger.warn('Invalid signature because no configured public key and incoming signed meter values has no public key.');
90
+ return false;
91
+ }
92
+ if (this._signedMeterValuesConfiguration?.signingMethod !== signingMethod) {
93
+ this._logger.warn('Invalid signature because incoming signing method does not match configured signing method.');
94
+ return false;
95
+ }
96
+ const configuredPublicKey = this.formatKey(await this._fileStorage.getFile(this._signedMeterValuesConfiguration.publicKeyFileId));
97
+ if (incomingPublicKeyString.length > 0) {
98
+ const signedMeterValuePublicKey = Buffer.from(signedMeterValue.publicKey, 'base64').toString();
99
+ if (configuredPublicKey !== signedMeterValuePublicKey) {
100
+ return false;
101
+ }
102
+ }
103
+ switch (signingMethod) {
104
+ case 'RSASSA-PKCS1-v1_5':
105
+ return await this.validateRsaSignature(configuredPublicKey, signingMethod, signedMeterValue.encodingMethod, signedMeterValue.signedMeterData);
106
+ default:
107
+ this._logger.warn(`${signingMethod} is not supported for Signed Meter Values.`);
108
+ return false;
109
+ }
110
+ }
111
+ async validateRsaSignature(configuredPublicKey, signingMethod, encodingMethod, signatureData) {
112
+ try {
113
+ const cryptoPublicKey = await crypto.subtle.importKey('spki', stringToArrayBuffer(atob(configuredPublicKey)), { name: signingMethod, hash: encodingMethod }, true, ['verify']);
114
+ const signatureBuffer = Buffer.from(signatureData, 'base64');
115
+ // For now, we only care that the signature could be read, regardless of the value in the signature.
116
+ await crypto.subtle.verify(signingMethod, cryptoPublicKey, signatureBuffer, signatureBuffer);
117
+ return true;
118
+ }
119
+ catch (e) {
120
+ const errorMessage = e instanceof DOMException ? e.message : JSON.stringify(e);
121
+ this._logger.warn(`Error decrypting public key or verifying signature from Signed Meter Value. Error: ${errorMessage}`);
122
+ return false;
123
+ }
124
+ }
125
+ formatKey(key) {
126
+ if (!key) {
127
+ throw new Error('Public key file is missing.');
128
+ }
129
+ return key
130
+ .replace('-----BEGIN PUBLIC KEY-----', '')
131
+ .replace('-----END PUBLIC KEY-----', '')
132
+ .replace(/(\r\n|\n|\r)/gm, '');
133
+ }
134
+ }
135
+ //# sourceMappingURL=SignedMeterValuesUtil.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SignedMeterValuesUtil.js","sourceRoot":"","sources":["../../src/security/SignedMeterValuesUtil.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function generatePassword(): string;
2
+ export declare function isValidPassword(password: string): boolean;
@@ -0,0 +1,26 @@
1
+ // SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { randomBytes } from 'node:crypto';
5
+ const MIN_LENGTH = 16;
6
+ const MAX_LENGTH = 40;
7
+ const LOWERCASE = 'abcdefghijklmnopqrstuvwxyz';
8
+ const UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
9
+ const DIGITS = '0123456789';
10
+ const SYMBOLS = '*-_=:+|@.';
11
+ const CHARSET = [...LOWERCASE, ...UPPERCASE, ...DIGITS, ...SYMBOLS];
12
+ export function generatePassword() {
13
+ return [...randomBytes(MAX_LENGTH)].map((value) => CHARSET[value % CHARSET.length]).join('');
14
+ }
15
+ export function isValidPassword(password) {
16
+ if (password.length < MIN_LENGTH || password.length > MAX_LENGTH) {
17
+ return false;
18
+ }
19
+ for (const char of password) {
20
+ if (!CHARSET.includes(char)) {
21
+ return false;
22
+ }
23
+ }
24
+ return true;
25
+ }
26
+ //# sourceMappingURL=authentication.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authentication.js","sourceRoot":"","sources":["../../src/security/authentication.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,EAAE;AACF,sCAAsC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,UAAU,GAAG,EAAE,CAAC;AAEtB,MAAM,SAAS,GAAG,4BAA4B,CAAC;AAC/C,MAAM,SAAS,GAAG,4BAA4B,CAAC;AAC/C,MAAM,MAAM,GAAG,YAAY,CAAC;AAC5B,MAAM,OAAO,GAAG,WAAW,CAAC;AAC5B,MAAM,OAAO,GAAG,CAAC,GAAG,SAAS,EAAE,GAAG,SAAS,EAAE,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC;AAEpE,MAAM,UAAU,gBAAgB;IAC9B,OAAO,CAAC,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC/F,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,QAAgB;IAC9C,IAAI,QAAQ,CAAC,MAAM,GAAG,UAAU,IAAI,QAAQ,CAAC,MAAM,GAAG,UAAU,EAAE,CAAC;QACjE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5B,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { IncomingMessage } from 'http';
2
+ /**
3
+ * Extracts credentials from the Authorization header.
4
+ *
5
+ * The Authorization header is formatted as follows:
6
+ * AUTHORIZATION: Basic <Base64 encoded(<Configured ChargingStationId>:<Configured BasicAuthPassword>)>
7
+ *
8
+ * @param {http.IncomingMessage} req - The request object.
9
+ * @returns Extracted credentials.
10
+ */
11
+ export declare function extractBasicCredentials(req: IncomingMessage): {
12
+ username?: string;
13
+ password?: string;
14
+ };
@@ -0,0 +1,25 @@
1
+ // SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ import { IncomingMessage } from 'http';
5
+ import { splitOnce } from './StringOperations.js';
6
+ /**
7
+ * Extracts credentials from the Authorization header.
8
+ *
9
+ * The Authorization header is formatted as follows:
10
+ * AUTHORIZATION: Basic <Base64 encoded(<Configured ChargingStationId>:<Configured BasicAuthPassword>)>
11
+ *
12
+ * @param {http.IncomingMessage} req - The request object.
13
+ * @returns Extracted credentials.
14
+ */
15
+ export function extractBasicCredentials(req) {
16
+ const authHeader = req.headers.authorization;
17
+ if (!authHeader?.startsWith('Basic ')) {
18
+ return {};
19
+ }
20
+ const base64Credentials = authHeader.split(' ')[1];
21
+ const decodedCredentials = Buffer.from(base64Credentials, 'base64').toString();
22
+ const [username, password] = splitOnce(decodedCredentials, ':');
23
+ return { username, password };
24
+ }
25
+ //# sourceMappingURL=RequestOperations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RequestOperations.js","sourceRoot":"","sources":["../../src/util/RequestOperations.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,EAAE;AACF,sCAAsC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CAAC,GAAoB;IAI1D,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;IAC7C,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACtC,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,iBAAiB,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,MAAM,kBAAkB,GAAG,MAAM,CAAC,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;IAE/E,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IAEhE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAChC,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function splitOnce(value: string, separator: string): [string, string?];
@@ -0,0 +1,8 @@
1
+ // SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ export function splitOnce(value, separator) {
5
+ const idx = value.indexOf(separator);
6
+ return idx == -1 ? [value] : [value.substring(0, idx), value.substring(idx + separator.length)];
7
+ }
8
+ //# sourceMappingURL=StringOperations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StringOperations.js","sourceRoot":"","sources":["../../src/util/StringOperations.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,EAAE;AACF,sCAAsC;AACtC,MAAM,UAAU,SAAS,CAAC,KAAa,EAAE,SAAiB;IACxD,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACrC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;AAClG,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Calculate check digit for eMAID according to eMI³ specification
3
+ * Based on the algorithm described in "Check Digit Calculation for Contract-IDs"
4
+ *
5
+ * This implementation can detect five most frequent error types:
6
+ * 1. Single error: one character is wrong
7
+ * 2. Adjacent transposition: two adjacent characters are swapped
8
+ * 3. Twin error: two identical adjacent characters are both changed
9
+ * 4. Jump transposition: abc becomes cba
10
+ * 5. Jump twin error: aca becomes bcb
11
+ *
12
+ * @param emaidWithoutCheckDigit - The first 14 characters of the eMAID (without check digit)
13
+ * @returns The calculated check digit character
14
+ */
15
+ export declare function calculateCheckDigit(emaidWithoutCheckDigit: string): string;