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

pgpainless / cert-d-java / #5

29 Sep 2025 11:40AM UTC coverage: 83.865% (-1.2%) from 85.102%
#5

push

other

vanitasvitae
Update changelog

421 of 502 relevant lines covered (83.86%)

0.84 hits per line

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

81.07
/pgp-cert-d-java/src/main/java/pgp/cert_d/backend/FileBasedCertificateDirectoryBackend.java
1
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
2
//
3
// SPDX-License-Identifier: Apache-2.0
4

5
package pgp.cert_d.backend;
6

7
import pgp.cert_d.PGPCertificateDirectory;
8
import pgp.cert_d.SpecialNames;
9
import pgp.certificate_store.certificate.Certificate;
10
import pgp.certificate_store.certificate.Key;
11
import pgp.certificate_store.certificate.KeyMaterial;
12
import pgp.certificate_store.certificate.KeyMaterialMerger;
13
import pgp.certificate_store.certificate.KeyMaterialReaderBackend;
14
import pgp.certificate_store.exception.BadDataException;
15
import pgp.certificate_store.exception.BadNameException;
16
import pgp.certificate_store.exception.NotAStoreException;
17

18
import java.io.BufferedInputStream;
19
import java.io.File;
20
import java.io.FileFilter;
21
import java.io.FileInputStream;
22
import java.io.FileNotFoundException;
23
import java.io.FileOutputStream;
24
import java.io.IOException;
25
import java.io.InputStream;
26
import java.io.RandomAccessFile;
27
import java.nio.channels.FileLock;
28
import java.nio.channels.OverlappingFileLockException;
29
import java.nio.file.Files;
30
import java.nio.file.Path;
31
import java.nio.file.attribute.BasicFileAttributes;
32
import java.util.ArrayList;
33
import java.util.Collections;
34
import java.util.Iterator;
35
import java.util.List;
36
import java.util.NoSuchElementException;
37
import java.util.regex.Pattern;
38

39
/**
40
 * Implementation of {@link PGPCertificateDirectory.Backend} which stores certificates in a directory structure.
41
 *
42
 * @see <a href="https://sequoia-pgp.gitlab.io/pgp-cert-d/#name-implementation">Shared PGP Certificate Directory</a>
43
 */
