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

pgpainless / sop-java / #17

15 Nov 2023 01:23PM UTC coverage: 65.932% (-3.9%) from 69.789%
#17

push

other

vanitasvitae
Fix gradle version

1167 of 1770 relevant lines covered (65.93%)

0.66 hits per line

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

67.59
/external-sop/src/main/java/sop/external/ExternalSOP.java
1
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
2
//
3
// SPDX-License-Identifier: Apache-2.0
4

5
package sop.external;
6

7
import sop.Ready;
8
import sop.SOP;
9
import sop.exception.SOPGPException;
10
import sop.external.operation.ArmorExternal;
11
import sop.external.operation.ChangeKeyPasswordExternal;
12
import sop.external.operation.DearmorExternal;
13
import sop.external.operation.DecryptExternal;
14
import sop.external.operation.DetachedSignExternal;
15
import sop.external.operation.DetachedVerifyExternal;
16
import sop.external.operation.EncryptExternal;
17
import sop.external.operation.ExtractCertExternal;
18
import sop.external.operation.GenerateKeyExternal;
19
import sop.external.operation.InlineDetachExternal;
20
import sop.external.operation.InlineSignExternal;
21
import sop.external.operation.InlineVerifyExternal;
22
import sop.external.operation.ListProfilesExternal;
23
import sop.external.operation.RevokeKeyExternal;
24
import sop.external.operation.VersionExternal;
25
import sop.operation.Armor;
26
import sop.operation.ChangeKeyPassword;
27
import sop.operation.Dearmor;
28
import sop.operation.Decrypt;
29
import sop.operation.DetachedSign;
30
import sop.operation.DetachedVerify;
31
import sop.operation.Encrypt;
32
import sop.operation.ExtractCert;
33
import sop.operation.GenerateKey;
34
import sop.operation.InlineDetach;
35
import sop.operation.InlineSign;
36
import sop.operation.InlineVerify;
37
import sop.operation.ListProfiles;
38
import sop.operation.RevokeKey;
39
import sop.operation.Version;
40

41
import javax.annotation.Nonnull;
42
import java.io.ByteArrayOutputStream;
43
import java.io.File;
44
import java.io.IOException;
45
import java.io.InputStream;
46
import java.io.OutputStream;
47
import java.nio.file.Files;
48
import java.nio.file.attribute.FileAttribute;
49
import java.util.ArrayList;
50
import java.util.List;
51
import java.util.Properties;
52

53
/**
54
 * Implementation of the {@link SOP} API using an external SOP binary.
55
 */
