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

pgpainless / sop-java / 164

pending completion
164

push

other

vanitasvitae
SOP-Java 7.0.1-SNAPSHOT

1259 of 1804 relevant lines covered (69.79%)

0.7 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
    public Version version() {
110
        return new VersionExternal(binaryName, properties);
1✔
111
    }
112

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

118
    @Override
119
    public ExtractCert extractCert() {
120
        return new ExtractCertExternal(binaryName, properties);
1✔
121
    }
122

123
    @Override
124
    public DetachedSign detachedSign() {
125
        return new DetachedSignExternal(binaryName, properties, tempDirProvider);
1✔
126
    }
127

128
    @Override
129
    public InlineSign inlineSign() {
130
        return new InlineSignExternal(binaryName, properties);
1✔
131
    }
132

133
    @Override
134
    public DetachedVerify detachedVerify() {
135
        return new DetachedVerifyExternal(binaryName, properties);
1✔
136
    }
137

138
    @Override
139
    public InlineVerify inlineVerify() {
140
        return new InlineVerifyExternal(binaryName, properties, tempDirProvider);
1✔
141
    }
142

143
    @Override
144
    public InlineDetach inlineDetach() {
145
        return new InlineDetachExternal(binaryName, properties, tempDirProvider);
×
146
    }
147

148
    @Override
149
    public Encrypt encrypt() {
150
        return new EncryptExternal(binaryName, properties);
1✔
151
    }
152

153
    @Override
154
    public Decrypt decrypt() {
155
        return new DecryptExternal(binaryName, properties, tempDirProvider);
1✔
156
    }
157

158
    @Override
159
    public Armor armor() {
160
        return new ArmorExternal(binaryName, properties);
1✔
161
    }
162

163
    @Override
164
    public ListProfiles listProfiles() {
165
        return new ListProfilesExternal(binaryName, properties);
1✔
166
    }
167

168
    @Override
169
    public RevokeKey revokeKey() {
170
        return new RevokeKeyExternal(binaryName, properties);
1✔
171
    }
172

173
    @Override
174
    public ChangeKeyPassword changeKeyPassword() {
175
        return new ChangeKeyPasswordExternal(binaryName, properties);
×
176
    }
177

178
    @Override
179
    public Dearmor dearmor() {
180
        return new DearmorExternal(binaryName, properties);
1✔
181
    }
182

183
    public static void finish(@Nonnull Process process) throws IOException {
184
        try {
185
            mapExitCodeOrException(process);
1✔
186
        } catch (InterruptedException e) {
×
187
            throw new RuntimeException(e);
×
188
        }
1✔
189
    }
1✔
190

191
    /**
192
     * Wait for the {@link Process} to finish and read out its exit code.
193
     * If the exit code is {@value "0"}, this method just returns.
194
     * Otherwise, the exit code gets mapped to a {@link SOPGPException} which then gets thrown.
195
     * If the exit code does not match any of the known exit codes defined in the SOP specification,
196
     * this method throws a {@link RuntimeException} instead.
197
     *
198
     * @param process process
199
     * @throws InterruptedException if the thread is interrupted before the process could exit
200
     * @throws IOException in case of an IO error
201
     */