44
public class FileBasedCertificateDirectoryBackend implements PGPCertificateDirectory.Backend {
45

46
    private abstract static class Lazy<E> {
47
        abstract E get() throws BadDataException;
48
    }
49

50
    /**
51
     * Locking mechanism which uses a lock file to synchronize write-access to the store.
52
     */
53
    private static class FileLockingMechanism implements PGPCertificateDirectory.LockingMechanism {
54

55
        private final File lockFile;
56
        private RandomAccessFile randomAccessFile;
57
        private FileLock fileLock;
58

59
        FileLockingMechanism(File lockFile) {
1✔
60
            this.lockFile = lockFile;
1✔
61
        }
1✔
62

63
        public static FileLockingMechanism defaultDirectoryFileLock(File baseDirectory) {
64
            return new FileLockingMechanism(new File(baseDirectory, "writelock"));
1✔
65
        }
66

67
        @Override
68
        public synchronized void lockDirectory() throws IOException, InterruptedException {
69
            if (randomAccessFile != null) {
1✔
70
                // we own the lock already. Let's wait...
71
                this.wait();
×
72
            }
73

74
            try {
75
                randomAccessFile = new RandomAccessFile(lockFile, "rw");
1✔
76
            } catch (FileNotFoundException e) {
×
77
                lockFile.createNewFile();
×
78
                randomAccessFile = new RandomAccessFile(lockFile, "rw");
×
79
            }
1✔
80

81
            fileLock = randomAccessFile.getChannel().lock();
1✔
82
        }
1✔
83

84
        @Override
85
        public synchronized boolean tryLockDirectory() throws IOException {
86
            if (randomAccessFile != null) {
1✔
87
                // We already locked the directory for another write operation.
88
                // We fail, since we have not yet released the lock from the other operation.
89
                return false;
1✔
90
            }
91

92
            try {
93
                randomAccessFile = new RandomAccessFile(lockFile, "rw");
1✔
94
            } catch (FileNotFoundException e) {
×
95
                lockFile.createNewFile();
×
96
                randomAccessFile = new RandomAccessFile(lockFile, "rw");
×
97
            }
1✔
98

99
            try {
100
                fileLock = randomAccessFile.getChannel().tryLock();
1✔
101
                if (fileLock == null) {
1✔
102
                    // try-lock failed, file is locked by another process.
103
                    randomAccessFile.close();
×
104
                    randomAccessFile = null;
×
105
                    return false;
×
106
                }
107
            } catch (OverlappingFileLockException e) {
×
108
                // Some other object is holding the lock.
109
                randomAccessFile.close();
×
110
                randomAccessFile = null;
×
111
                return false;
×
112
            }
1✔
113
            return true;
1✔
114
        }
115

116
        @Override
117
        public boolean isLocked() {
118
            return randomAccessFile != null;
1✔
119
        }
120

121
        @Override
122
        public synchronized void releaseDirectory() throws IOException {
123
            // unlock file
124
            if (fileLock != null) {
1✔
125
                fileLock.release();
1✔
126
                fileLock = null;
1✔
127
            }
128
            // close file
129
            if (randomAccessFile != null) {
1✔
130
                randomAccessFile.close();
1✔
131
                randomAccessFile = null;
1✔
132
            }
133
            // delete file
134
            if (lockFile.exists()) {
1✔
135
                lockFile.delete();
1✔
136
            }
137
            // notify waiters
138
            this.notify();
1✔
139
        }
1✔
140
    }
141

142
    private final File baseDirectory;
143
    private final PGPCertificateDirectory.LockingMechanism lock;
144
    private final FilenameResolver resolver;
145
    private final KeyMaterialReaderBackend reader;
146

147
    public FileBasedCertificateDirectoryBackend(File baseDirectory, KeyMaterialReaderBackend reader) throws NotAStoreException {
1✔
148
        this.baseDirectory = baseDirectory;
1✔
149
        this.resolver = new FilenameResolver(baseDirectory);
1✔
150

151
        if (!baseDirectory.exists()) {
1✔
152
            if (!baseDirectory.mkdirs()) {
×
153
                throw new NotAStoreException("Cannot create base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "'");
×
154
            }
155
        } else {
156
            if (baseDirectory.isFile()) {
1✔
157
                throw new NotAStoreException("Base directory '" + resolver.getBaseDirectory().getAbsolutePath() + "' appears to be a file.");
1✔
158
            }
159
        }
160
        this.lock = FileLockingMechanism.defaultDirectoryFileLock(baseDirectory);
1✔
161
        this.reader = reader;
1✔
162
    }
1✔
163

164
    @Override
165
    public PGPCertificateDirectory.LockingMechanism getLock() {
166
        return lock;
1✔
167
    }
168

169
    @Override
170
    public Certificate readByFingerprint(String fingerprint) throws BadNameException, IOException, BadDataException {
171
        File certFile = resolver.getCertFileByFingerprint(fingerprint);
1✔
172
        if (!certFile.exists()) {
1✔
173
            return null;
1✔
174
        }
175

176
        long tag = getTagForFingerprint(fingerprint);
1✔
177

178
        FileInputStream fileIn = new FileInputStream(certFile);
1✔
179
        BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
1✔
180

181
        Certificate certificate = reader.read(bufferedIn, tag).asCertificate();
1✔
182
        if (!certificate.getFingerprint().equals(fingerprint)) {
1✔
183
            // TODO: Figure out more suitable exception
184
            throw new BadDataException("Identified certificate fingerprint does not match queried fingerprint:\n" +
1✔
185
                    "found: " + certificate.getFingerprint() + "\n" +
1✔
186
                    "query: " + fingerprint);
187
        }
188

189
        return certificate;
1✔
190
    }
191

192
    @Override
193
    public KeyMaterial readBySpecialName(String specialName) throws BadNameException, IOException, BadDataException {
194
        File certFile = resolver.getCertFileBySpecialName(specialName);
1✔
195
        if (!certFile.exists()) {
1✔
196
            return null;
1✔
197
        }
198

199
        long tag = getTagForSpecialName(specialName);
1✔
200

201
        FileInputStream fileIn = new FileInputStream(certFile);
1✔
202
        BufferedInputStream bufferedIn = new BufferedInputStream(fileIn);
1✔
203
        KeyMaterial keyMaterial = reader.read(bufferedIn, tag);
1✔
204

205
        return keyMaterial;
1✔
206
    }
207

208
    @Override
209
    public Iterator<Certificate> readItems() {
210
        return new Iterator<Certificate>() {
1✔
211

212
            private final List<Lazy<Certificate>> certificateQueue = Collections.synchronizedList(new ArrayList<>());
1✔
213

214
            // Constructor... wtf.
215
            {
216
                File[] subdirectories = baseDirectory.listFiles(new FileFilter() {
1✔
217
                    @Override
218
                    public boolean accept(File file) {
219
                        return file.isDirectory() && file.getName().matches("^[a-f0-9]{2}$");
1✔
220
                    }
221
                });
222

223
                if (subdirectories == null) {
1✔
224
                    subdirectories = new File[0];
×
225
                }
226

227
                for (File subdirectory : subdirectories) {
1✔
228
                    File[] files = subdirectory.listFiles(new FileFilter() {
1✔
229
                        @Override
230
                        public boolean accept(File file) {
231
                            return file.isFile() && file.getName().matches("^[a-f0-9]{38}$");
1✔
232
                        }
233
                    });
234

235
                    if (files == null) {
1✔
236
                        files = new File[0];
×
237
                    }
238

239
                    for (File certFile : files) {
1✔
240
                        certificateQueue.add(new Lazy<Certificate>() {
1✔
241
                            @Override
242
                            Certificate get() throws BadDataException {
243
                                try {
244
                                    long tag = getTag(certFile);
1✔
245
                                    Certificate certificate = reader.read(new FileInputStream(certFile), tag).asCertificate();
1✔
246
                                    if (!(subdirectory.getName() + certFile.getName()).equals(certificate.getFingerprint())) {
1✔
247
                                        throw new BadDataException("Certificate fingerprint does not match file location+name.\n" +
×
248
                                                "Fingerprint: " + certificate.getFingerprint() + "\n" +
×
249
                                                "Location+name: " + subdirectory.getName() + certFile.getName());
×
250
                                    }
251
                                    return certificate;
1✔
252
                                } catch (IOException e) {
×
253
                                    throw new AssertionError("File got deleted.");
×
254
                                }
255
                            }
256
                        });
257
                    }
258
                }
259
            }
1✔
260

261
            @Override
262
            public boolean hasNext() {
263
                return !certificateQueue.isEmpty();
1✔
264
            }
265

266
            @Override
267
            public Certificate next() {
268
                try {
269
                    return certificateQueue.remove(0).get();
1✔
270
                } catch (BadDataException e) {
×
271
                    throw new AssertionError("Could not retrieve item: " + e.getMessage());
×
272
                }
273
            }
274
        };
275
    }
276

277
    @Override
278
    public KeyMaterial doInsertTrustRoot(InputStream data, KeyMaterialMerger merge) throws BadDataException, IOException {
279
        KeyMaterial newCertificate = reader.read(data, null);
1✔
280
        KeyMaterial existingCertificate;
281
        File certFile;
282
        try {
283
            existingCertificate = readBySpecialName(SpecialNames.TRUST_ROOT);
1✔
284
            certFile = resolver.getCertFileBySpecialName(SpecialNames.TRUST_ROOT);
1✔
285
        } catch (BadNameException e) {
×
286
            throw new BadDataException("Unknown special name '" + SpecialNames.TRUST_ROOT + "'");
×
287
        }
1✔
288

289
        if (existingCertificate != null) {
1✔
290
            newCertificate = merge.merge(newCertificate, existingCertificate);
1✔
291
        }
292

293
        long tag = writeToFile(newCertificate.getInputStream(), certFile);
1✔
294
        if (newCertificate instanceof Key) {
1✔
295
            newCertificate = new Key((Key) newCertificate, tag);
1✔
296
        } else {
297
            newCertificate = new Certificate((Certificate) newCertificate, tag);
1✔
298
        }
299
        return newCertificate;
1✔
300
    }
301

302
    @Override
303
    public Certificate doInsert(InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException {
304
        KeyMaterial newCertificate = reader.read(data, null);
1✔
305
        Certificate existingCertificate;
306
        File certFile;
307
        try {
308
            existingCertificate = readByFingerprint(newCertificate.getFingerprint());
1✔
309
            certFile = resolver.getCertFileByFingerprint(newCertificate.getFingerprint());
1✔
310
        } catch (BadNameException e) {
×
311
            throw new BadDataException("Malformed key fingerprint: " + newCertificate.getFingerprint());
×
312
        }
1✔
313

314
        if (existingCertificate != null) {
1✔
315
            newCertificate = merge.merge(newCertificate, existingCertificate);
1✔
316
        }
317

318
        long tag = writeToFile(newCertificate.getInputStream(), certFile);
1✔
319
        return new Certificate(newCertificate.asCertificate(), tag);
1✔
320
    }
321

322
    @Override
323
    public Certificate doInsertWithSpecialName(String specialName, InputStream data, KeyMaterialMerger merge) throws IOException, BadDataException, BadNameException {
324
        KeyMaterial newCertificate = reader.read(data, null);
1✔
325
        KeyMaterial existingCertificate;
326
        File certFile;
327
        try {
328
            existingCertificate = readBySpecialName(specialName);
1✔
329
            certFile = resolver.getCertFileBySpecialName(specialName);
1✔
330
        } catch (BadNameException e) {
×
331
            throw new BadDataException("Unknown special name '" + specialName + "'");
×
332
        }
1✔
333

334
        if (existingCertificate != null) {
1✔
335
            newCertificate = merge.merge(newCertificate, existingCertificate);
1✔
336
        }
337

338
        long tag = writeToFile(newCertificate.getInputStream(), certFile);
1✔
339
        return new Certificate(newCertificate.asCertificate(), tag);
1✔
340
    }
341

342
    @Override
343
    public Long getTagForFingerprint(String fingerprint) throws BadNameException, IOException {
344
        File file = resolver.getCertFileByFingerprint(fingerprint);
1✔
345
        return getTag(file);
1✔
346
    }
347

348
    @Override
349
    public Long getTagForSpecialName(String specialName) throws BadNameException, IOException {
350
        File file = resolver.getCertFileBySpecialName(specialName);
1✔
351
        return getTag(file);
1✔
352
    }
353

354
    private Long getTag(File file) throws IOException {
355
        if (!file.exists()) {
1✔
356
            throw new NoSuchElementException("File '" + file.getAbsolutePath() + "' does not exist.");
1✔
357
        }
358
        Path path = file.toPath();
1✔
359
        BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
1✔
360

361
        // On UNIX file systems, for example, fileKey() will return the device ID and inode
362
        int fileId = attrs.fileKey().hashCode();
1✔
363
        long lastMod = attrs.lastModifiedTime().toMillis();
1✔
364

365
        return lastMod + (11L * fileId);
1✔
366
    }
367

368
    private long writeToFile(InputStream inputStream, File certFile)
369
            throws IOException {
370
        certFile.getParentFile().mkdirs();
1✔
371
        if (!certFile.exists() && !certFile.createNewFile()) {
1✔
372
            throw new IOException("Could not create cert file " + certFile.getAbsolutePath());
×
373
        }
374

375
        FileOutputStream fileOut = new FileOutputStream(certFile);
1✔
376

377
        byte[] buffer = new byte[4096];
1✔
378
        int read;
379
        while ((read = inputStream.read(buffer)) != -1) {
1✔
380
            fileOut.write(buffer, 0, read);
1✔
381
        }
382

383
        inputStream.close();
1✔
384
        fileOut.close();
1✔
385
        return getTag(certFile);
1✔
386
    }
387

388
    /**
389
     * Class to resolve file names from certificate fingerprints / special names.
390
     */
391
    public static class FilenameResolver {
392

393
        private final File baseDirectory;
394
        // matches v4 and v5 fingerprints (v4 = 40 hex chars, v5 = 64 hex chars)
395
        private final Pattern openPgpFingerprint = Pattern.compile("^[a-f0-9]{40}([a-f0-9]{24})?$");
1✔
396

397
        public FilenameResolver(File baseDirectory) {
1✔
398
            this.baseDirectory = baseDirectory;
1✔
399
        }
1✔
400

401
        public File getBaseDirectory() {
402
            return baseDirectory;
1✔
403
        }
404

405
        /**
406
         * Calculate the file location for the certificate addressed by the given
407
         * lowercase hexadecimal OpenPGP fingerprint.
408
         *
409
         * @param fingerprint fingerprint
410
         * @return absolute certificate file location
411
         *
412
         * @throws BadNameException if the given fingerprint string is not a fingerprint
413
         */
414
        public File getCertFileByFingerprint(String fingerprint) throws BadNameException {
415
            if (!isFingerprint(fingerprint)) {
1✔
416
                throw new BadNameException("Malformed query fingerprint '" + fingerprint + "'");
1✔
417
            }
418

419
            // is fingerprint
420
            File subdirectory = new File(getBaseDirectory(), fingerprint.substring(0, 2));
1✔
421
            File file = new File(subdirectory, fingerprint.substring(2));
1✔
422
            return file;
1✔
423
        }
424

425
        /**
426
         * Calculate the file location for the certification addressed using the given special name.
427
         * For known special names, see {@link SpecialNames}.
428
         *
429
         * @param specialName special name (e.g. "trust-root")
430
         * @return absolute certificate file location
431
         *
432
         * @throws BadNameException in case the given special name is not known
433
         */
434
        public File getCertFileBySpecialName(String specialName)
435
                throws BadNameException {
436
            if (!isSpecialName(specialName)) {
1✔
437
                throw new BadNameException(String.format("%s is not a known special name", specialName));
1✔
438
            }
439

440
            return new File(getBaseDirectory(), specialName);
1✔
441
        }
442

443
        private boolean isFingerprint(String fingerprint) {
444
            return openPgpFingerprint.matcher(fingerprint).matches();
1✔
445
        }
446

447
        private boolean isSpecialName(String specialName) {
448
            return SpecialNames.lookupSpecialName(specialName) != null;
1✔
449
        }
450

451
    }
452
}
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

© 2025 Coveralls, Inc