1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.forgerock.audit.handlers.csv;
17
18 import static java.lang.String.format;
19 import static java.util.Collections.singletonMap;
20 import static java.util.concurrent.TimeUnit.MILLISECONDS;
21 import static org.forgerock.audit.handlers.csv.CsvSecureConstants.HEADER_HMAC;
22 import static org.forgerock.audit.handlers.csv.CsvSecureConstants.HEADER_SIGNATURE;
23 import static org.forgerock.audit.handlers.csv.CsvSecureConstants.SIGNATURE_ALGORITHM;
24 import static org.forgerock.audit.handlers.csv.CsvSecureUtils.dataToSign;
25 import static org.forgerock.util.Reject.checkNotNull;
26
27 import java.io.File;
28 import java.io.FileOutputStream;
29 import java.io.IOException;
30 import java.io.Writer;
31 import java.security.PrivateKey;
32 import java.security.PublicKey;
33 import java.security.SignatureException;
34 import java.util.HashMap;
35 import java.util.Map;
36 import java.util.Random;
37 import java.util.concurrent.Executors;
38 import java.util.concurrent.RejectedExecutionException;
39 import java.util.concurrent.ScheduledExecutorService;
40 import java.util.concurrent.ScheduledFuture;
41 import java.util.concurrent.locks.ReentrantLock;
42
43 import javax.crypto.SecretKey;
44 import javax.crypto.spec.SecretKeySpec;
45
46 import org.forgerock.audit.events.handlers.writers.RotatableWriter;
47 import org.forgerock.audit.events.handlers.writers.TextWriter;
48 import org.forgerock.audit.events.handlers.writers.TextWriterAdapter;
49 import org.forgerock.audit.events.handlers.writers.RotatableWriter.RolloverLifecycleHook;
50 import org.forgerock.audit.rotation.RotationContext;
51 import org.forgerock.audit.rotation.RotationHooks;
52 import org.forgerock.audit.secure.JcaKeyStoreHandler;
53 import org.forgerock.audit.secure.KeyStoreHandler;
54 import org.forgerock.audit.secure.KeyStoreHandlerDecorator;
55 import org.forgerock.audit.secure.KeyStoreSecureStorage;
56 import org.forgerock.audit.secure.SecureStorageException;
57 import org.forgerock.util.Reject;
58 import org.forgerock.util.annotations.VisibleForTesting;
59 import org.forgerock.util.encode.Base64;
60 import org.forgerock.util.time.Duration;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63 import org.supercsv.prefs.CsvPreference;
64
65
66
67
68
69
70 class SecureCsvWriter implements CsvWriter, RolloverLifecycleHook {
71
72 private static final Logger logger = LoggerFactory.getLogger(SecureCsvWriter.class);
73
74 private final CsvFormatter csvFormatter;
75 private final String[] headers;
76 private Writer csvWriter;
77 private RotatableWriter rotatableWriter;
78
79 private HmacCalculator hmacCalculator;
80 private final ScheduledExecutorService scheduler;
81 private final ReentrantLock signatureLock = new ReentrantLock();
82 private final Runnable signatureTask;
83 private KeyStoreSecureStorage secureStorage;
84 private final Duration signatureInterval;
85 private ScheduledFuture<?> scheduledSignature;
86
87 private String lastHMAC;
88 private byte[] lastSignature;
89 private boolean headerWritten = false;
90 private final Random random;
91 private File keyStoreFile;
92 private String keyStorePassword;
93
94 SecureCsvWriter(File csvFile, String[] headers, CsvPreference csvPreference,
95 CsvAuditEventHandlerConfiguration config, KeyStoreHandler keyStoreHandler, Random random)
96 throws IOException {
97 Reject.ifFalse(config.getSecurity().isEnabled(), "SecureCsvWriter should only be used if security is enabled");
98 final boolean fileAlreadyInitialized = csvFile.exists() && csvFile.length() > 0;
99 this.random = random;
100 this.keyStoreFile = new File(csvFile.getPath() + ".keystore");
101 this.headers = checkNotNull(headers, "The headers can't be null.");
102 this.csvFormatter = new CsvFormatter(csvPreference);
103 this.csvWriter = constructWriter(csvFile, fileAlreadyInitialized, config);
104 this.hmacCalculator = new HmacCalculator(CsvSecureConstants.HMAC_ALGORITHM);
105
106 try {
107 KeyStoreHandlerDecorator keyStoreHandlerDecorated = new KeyStoreHandlerDecorator(keyStoreHandler);
108 SecretKey password = keyStoreHandlerDecorated.readSecretKeyFromKeyStore(CsvSecureConstants.ENTRY_PASSWORD);
109 if (password == null) {
110 throw new IllegalArgumentException(format(
111 "No '%s' symmetric key found in the provided keystore: %s. This key must be provided.",
112 CsvSecureConstants.ENTRY_PASSWORD, keyStoreHandlerDecorated.getLocation()));
113 }
114 this.keyStorePassword = Base64.encode(password.getEncoded());
115 KeyStoreHandler hmacKeyStoreHandler =
116 new JcaKeyStoreHandler(CsvSecureConstants.KEYSTORE_TYPE, keyStoreFile.getPath(), keyStorePassword);
117 PublicKey publicSignatureKey =
118 keyStoreHandlerDecorated.readPublicKeyFromKeyStore(CsvSecureConstants.ENTRY_SIGNATURE);
119 PrivateKey privateSignatureKey =
120 keyStoreHandlerDecorated.readPrivateKeyFromKeyStore(CsvSecureConstants.ENTRY_SIGNATURE);
121 if (publicSignatureKey == null || privateSignatureKey == null) {
122 throw new IllegalArgumentException(format(
123 "No '%s' signing key found in the provided keystore: %s. This key must be provided.",
124 CsvSecureConstants.ENTRY_SIGNATURE, keyStoreHandlerDecorated.getLocation()));
125 }
126 this.secureStorage = new KeyStoreSecureStorage(hmacKeyStoreHandler, publicSignatureKey,
127 privateSignatureKey);
128 final CsvAuditEventHandlerConfiguration.CsvSecurity securityConfiguration = config.getSecurity();
129 if (fileAlreadyInitialized) {
130
131 CsvSecureVerifier verifier = new CsvSecureVerifier(csvFile, csvPreference, secureStorage);
132 CsvSecureVerifier.VerificationResult verificationResult = verifier.verify();
133 if (!verificationResult.hasPassedVerification()) {
134 throw new IOException("The CSV file was tampered: " + verificationResult.getFailureReason());
135 }
136
137
138 final String[] actualHeaders = verifier.getHeaders();
139 if (actualHeaders != null) {
140 if (actualHeaders.length != headers.length) {
141 throw new IOException("Resuming an existing CSV file but the headers do not match.");
142 }
143 for (int idx = 0; idx < actualHeaders.length; idx++) {
144 if (!actualHeaders[idx].equals(headers[idx])) {
145 throw new IOException("Resuming an existing CSV file but the headers do not match.");
146 }
147 }
148 }
149
150 SecretKey currentKey = secureStorage.readCurrentKey();
151 if (currentKey == null) {
152 throw new IllegalStateException("We are supposed to resume but there is not entry for CurrentKey.");
153 }
154 this.hmacCalculator.setCurrentKey(currentKey.getEncoded());
155
156 setLastHMAC(verifier.getLastHMAC());
157 setLastSignature(verifier.getLastSignature());
158 this.headerWritten = true;
159 } else {
160 initHmacCalculatorWithRandomData();
161 }
162
163 this.signatureInterval = securityConfiguration.getSignatureIntervalDuration();
164 this.scheduler = Executors.newScheduledThreadPool(1);
165 this.signatureTask = new Runnable() {
166 @Override
167 public void run() {
168 try {
169 writeSignature(csvWriter);
170 } catch (Exception ex) {
171 logger.error("An error occurred while writing the signature", ex);
172 }
173 }
174 };
175 } catch (Exception e) {
176 throw new RuntimeException("Error when initializing a secure CSV writer", e);
177 }
178 }
179
180 @Override
181 public void beforeRollingOver() {
182
183
184
185 signatureLock.lock();
186 }
187
188 @Override
189 public void afterRollingOver() {
190 signatureLock.unlock();
191 }
192
193 private void initHmacCalculatorWithRandomData() throws SecureStorageException {
194 this.hmacCalculator.setCurrentKey(getRandomBytes());
195
196 secureStorage.writeInitialKey(hmacCalculator.getCurrentKey());
197 secureStorage.writeCurrentKey(hmacCalculator.getCurrentKey());
198 }
199
200 private byte[] getRandomBytes() {
201 byte[] randomBytes = new byte[32];
202 this.random.nextBytes(randomBytes);
203 return randomBytes;
204 }
205
206 private Writer constructWriter(File csvFile, boolean append, CsvAuditEventHandlerConfiguration config)
207 throws IOException {
208 TextWriter textWriter;
209 if (config.getFileRotation().isRotationEnabled()) {
210 rotatableWriter = new RotatableWriter(csvFile, config, append, this);
211 rotatableWriter.registerRotationHooks(new SecureCsvWriterRotationHooks());
212 textWriter = rotatableWriter;
213 } else {
214 textWriter = new TextWriter.Stream(new FileOutputStream(csvFile, append));
215 }
216
217 if (config.getBuffering().isEnabled()) {
218 logger.warn("Secure CSV logging does not support buffering. Buffering config will be ignored.");
219 }
220 return new TextWriterAdapter(textWriter);
221 }
222
223 @Override
224 public void flush() throws IOException {
225 csvWriter.flush();
226 }
227
228 @Override
229 public void close() throws IOException {
230 flush();
231 signatureLock.lock();
232 try {
233 forceWriteSignature(csvWriter);
234 } finally {
235 signatureLock.unlock();
236 }
237 scheduler.shutdown();
238 try {
239 while (!scheduler.awaitTermination(500, MILLISECONDS)) {
240 logger.debug("Waiting to terminate the scheduler.");
241 }
242 } catch (InterruptedException ex) {
243 logger.error("Unable to terminate the scheduler", ex);
244 Thread.currentThread().interrupt();
245 }
246 csvWriter.close();
247 }
248
249 private void forceWriteSignature(Writer writer) throws IOException {
250 if (scheduledSignature != null && scheduledSignature.cancel(false)) {
251
252 writeSignature(writer);
253 }
254 }
255
256 public void writeHeader(String... header) throws IOException {
257 writeHeader(csvWriter, header);
258 }
259
260 public void writeHeader(Writer writer, String... header) throws IOException {
261 String[] newHeader = addExtraColumns(header);
262 writer.write(csvFormatter.formatHeader(newHeader));
263 logger.trace("Header written to file");
264 headerWritten = true;
265 }
266
267 @VisibleForTesting
268 void writeSignature(Writer writer) throws IOException {
269
270
271 signatureLock.lock();
272 try {
273 lastSignature = secureStorage.sign(dataToSign(lastSignature, lastHMAC));
274 logger.trace("Calculated new Signature");
275 Map<String, String> values = singletonMap(HEADER_SIGNATURE, Base64.encode(lastSignature));
276 writeEvent(writer, values);
277 logger.trace("Signature written to file");
278
279
280 secureStorage.writeCurrentSignatureKey(new SecretKeySpec(lastSignature, SIGNATURE_ALGORITHM));
281 logger.trace("Signature written to secureStorage");
282 } catch (SecureStorageException ex) {
283 logger.error(ex.getMessage(), ex);
284 throw new IOException(ex);
285 } finally {
286 signatureLock.unlock();
287 flush();
288 }
289 }
290
291
292
293
294
295
296
297
298
299
300 @Override
301 public boolean forceRotation() throws IOException {
302 return rotatableWriter != null ? rotatableWriter.forceRotation() : false;
303 }
304
305
306
307
308
309
310 @Override
311 public void writeEvent(Map<String, String> values) throws IOException {
312 writeEvent(csvWriter, values);
313 }
314
315
316
317
318
319
320 public void writeEvent(Writer writer, Map<String, String> values) throws IOException {
321 signatureLock.lock();
322 try {
323 if (!headerWritten) {
324 writeHeader(headers);
325 }
326 String[] extendedHeaders = addExtraColumns(headers);
327
328 Map<String, String> extendedValues = new HashMap<>(values);
329 if (!values.containsKey(CsvSecureConstants.HEADER_SIGNATURE)) {
330 insertHMACSignature(extendedValues, headers);
331 }
332
333 writer.write(csvFormatter.formatEvent(extendedValues, extendedHeaders));
334 writer.flush();
335
336 secureStorage.writeCurrentKey(hmacCalculator.getCurrentKey());
337
338
339 if (!values.containsKey(HEADER_SIGNATURE)
340 && (scheduledSignature == null || scheduledSignature.isDone())) {
341 logger.trace("Triggering a new signature task to be executed in {}", signatureInterval);
342 try {
343 scheduledSignature = scheduler.schedule(signatureTask, signatureInterval.getValue(),
344 signatureInterval.getUnit());
345 } catch (RejectedExecutionException e) {
346 logger.error(e.getMessage(), e);
347 }
348 }
349 } catch (SecureStorageException ex) {
350 throw new IOException(ex);
351 } finally {
352 signatureLock.unlock();
353 }
354 }
355
356 private void insertHMACSignature(Map<String, String> values, String[] nameMapping) throws IOException {
357 try {
358 lastHMAC = hmacCalculator.calculate(dataToSign(logger, values, nameMapping));
359 values.put(CsvSecureConstants.HEADER_HMAC, lastHMAC);
360 } catch (SignatureException ex) {
361 logger.error(ex.getMessage(), ex);
362 throw new IOException(ex);
363 }
364 }
365
366 private String[] addExtraColumns(String... header) {
367 String[] newHeader = new String[header.length + 2];
368 System.arraycopy(header, 0, newHeader, 0, header.length);
369 newHeader[header.length] = HEADER_HMAC;
370 newHeader[header.length + 1] = HEADER_SIGNATURE;
371 return newHeader;
372 }
373
374 private void setLastHMAC(String lastHMac) {
375 this.lastHMAC = lastHMac;
376 }
377
378 private void setLastSignature(byte[] lastSignature) {
379 this.lastSignature = lastSignature;
380 }
381
382 private void writeLastSignature(Writer writer) throws IOException {
383
384
385 signatureLock.lock();
386 try {
387 Map<String, String> values = singletonMap(HEADER_SIGNATURE, Base64.encode(lastSignature));
388 writeEvent(writer, values);
389 logger.trace("Signature from previous file written to new file");
390 } catch (IOException ex) {
391 logger.error(ex.getMessage(), ex);
392 throw new IOException(ex);
393 } finally {
394 signatureLock.unlock();
395 }
396 }
397
398 private class SecureCsvWriterRotationHooks implements RotationHooks {
399
400 @Override
401 public void preRotationAction(RotationContext context) throws IOException {
402
403 forceWriteSignature(context.getWriter());
404 }
405
406 @Override
407 public void postRotationAction(RotationContext context) throws IOException {
408
409 String currentName = keyStoreFile.getName();
410 String nextName = currentName.replaceFirst(context.getInitialFile().getName(),
411 context.getNextFile().getName());
412 final File nextFile = new File(keyStoreFile.getParent(), nextName);
413 logger.trace("Renaming keystore file {} to {}", currentName, nextName);
414 boolean renamed = keyStoreFile.renameTo(nextFile);
415 if (!renamed) {
416 logger.error("Unable to rename {} to {}", keyStoreFile.getAbsolutePath(), nextFile.getAbsolutePath());
417 }
418 try {
419 secureStorage.setKeyStoreHandler(new JcaKeyStoreHandler(CsvSecureConstants.KEYSTORE_TYPE,
420 keyStoreFile.getPath(), keyStorePassword));
421 logger.trace("Updated secureStorage to reference new keyStoreFile");
422 initHmacCalculatorWithRandomData();
423 } catch (Exception ex) {
424 throw new IOException(ex);
425 }
426
427 Writer writer = context.getWriter();
428 writeHeader(writer, headers);
429
430 writeLastSignature(writer);
431
432 writer.flush();
433 }
434 }
435 }