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 org.forgerock.audit.handlers.csv.CsvSecureConstants.*;
19 import static org.forgerock.audit.handlers.csv.CsvSecureUtils.*;
20
21 import java.io.BufferedReader;
22 import java.io.File;
23 import java.io.FileNotFoundException;
24 import java.io.FileReader;
25 import java.io.IOException;
26 import java.security.SignatureException;
27 import java.util.Arrays;
28 import java.util.Map;
29
30 import javax.crypto.SecretKey;
31
32 import org.forgerock.audit.secure.SecureStorage;
33 import org.forgerock.audit.secure.SecureStorageException;
34 import org.forgerock.util.encode.Base64;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37 import org.supercsv.io.CsvMapReader;
38 import org.supercsv.io.ICsvMapReader;
39 import org.supercsv.prefs.CsvPreference;
40
41
42
43
44 class CsvSecureVerifier {
45
46 private static final Logger logger = LoggerFactory.getLogger(CsvSecureVerifier.class);
47
48 private File csvFile;
49 private final CsvPreference csvPreference;
50 private final HmacCalculator hmacCalculator;
51 private final SecureStorage secureStorage;
52 private String lastHMAC;
53 private byte[] lastSignature;
54 private String[] headers;
55
56
57
58
59
60
61
62
63
64
65
66 public CsvSecureVerifier(File csvFile, CsvPreference csvPreference, SecureStorage secureStorage) {
67 this.csvFile = csvFile;
68 this.csvPreference = csvPreference;
69 this.secureStorage = secureStorage;
70
71 try {
72 SecretKey initialKey = secureStorage.readInitialKey();
73 if (initialKey == null) {
74 throw new IllegalStateException("Expecting to find an initial key into the keystore.");
75 }
76
77 this.hmacCalculator = new HmacCalculator(HMAC_ALGORITHM);
78 this.hmacCalculator.setCurrentKey(initialKey.getEncoded());
79 } catch (SecureStorageException e) {
80 throw new IllegalStateException(e);
81 }
82 }
83
84 public VerificationResult verify() throws IOException {
85 boolean lastRowWasSigned = false;
86 try (ICsvMapReader csvReader = newBufferedCsvMapReader()) {
87 final String[] header = csvReader.getHeader(true);
88
89
90 int checkCount = 0;
91 for (String string : header) {
92 if (HEADER_HMAC.equals(string) || HEADER_SIGNATURE.equals(string)) {
93 checkCount++;
94 }
95 }
96
97 if (!(HEADER_HMAC.equals(header[header.length - 2])
98 && HEADER_SIGNATURE.equals(header[header.length - 1]))) {
99 String msg = "Found only " + checkCount + " checked headers from : " + Arrays.toString(header);
100 logger.debug(msg);
101 return newVerificationFailureResult(msg);
102 }
103 this.headers = new String[header.length - 2];
104 System.arraycopy(header, 0, this.headers, 0, this.headers.length);
105
106
107 Map<String, String> values;
108 while ((values = csvReader.read(header)) != null) {
109 logger.trace("Verifying row {}", csvReader.getRowNumber());
110 lastRowWasSigned = false;
111 final String encodedSign = values.get(HEADER_SIGNATURE);
112
113 if (encodedSign != null) {
114 if (csvReader.getRowNumber() == 2) {
115
116 lastSignature = Base64.decode(encodedSign);
117 } else if (!verifySignature(encodedSign)) {
118 String msg = "The signature at row " + csvReader.getRowNumber() + " is not correct.";
119 logger.trace(msg);
120 return newVerificationFailureResult(msg);
121 } else {
122 logger.trace("The signature at row {} is correct.", csvReader.getRowNumber());
123 lastRowWasSigned = true;
124
125 continue;
126 }
127 } else {
128
129 if (!verifyHMAC(values, header)) {
130 String msg = "The HMac at row " + csvReader.getRowNumber() + " is not correct.";
131 logger.trace(msg);
132 return newVerificationFailureResult(msg);
133 } else {
134 logger.trace("The HMac at row {} is correct.", csvReader.getRowNumber());
135
136 continue;
137 }
138 }
139 }
140 }
141
142 try {
143 SecretKey currentKey = secureStorage.readCurrentKey();
144 if (currentKey != null) {
145 boolean keysMatch = Arrays.equals(hmacCalculator.getCurrentKey().getEncoded(), currentKey.getEncoded());
146 logger.trace("keysMatch={}, lastRowWasSigned={}", keysMatch, lastRowWasSigned);
147 if (!keysMatch) {
148 return newVerificationFailureResult("Final HMAC key doesn't match expected value");
149 } else if (!lastRowWasSigned) {
150 return newVerificationFailureResult("Missing final signature");
151 } else {
152 return newVerificationSuccessResult();
153 }
154 } else {
155 logger.trace("currentKey is null");
156 return newVerificationFailureResult("Final HMAC key is null");
157 }
158 } catch (SecureStorageException ex) {
159 throw new IOException(ex);
160 }
161 }
162
163 private CsvMapReader newBufferedCsvMapReader() throws FileNotFoundException {
164 return new CsvMapReader(new BufferedReader(new FileReader(csvFile)), csvPreference);
165 }
166
167 private VerificationResult newVerificationFailureResult(String msg) {
168 return new VerificationResult(csvFile, false, msg);
169 }
170
171 private VerificationResult newVerificationSuccessResult() {
172 return new VerificationResult(csvFile, true, "");
173 }
174
175 private boolean verifyHMAC(Map<String, String> values, String[] header) throws IOException {
176 try {
177 String actualHMAC = values.get(HEADER_HMAC);
178 String expectedHMAC = hmacCalculator.calculate(dataToSign(logger, values, dropExtraHeaders(header)));
179 if (!actualHMAC.equals(expectedHMAC)) {
180 logger.trace("The HMAC is not valid. Expected : {} Found : {}", expectedHMAC, actualHMAC);
181 return false;
182 } else {
183 lastHMAC = actualHMAC;
184 return true;
185 }
186 } catch (SignatureException ex) {
187 logger.error(ex.getMessage(), ex);
188 throw new IOException(ex);
189 }
190 }
191
192 private boolean verifySignature(final String encodedSign) throws IOException {
193 try {
194 byte[] signature = Base64.decode(encodedSign);
195 boolean verify = secureStorage.verify(dataToSign(lastSignature, lastHMAC), signature);
196 if (!verify) {
197 logger.trace("The signature does not match the expecting one.");
198 return false;
199 } else {
200 lastSignature = signature;
201 return true;
202 }
203 } catch (SecureStorageException ex) {
204 logger.error(ex.getMessage(), ex);
205 throw new IOException(ex);
206 }
207 }
208
209 private String[] dropExtraHeaders(String... header) {
210
211 return Arrays.copyOf(header, header.length - 2);
212 }
213
214
215
216
217
218
219 public String[] getHeaders() {
220 return headers;
221 }
222
223
224
225
226
227
228 public String getLastHMAC() {
229 return lastHMAC;
230 }
231
232
233
234
235
236
237 public byte[] getLastSignature() {
238 return lastSignature;
239 }
240
241 static final class VerificationResult {
242
243 private final File archiveFile;
244 private final boolean passedVerification;
245 private final String failureReason;
246
247 VerificationResult(final File archiveFile, final boolean passedVerification, final String message) {
248 this.archiveFile = archiveFile;
249 this.passedVerification = passedVerification;
250 this.failureReason = message;
251 }
252
253 public File getArchiveFile() {
254 return archiveFile;
255 }
256
257 public boolean hasPassedVerification() {
258 return passedVerification;
259 }
260
261 public String getFailureReason() {
262 return failureReason;
263 }
264 }
265 }