202
    private static void mapExitCodeOrException(@Nonnull Process process) throws InterruptedException, IOException {
203
        // wait for process termination
204
        int exitCode = process.waitFor();
1✔
205

206
        if (exitCode == 0) {
1✔
207
            // we're good, bye
208
            return;
1✔
209
        }
210

211
        // Read error message
212
        InputStream errIn = process.getErrorStream();
1✔
213
        String errorMessage = readString(errIn);
1✔
214

215
        switch (exitCode) {
1✔
216
            case SOPGPException.NoSignature.EXIT_CODE:
217
                throw new SOPGPException.NoSignature("External SOP backend reported error NoSignature (" +
×
218
                        exitCode + "):\n" + errorMessage);
219

220
            case SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE:
221
                throw new UnsupportedOperationException("External SOP backend reported error UnsupportedAsymmetricAlgo (" +
×
222
                        exitCode + "):\n" + errorMessage);
223

224
            case SOPGPException.CertCannotEncrypt.EXIT_CODE:
225
                throw new SOPGPException.CertCannotEncrypt("External SOP backend reported error CertCannotEncrypt (" +
×
226
                        exitCode + "):\n" + errorMessage);
227

228
            case SOPGPException.MissingArg.EXIT_CODE:
229
                throw new SOPGPException.MissingArg("External SOP backend reported error MissingArg (" +
×
230
                        exitCode + "):\n" + errorMessage);
231

232
            case SOPGPException.IncompleteVerification.EXIT_CODE:
233
                throw new SOPGPException.IncompleteVerification("External SOP backend reported error IncompleteVerification (" +
×
234
                        exitCode + "):\n" + errorMessage);
235

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

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

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

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

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

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

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

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

268
            case SOPGPException.UnsupportedSubcommand.EXIT_CODE:
269
                throw new SOPGPException.UnsupportedSubcommand("External SOP backend reported error UnsupportedSubcommand (" +
1✔
270
                        exitCode + "):\n" + errorMessage);
271

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

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

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

284
            case SOPGPException.IncompatibleOptions.EXIT_CODE:
285
                throw new SOPGPException.IncompatibleOptions("External SOP backend reported error IncompatibleOptions (" +
×
286
                        exitCode + "):\n" + errorMessage);
287

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

292
            default:
293
                // Did you forget to add a case for a new exception type?
294
                throw new RuntimeException("External SOP backend reported unknown exit code (" +
1✔
295
                        exitCode + "):\n" + errorMessage);
296
        }
297
    }
298

299
    /**
300
     * Return all key-value pairs from the given {@link Properties} object as a list with items of the form
301
     * <pre>key=value</pre>.
302
     *
303
     * @param properties properties
304
     * @return list of key=value strings
305
     */
306
    public static List<String> propertiesToEnv(@Nonnull Properties properties) {
307
        List<String> env = new ArrayList<>();
1✔
308
        for (Object key : properties.keySet()) {
1✔
309
            env.add(key + "=" + properties.get(key));
×
310
        }
×
311
        return env;
1✔
312
    }
313

314
    /**
315
     * Read the contents of the {@link InputStream} and return them as a {@link String}.
316
     *
317
     * @param inputStream input stream
318
     * @return string
319
     * @throws IOException in case of an IO error
320
     */
321
    public static String readString(@Nonnull InputStream inputStream) throws IOException {
322
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
1✔
323
        byte[] buf = new byte[4096];
1✔
324
        int r;
325
        while ((r = inputStream.read(buf)) > 0) {
1✔
326
            bOut.write(buf, 0, r);
1✔
327
        }
328
        return bOut.toString();
1✔
329
    }
330

331
    /**
332
     * Execute the given command on the given {@link Runtime} with the given list of environment variables.
333
     * This command does not transform any input data, and instead is purely a producer.
334
     *
335
     * @param runtime runtime
336
     * @param commandList command
337
     * @param envList environment variables
338
     * @return ready to read the result from
339
     */
340
    public static Ready executeProducingOperation(@Nonnull Runtime runtime,
341
                                                  @Nonnull List<String> commandList,
342
                                                  @Nonnull List<String> envList) {
343
        String[] command = commandList.toArray(new String[0]);
1✔
344
        String[] env = envList.toArray(new String[0]);
1✔
345

346
        try {
347
            Process process = runtime.exec(command, env);
1✔
348
            InputStream stdIn = process.getInputStream();
1✔
349

350
            return new Ready() {
1✔
351
                @Override
352
                public void writeTo(OutputStream outputStream) throws IOException {
353
                    byte[] buf = new byte[4096];
1✔
354
                    int r;
355
                    while ((r = stdIn.read(buf)) >= 0) {
1✔
356
                        outputStream.write(buf, 0, r);
1✔
357
                    }
358

359
                    outputStream.flush();
1✔
360
                    outputStream.close();
1✔
361

362
                    ExternalSOP.finish(process);
1✔
363
                }
1✔
364
            };
365
        } catch (IOException e) {
×
366
            throw new RuntimeException(e);
×
367
        }
368
    }