56
public class ExternalSOP implements SOP {
57

58
    private final String binaryName;
59
    private final Properties properties;
60
    private final TempDirProvider tempDirProvider;
61

62
    /**
63
     * Instantiate an {@link ExternalSOP} object for the given binary and pass it empty environment variables,
64
     * as well as a default {@link TempDirProvider}.
65
     *
66
     * @param binaryName name / path of the SOP binary
67
     */
68
    public ExternalSOP(@Nonnull String binaryName) {
69
        this(binaryName, new Properties());
×
70
    }
×
71

72
    /**
73
     * Instantiate an {@link ExternalSOP} object for the given binary, and pass it the given properties as
74
     * environment variables, as well as a default {@link TempDirProvider}.
75
     *
76
     * @param binaryName name / path of the SOP binary
77
     * @param properties environment variables
78
     */
79
    public ExternalSOP(@Nonnull String binaryName, @Nonnull Properties properties) {
80
        this(binaryName, properties, defaultTempDirProvider());
1✔
81
    }
1✔
82

83
    /**
84
     * Instantiate an {@link ExternalSOP} object for the given binary and the given {@link TempDirProvider}
85
     * using empty environment variables.
86
     *
87
     * @param binaryName name / path of the SOP binary
88
     * @param tempDirProvider custom tempDirProvider
89
     */
90
    public ExternalSOP(@Nonnull String binaryName, @Nonnull TempDirProvider tempDirProvider) {
91
        this(binaryName, new Properties(), tempDirProvider);
×
92
    }
×
93

94
    /**
95
     * Instantiate an {@link ExternalSOP} object for the given binary using the given properties and
96
     * custom {@link TempDirProvider}.
97
     *
98
     * @param binaryName name / path of the SOP binary
99
     * @param properties environment variables
100
     * @param tempDirProvider tempDirProvider
101
     */
102
    public ExternalSOP(@Nonnull String binaryName, @Nonnull Properties properties, @Nonnull TempDirProvider tempDirProvider) {
1✔
103
        this.binaryName = binaryName;
1✔
104
        this.properties = properties;
1✔
105
        this.tempDirProvider = tempDirProvider;
1✔
106
    }
1✔
107

108
    @Override
109
    @Nonnull
110
    public Version version() {
111
        return new VersionExternal(binaryName, properties);
1✔
112
    }
113

114
    @Override
115
    @Nonnull
116
    public GenerateKey generateKey() {
117
        return new GenerateKeyExternal(binaryName, properties);
1✔
118
    }
119

120
    @Override
121
    @Nonnull
122
    public ExtractCert extractCert() {
123
        return new ExtractCertExternal(binaryName, properties);
1✔
124
    }
125

126
    @Override
127
    @Nonnull
128
    public DetachedSign detachedSign() {
129
        return new DetachedSignExternal(binaryName, properties, tempDirProvider);
1✔
130
    }
131

132
    @Override
133
    @Nonnull
134
    public InlineSign inlineSign() {
135
        return new InlineSignExternal(binaryName, properties);
1✔
136
    }
137

138
    @Override
139
    @Nonnull
140
    public DetachedVerify detachedVerify() {
141
        return new DetachedVerifyExternal(binaryName, properties);
1✔
142
    }
143

144
    @Override
145
    @Nonnull
146
    public InlineVerify inlineVerify() {
147
        return new InlineVerifyExternal(binaryName, properties, tempDirProvider);
1✔
148
    }
149

150
    @Override
151
    @Nonnull
152
    public InlineDetach inlineDetach() {
153
        return new InlineDetachExternal(binaryName, properties, tempDirProvider);
×
154
    }
155

156
    @Override
157
    @Nonnull
158
    public Encrypt encrypt() {
159
        return new EncryptExternal(binaryName, properties, tempDirProvider);
1✔
160
    }
161

162
    @Override
163
    @Nonnull
164
    public Decrypt decrypt() {
165
        return new DecryptExternal(binaryName, properties, tempDirProvider);
1✔
166
    }
167

168
    @Override
169
    @Nonnull
170
    public Armor armor() {
171
        return new ArmorExternal(binaryName, properties);
1✔
172
    }
173

174
    @Override
175
    @Nonnull
176
    public ListProfiles listProfiles() {
177
        return new ListProfilesExternal(binaryName, properties);
1✔
178
    }
179

180
    @Override
181
    @Nonnull
182
    public RevokeKey revokeKey() {
183
        return new RevokeKeyExternal(binaryName, properties);
1✔
184
    }
185

186
    @Override
187
    @Nonnull
188
    public ChangeKeyPassword changeKeyPassword() {
189
        return new ChangeKeyPasswordExternal(binaryName, properties);
×
190
    }
191

192
    @Override
193
    @Nonnull
194
    public Dearmor dearmor() {
195
        return new DearmorExternal(binaryName, properties);
1✔
196
    }
197

198
    public static void finish(@Nonnull Process process) throws IOException {
199
        try {
200
            mapExitCodeOrException(process);
1✔
201
        } catch (InterruptedException e) {
×
202
            throw new RuntimeException(e);
×
203
        }
1✔
204
    }
1✔
205

206
    /**
207
     * Wait for the {@link Process} to finish and read out its exit code.
208
     * If the exit code is {@value "0"}, this method just returns.
209
     * Otherwise, the exit code gets mapped to a {@link SOPGPException} which then gets thrown.
210
     * If the exit code does not match any of the known exit codes defined in the SOP specification,
211
     * this method throws a {@link RuntimeException} instead.
212
     *
213
     * @param process process
214
     * @throws InterruptedException if the thread is interrupted before the process could exit
215
     * @throws IOException in case of an IO error
216
     */
217
    private static void mapExitCodeOrException(@Nonnull Process process) throws InterruptedException, IOException {
218
        // wait for process termination
219
        int exitCode = process.waitFor();
1✔
220

221
        if (exitCode == 0) {
1✔
222
            // we're good, bye
223
            return;
1✔
224
        }
225

226
        // Read error message
227
        InputStream errIn = process.getErrorStream();
1✔
228
        String errorMessage = readString(errIn);
1✔
229

230
        switch (exitCode) {
1✔
231
            case SOPGPException.NoSignature.EXIT_CODE:
232
                throw new SOPGPException.NoSignature("External SOP backend reported error NoSignature (" +
×
233
                        exitCode + "):\n" + errorMessage);
234

235
            case SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE:
236
                throw new UnsupportedOperationException("External SOP backend reported error UnsupportedAsymmetricAlgo (" +
×
237
                        exitCode + "):\n" + errorMessage);
238

239
            case SOPGPException.CertCannotEncrypt.EXIT_CODE:
240
                throw new SOPGPException.CertCannotEncrypt("External SOP backend reported error CertCannotEncrypt (" +
×
241
                        exitCode + "):\n" + errorMessage);
242

243
            case SOPGPException.MissingArg.EXIT_CODE:
244
                throw new SOPGPException.MissingArg("External SOP backend reported error MissingArg (" +
×
245
                        exitCode + "):\n" + errorMessage);
246

247
            case SOPGPException.IncompleteVerification.EXIT_CODE:
248
                throw new SOPGPException.IncompleteVerification("External SOP backend reported error IncompleteVerification (" +
×
249
                        exitCode + "):\n" + errorMessage);
250

251
            case SOPGPException.CannotDecrypt.EXIT_CODE:
252
                throw new SOPGPException.CannotDecrypt("External SOP backend reported error CannotDecrypt (" +
×
253
                        exitCode + "):\n" + errorMessage);
254

255
            case SOPGPException.PasswordNotHumanReadable.EXIT_CODE:
256
                throw new SOPGPException.PasswordNotHumanReadable("External SOP backend reported error PasswordNotHumanReadable (" +
×
257
                        exitCode + "):\n" + errorMessage);
258

259
            case SOPGPException.UnsupportedOption.EXIT_CODE:
260
                throw new SOPGPException.UnsupportedOption("External SOP backend reported error UnsupportedOption (" +
×
261
                        exitCode + "):\n" + errorMessage);
262

263
            case SOPGPException.BadData.EXIT_CODE:
264
                throw new SOPGPException.BadData("External SOP backend reported error BadData (" +
×
265
                        exitCode + "):\n" + errorMessage);
266

267
            case SOPGPException.ExpectedText.EXIT_CODE:
268
                throw new SOPGPException.ExpectedText("External SOP backend reported error ExpectedText (" +
×
269
                        exitCode + "):\n" + errorMessage);
270

271
            case SOPGPException.OutputExists.EXIT_CODE:
272
                throw new SOPGPException.OutputExists("External SOP backend reported error OutputExists (" +
×
273
                        exitCode + "):\n" + errorMessage);
274

275
            case SOPGPException.MissingInput.EXIT_CODE:
276
                throw new SOPGPException.MissingInput("External SOP backend reported error MissingInput (" +
×
277
                        exitCode + "):\n" + errorMessage);
278

279
            case SOPGPException.KeyIsProtected.EXIT_CODE:
280
                throw new SOPGPException.KeyIsProtected("External SOP backend reported error KeyIsProtected (" +
×
281
                        exitCode + "):\n" + errorMessage);
282

283
            case SOPGPException.UnsupportedSubcommand.EXIT_CODE:
284
                throw new SOPGPException.UnsupportedSubcommand("External SOP backend reported error UnsupportedSubcommand (" +
1✔
285
                        exitCode + "):\n" + errorMessage);
286

287
            case SOPGPException.UnsupportedSpecialPrefix.EXIT_CODE:
288
                throw new SOPGPException.UnsupportedSpecialPrefix("External SOP backend reported error UnsupportedSpecialPrefix (" +
×
289
                        exitCode + "):\n" + errorMessage);
290

291
            case SOPGPException.AmbiguousInput.EXIT_CODE:
292
                throw new SOPGPException.AmbiguousInput("External SOP backend reported error AmbiguousInput (" +
×
293
                        exitCode + "):\n" + errorMessage);
294

295
            case SOPGPException.KeyCannotSign.EXIT_CODE:
296
                throw new SOPGPException.KeyCannotSign("External SOP backend reported error KeyCannotSign (" +
×
297
                        exitCode + "):\n" + errorMessage);
298

299
            case SOPGPException.IncompatibleOptions.EXIT_CODE:
300
                throw new SOPGPException.IncompatibleOptions("External SOP backend reported error IncompatibleOptions (" +
×
301
                        exitCode + "):\n" + errorMessage);
302

303
            case SOPGPException.UnsupportedProfile.EXIT_CODE:
304
                throw new SOPGPException.UnsupportedProfile("External SOP backend reported error UnsupportedProfile (" +
×
305
                        exitCode + "):\n" + errorMessage);
306

307
            default:
308
                // Did you forget to add a case for a new exception type?
309
                throw new RuntimeException("External SOP backend reported unknown exit code (" +
1✔
310
                        exitCode + "):\n" + errorMessage);
311
        }
312
    }
313

314
    /**
315
     * Return all key-value pairs from the given {@link Properties} object as a list with items of the form
316
     * <pre>key=value</pre>.
317
     *
318
     * @param properties properties
319
     * @return list of key=value strings
320
     */
321
    public static List<String> propertiesToEnv(@Nonnull Properties properties) {
322
        List<String> env = new ArrayList<>();
1✔
323
        for (Object key : properties.keySet()) {
1✔
324
            env.add(key + "=" + properties.get(key));
×
325
        }
×
326
        return env;
1✔
327
    }
328

329
    /**
330
     * Read the contents of the {@link InputStream} and return them as a {@link String}.
331
     *
332
     * @param inputStream input stream
333
     * @return string
334
     * @throws IOException in case of an IO error
335
     */
336
    public static String readString(@Nonnull InputStream inputStream) throws IOException {
337
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
1✔
338
        byte[] buf = new byte[4096];
1✔
339
        int r;
340
        while ((r = inputStream.read(buf)) > 0) {
1✔
341
            bOut.write(buf, 0, r);
1✔
342
        }
343
        return bOut.toString();
1✔
344
    }
345

346
    /**
347
     * Execute the given command on the given {@link Runtime} with the given list of environment variables.
348
     * This command does not transform any input data, and instead is purely a producer.
349
     *
350
     * @param runtime runtime
351
     * @param commandList command
352
     * @param envList environment variables
353
     * @return ready to read the result from
354
     */
355
    public static Ready executeProducingOperation(@Nonnull Runtime runtime,
356
                                                  @Nonnull List<String> commandList,
357
                                                  @Nonnull List<String> envList) {
358
        String[] command = commandList.toArray(new String[0]);
1✔
359
        String[] env = envList.toArray(new String[0]);
1✔
360

361
        try {
362
            Process process = runtime.exec(command, env);
1✔
363
            InputStream stdIn = process.getInputStream();
1✔
364

365
            return new Ready() {
1✔
366
                @Override
367
                public void writeTo(@Nonnull OutputStream outputStream) throws IOException {
368
                    byte[] buf = new byte[4096];
1✔
369
                    int r;
370
                    while ((r = stdIn.read(buf)) >= 0) {
1✔
371
                        outputStream.write(buf, 0, r);
1✔
372
                    }
373

374
                    outputStream.flush();
1✔
375
                    outputStream.close();
1✔
376

377
                    ExternalSOP.finish(process);
1✔
378
                }
1✔
379
            };
380
        } catch (IOException e) {
×
381
            throw new RuntimeException(e);
×
382
        }
383
    }
384

385
    /**
386
     * Execute the given command on the given runtime using the given environment variables.
387
     * The given input stream provides input for the process.
388
     * This command is a transformation, meaning it is given input data and transforms it into output data.
389
     *
390
     * @param runtime runtime
391
     * @param commandList command
392
     * @param envList environment variables
393
     * @param standardIn stream of input data for the process
394
     * @return ready to read the result from
395
     */
396
    public static Ready executeTransformingOperation(@Nonnull Runtime runtime, @Nonnull List<String> commandList, @Nonnull List<String> envList, @Nonnull InputStream standardIn) {
397
        String[] command = commandList.toArray(new String[0]);
1✔
398
        String[] env = envList.toArray(new String[0]);
1✔
399
        try {
400
            Process process = runtime.exec(command, env);
1✔
401
            OutputStream processOut = process.getOutputStream();
1✔
402
            InputStream processIn = process.getInputStream();
1✔
403

404
            return new Ready() {
1✔
405
                @Override
406
                public void writeTo(@Nonnull OutputStream outputStream) throws IOException {
407
                    byte[] buf = new byte[4096];
1✔
408
                    int r;
409
                    while ((r = standardIn.read(buf)) > 0) {
1✔
410
                        processOut.write(buf, 0, r);
1✔
411
                    }
412
                    standardIn.close();
1✔
413

414
                    try {
415
                        processOut.flush();
1✔
416
                        processOut.close();
1✔
417
                    } catch (IOException e) {
×
418
                        // Perhaps the stream is already closed, in which case we ignore the exception.
419
                        if (!"Stream closed".equals(e.getMessage())) {
×
420
                            throw e;
×
421
                        }
422
                    }
1✔
423

424
                    while ((r = processIn.read(buf)) > 0) {
1✔
425
                        outputStream.write(buf, 0 , r);
1✔
426
                    }
427
                    processIn.close();
1✔
428

429
                    outputStream.flush();
1✔
430
                    outputStream.close();
1✔
431

432
                    finish(process);
1✔
433
                }
1✔
434
            };
435
        } catch (IOException e) {
×
436
            throw new RuntimeException(e);
×
437
        }
438
    }
439

440
    /**
441
     * This interface can be used to provide a directory in which external SOP binaries can temporarily store
442
     * additional results of OpenPGP operations such that the binding classes can parse them out from there.
443
     * Unfortunately, on Java you cannot open {@link java.io.FileDescriptor FileDescriptors} arbitrarily, so we
444
     * have to rely on temporary files to pass results.
445
     * An example:
446
     * <pre>sop decrypt</pre> can emit signature verifications via <pre>--verify-out=/path/to/tempfile</pre>.
447
     * {@link DecryptExternal} will then parse the temp file to make the result available to consumers.
448
     * Temporary files are deleted after being read, yet creating temp files for sensitive information on disk
449
     * might pose a security risk. Use with care!
450
     */
451
    public interface TempDirProvider {
452
        File provideTempDirectory() throws IOException;
453
    }
454

455
    /**
456
     * Default implementation of the {@link TempDirProvider} which stores temporary files in the systems temp dir
457
     * ({@link Files#createTempDirectory(String, FileAttribute[])}).
458
     *
459
     * @return default implementation
460
     */
461
    public static TempDirProvider defaultTempDirProvider() {
462
        return new TempDirProvider() {
1✔
463
            @Override
464
            public File provideTempDirectory() throws IOException {
465
                return Files.createTempDirectory("ext-sop").toFile();
1✔
466
            }
467
        };
468
    }
469
}
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