1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugins.gpg;
20
21 import java.io.BufferedReader;
22 import java.io.ByteArrayInputStream;
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.InputStreamReader;
27 import java.io.OutputStream;
28 import java.net.SocketException;
29 import java.nio.charset.StandardCharsets;
30 import java.nio.file.Files;
31 import java.nio.file.Path;
32 import java.nio.file.Paths;
33 import java.time.LocalDateTime;
34 import java.time.ZoneId;
35 import java.util.Arrays;
36 import java.util.Iterator;
37 import java.util.List;
38 import java.util.Locale;
39 import java.util.stream.Collectors;
40 import java.util.stream.Stream;
41
42 import org.apache.maven.plugin.MojoExecutionException;
43 import org.apache.maven.plugin.MojoFailureException;
44 import org.bouncycastle.bcpg.ArmoredOutputStream;
45 import org.bouncycastle.bcpg.BCPGOutputStream;
46 import org.bouncycastle.bcpg.HashAlgorithmTags;
47 import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
48 import org.bouncycastle.openpgp.PGPException;
49 import org.bouncycastle.openpgp.PGPPrivateKey;
50 import org.bouncycastle.openpgp.PGPSecretKey;
51 import org.bouncycastle.openpgp.PGPSecretKeyRing;
52 import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
53 import org.bouncycastle.openpgp.PGPSignature;
54 import org.bouncycastle.openpgp.PGPSignatureGenerator;
55 import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
56 import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
57 import org.bouncycastle.openpgp.PGPUtil;
58 import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator;
59 import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
60 import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
61 import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
62 import org.bouncycastle.util.encoders.Hex;
63 import org.codehaus.plexus.util.io.CachingOutputStream;
64 import org.eclipse.aether.RepositorySystemSession;
65 import org.newsclub.net.unix.AFUNIXSocket;
66 import org.newsclub.net.unix.AFUNIXSocketAddress;
67
68
69
70
71 @SuppressWarnings("checkstyle:magicnumber")
72 public class BcSigner extends AbstractGpgSigner {
73 public static final String NAME = "bc";
74
75 public interface Loader {
76
77
78
79 default byte[] loadKeyRingMaterial(RepositorySystemSession session) throws IOException {
80 return null;
81 }
82
83
84
85
86 default byte[] loadKeyFingerprint(RepositorySystemSession session) throws IOException {
87 return null;
88 }
89
90
91
92
93 default char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
94 return null;
95 }
96 }
97
98 public final class GpgEnvLoader implements Loader {
99 @Override
100 public byte[] loadKeyRingMaterial(RepositorySystemSession session) {
101 String keyMaterial = (String) session.getConfigProperties().get("env." + keyEnvName);
102 if (keyMaterial != null) {
103 return keyMaterial.getBytes(StandardCharsets.UTF_8);
104 }
105 return null;
106 }
107
108 @Override
109 public byte[] loadKeyFingerprint(RepositorySystemSession session) {
110 String keyFingerprint = (String) session.getConfigProperties().get("env." + keyFingerprintEnvName);
111 if (keyFingerprint != null) {
112 if (keyFingerprint.trim().length() == 40) {
113 return Hex.decode(keyFingerprint);
114 } else {
115 throw new IllegalArgumentException(
116 "Key fingerprint configuration is wrong (hex encoded, 40 characters)");
117 }
118 }
119 return null;
120 }
121 }
122
123 public final class GpgConfLoader implements Loader {
124
125
126
127
128
129
130
131
132 private static final long MAX_SIZE = 64 * 1000 + 1L;
133
134 @Override
135 public byte[] loadKeyRingMaterial(RepositorySystemSession session) throws IOException {
136 Path keyPath = Paths.get(keyFilePath);
137 if (!keyPath.isAbsolute()) {
138 keyPath = Paths.get(System.getProperty("user.home"))
139 .resolve(keyPath)
140 .toAbsolutePath();
141 }
142 if (Files.isRegularFile(keyPath)) {
143 if (Files.size(keyPath) < MAX_SIZE) {
144 return Files.readAllBytes(keyPath);
145 } else {
146 throw new IOException("Refusing to load file " + keyPath + "; is larger than 64 kB");
147 }
148 }
149 return null;
150 }
151
152 @Override
153 public byte[] loadKeyFingerprint(RepositorySystemSession session) {
154 if (keyFingerprint != null) {
155 if (keyFingerprint.trim().length() == 40) {
156 return Hex.decode(keyFingerprint);
157 } else {
158 throw new IllegalArgumentException(
159 "Key fingerprint configuration is wrong (hex encoded, 40 characters)");
160 }
161 }
162 return null;
163 }
164 }
165
166 public final class GpgAgentPasswordLoader implements Loader {
167 @Override
168 public char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
169 if (!useAgent) {
170 return null;
171 }
172 List<String> socketLocations = Arrays.stream(agentSocketLocations.split(","))
173 .filter(s -> s != null && !s.isEmpty())
174 .collect(Collectors.toList());
175 for (String socketLocation : socketLocations) {
176 try {
177 Path socketLocationPath = Paths.get(socketLocation);
178 if (!socketLocationPath.isAbsolute()) {
179 socketLocationPath = Paths.get(System.getProperty("user.home"))
180 .resolve(socketLocationPath)
181 .toAbsolutePath();
182 }
183 return load(fingerprint, socketLocationPath);
184 } catch (SocketException e) {
185
186 }
187 }
188 return null;
189 }
190
191 private char[] load(byte[] fingerprint, Path socketPath) throws IOException {
192 try (AFUNIXSocket sock = AFUNIXSocket.newInstance()) {
193 sock.connect(AFUNIXSocketAddress.of(socketPath));
194 try (BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
195 OutputStream os = sock.getOutputStream()) {
196
197 expectOK(in);
198 String display = System.getenv("DISPLAY");
199 if (display != null) {
200 os.write(("OPTION display=" + display + "\n").getBytes());
201 os.flush();
202 expectOK(in);
203 }
204 String term = System.getenv("TERM");
205 if (term != null) {
206 os.write(("OPTION ttytype=" + term + "\n").getBytes());
207 os.flush();
208 expectOK(in);
209 }
210 String hexKeyFingerprint = Hex.toHexString(fingerprint);
211 String displayFingerprint = hexKeyFingerprint.toUpperCase(Locale.ROOT);
212
213 String instruction = "GET_PASSPHRASE "
214 + (!isInteractive ? "--no-ask " : "")
215 + hexKeyFingerprint
216 + " "
217 + "X "
218 + "GnuPG+Passphrase "
219 + "Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key+with+fingerprint:+"
220 + displayFingerprint
221 + "+to+use+it+for+signing+Maven+Artifacts\n";
222 os.write((instruction).getBytes());
223 os.flush();
224 return mayExpectOK(in);
225 }
226 }
227 }
228
229 private void expectOK(BufferedReader in) throws IOException {
230 String response = in.readLine();
231 if (!response.startsWith("OK")) {
232 throw new IOException("Expected OK but got this instead: " + response);
233 }
234 }
235
236 private char[] mayExpectOK(BufferedReader in) throws IOException {
237 String response = in.readLine();
238 if (response.startsWith("ERR")) {
239 return null;
240 } else if (!response.startsWith("OK")) {
241 throw new IOException("Expected OK/ERR but got this instead: " + response);
242 }
243 return new String(Hex.decode(
244 response.substring(Math.min(response.length(), 3)).trim()))
245 .toCharArray();
246 }
247 }
248
249 private final RepositorySystemSession session;
250 private final String keyEnvName;
251 private final String keyFingerprintEnvName;
252 private final String agentSocketLocations;
253 private final String keyFilePath;
254 private final String keyFingerprint;
255 private PGPSecretKey secretKey;
256 private PGPPrivateKey privateKey;
257 private PGPSignatureSubpacketVector hashSubPackets;
258
259 public BcSigner(
260 RepositorySystemSession session,
261 String keyEnvName,
262 String keyFingerprintEnvName,
263 String agentSocketLocations,
264 String keyFilePath,
265 String keyFingerprint) {
266 this.session = session;
267 this.keyEnvName = keyEnvName;
268 this.keyFingerprintEnvName = keyFingerprintEnvName;
269 this.agentSocketLocations = agentSocketLocations;
270 this.keyFilePath = keyFilePath;
271 this.keyFingerprint = keyFingerprint;
272 }
273
274 @Override
275 public String signerName() {
276 return NAME;
277 }
278
279 @Override
280 public void prepare() throws MojoFailureException {
281 try {
282 List<Loader> loaders = Stream.of(new GpgEnvLoader(), new GpgConfLoader(), new GpgAgentPasswordLoader())
283 .collect(Collectors.toList());
284
285 byte[] keyRingMaterial = null;
286 for (Loader loader : loaders) {
287 keyRingMaterial = loader.loadKeyRingMaterial(session);
288 if (keyRingMaterial != null) {
289 break;
290 }
291 }
292 if (keyRingMaterial == null) {
293 throw new MojoFailureException("Key ring material not found");
294 }
295
296 byte[] fingerprint = null;
297 for (Loader loader : loaders) {
298 fingerprint = loader.loadKeyFingerprint(session);
299 if (fingerprint != null) {
300 break;
301 }
302 }
303
304 PGPSecretKeyRingCollection pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(
305 PGPUtil.getDecoderStream(new ByteArrayInputStream(keyRingMaterial)),
306 new BcKeyFingerprintCalculator());
307
308 PGPSecretKey secretKey = null;
309 for (PGPSecretKeyRing ring : pgpSecretKeyRingCollection) {
310 for (PGPSecretKey key : ring) {
311 if (!key.isPrivateKeyEmpty()) {
312 if (fingerprint == null || Arrays.equals(fingerprint, key.getFingerprint())) {
313 secretKey = key;
314 break;
315 }
316 }
317 }
318 }
319 if (secretKey == null) {
320 throw new MojoFailureException("Secret key not found");
321 }
322 if (secretKey.isPrivateKeyEmpty()) {
323 throw new MojoFailureException("Private key not found in Secret key");
324 }
325
326 long validSeconds = secretKey.getPublicKey().getValidSeconds();
327 if (validSeconds > 0) {
328 LocalDateTime expireDateTime = secretKey
329 .getPublicKey()
330 .getCreationTime()
331 .toInstant()
332 .atZone(ZoneId.systemDefault())
333 .toLocalDateTime()
334 .plusSeconds(validSeconds);
335 if (LocalDateTime.now().isAfter(expireDateTime)) {
336 throw new MojoFailureException("Secret key expired at: " + expireDateTime);
337 }
338 }
339
340 char[] keyPassword = passphrase != null ? passphrase.toCharArray() : null;
341 final boolean keyPassNeeded = secretKey.getKeyEncryptionAlgorithm() != SymmetricKeyAlgorithmTags.NULL;
342 if (keyPassNeeded && keyPassword == null) {
343 for (Loader loader : loaders) {
344 keyPassword = loader.loadPassword(session, secretKey.getFingerprint());
345 if (keyPassword != null) {
346 break;
347 }
348 }
349 if (keyPassword == null) {
350 throw new MojoFailureException("Secret key is encrypted but no passphrase provided");
351 }
352 }
353
354 this.secretKey = secretKey;
355 this.privateKey = secretKey.extractPrivateKey(
356 new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()).build(keyPassword));
357 if (keyPassword != null) {
358 Arrays.fill(keyPassword, ' ');
359 }
360 PGPSignatureSubpacketGenerator subPacketGenerator = new PGPSignatureSubpacketGenerator();
361 subPacketGenerator.setIssuerFingerprint(false, secretKey);
362 this.hashSubPackets = subPacketGenerator.generate();
363 } catch (PGPException | IOException e) {
364 throw new MojoFailureException(e);
365 }
366 }
367
368 @Override
369 public String getKeyInfo() {
370 Iterator<String> userIds = secretKey.getPublicKey().getUserIDs();
371 if (userIds.hasNext()) {
372 return userIds.next();
373 }
374 return Hex.toHexString(secretKey.getPublicKey().getFingerprint());
375 }
376
377 @Override
378 protected void generateSignatureForFile(File file, File signature) throws MojoExecutionException {
379 try (InputStream in = Files.newInputStream(file.toPath());
380 OutputStream out = new CachingOutputStream(signature.toPath())) {
381 PGPSignatureGenerator sGen = new PGPSignatureGenerator(
382 new BcPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA512));
383 sGen.init(PGPSignature.BINARY_DOCUMENT, privateKey);
384 sGen.setHashedSubpackets(hashSubPackets);
385 int len;
386 byte[] buffer = new byte[8 * 1024];
387 while ((len = in.read(buffer)) >= 0) {
388 sGen.update(buffer, 0, len);
389 }
390 try (BCPGOutputStream bcpgOutputStream = new BCPGOutputStream(new ArmoredOutputStream(out))) {
391 sGen.generate().encode(bcpgOutputStream);
392 }
393 } catch (PGPException | IOException e) {
394 throw new MojoExecutionException(e);
395 }
396 }
397 }