369

370
    /**
371
     * Execute the given command on the given runtime using the given environment variables.
372
     * The given input stream provides input for the process.
373
     * This command is a transformation, meaning it is given input data and transforms it into output data.
374
     *
375
     * @param runtime runtime
376
     * @param commandList command
377
     * @param envList environment variables
378
     * @param standardIn stream of input data for the process
379
     * @return ready to read the result from
380
     */
381
    public static Ready executeTransformingOperation(@Nonnull Runtime runtime, @Nonnull List<String> commandList, @Nonnull List<String> envList, @Nonnull InputStream standardIn) {
382
        String[] command = commandList.toArray(new String[0]);
1✔
383
        String[] env = envList.toArray(new String[0]);
1✔
384
        try {
385
            Process process = runtime.exec(command, env);
1✔
386
            OutputStream processOut = process.getOutputStream();
1✔
387
            InputStream processIn = process.getInputStream();
1✔
388

389
            return new Ready() {
1✔
390
                @Override
391
                public void writeTo(OutputStream outputStream) throws IOException {
392
                    byte[] buf = new byte[4096];
1✔
393
                    int r;
394
                    while ((r = standardIn.read(buf)) > 0) {
1✔
395
                        processOut.write(buf, 0, r);
1✔
396
                    }
397
                    standardIn.close();
1✔
398

399
                    try {
400
                        processOut.flush();
1✔
401
                        processOut.close();
1✔
402
                    } catch (IOException e) {
×
403
                        // Perhaps the stream is already closed, in which case we ignore the exception.
404
                        if (!"Stream closed".equals(e.getMessage())) {
×
405
                            throw e;
×
406
                        }
407
                    }
1✔
408

409
                    while ((r = processIn.read(buf)) > 0) {
1✔
410
                        outputStream.write(buf, 0 , r);
1✔
411
                    }
412
                    processIn.close();
1✔
413

414
                    outputStream.flush();
1✔
415
                    outputStream.close();
1✔
416

417
                    finish(process);
1✔
418
                }
1✔
419
            };
420
        } catch (IOException e) {
×
421
            throw new RuntimeException(e);
×
422
        }
423
    }
424

425
    /**
426
     * This interface can be used to provide a directory in which external SOP binaries can temporarily store
427
     * additional results of OpenPGP operations such that the binding classes can parse them out from there.
428
     * Unfortunately, on Java you cannot open {@link java.io.FileDescriptor FileDescriptors} arbitrarily, so we
429
     * have to rely on temporary files to pass results.
430
     * An example:
431
     * <pre>sop decrypt</pre> can emit signature verifications via <pre>--verify-out=/path/to/tempfile</pre>.
432
     * {@link DecryptExternal} will then parse the temp file to make the result available to consumers.
433
     * Temporary files are deleted after being read, yet creating temp files for sensitive information on disk
434
     * might pose a security risk. Use with care!
435
     */
436
    public interface TempDirProvider {
437
        File provideTempDirectory() throws IOException;
438
    }
439

440
    /**
441
     * Default implementation of the {@link TempDirProvider} which stores temporary files in the systems temp dir
442
     * ({@link Files#createTempDirectory(String, FileAttribute[])}).
443
     *
444
     * @return default implementation
445
     */
446
    public static TempDirProvider defaultTempDirProvider() {
447
        return new TempDirProvider() {
1✔
448
            @Override
449
            public File provideTempDirectory() throws IOException {
450
                return Files.createTempDirectory("ext-sop").toFile();
1✔
451
            }
452
        };
453
    }
454
}
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