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 private static final long MAX_SIZE = 16 * 1024 + 1L;
128
129 @Override
130 public byte[] loadKeyRingMaterial(RepositorySystemSession session) throws IOException {
131 Path keyPath = Paths.get(keyFilePath);
132 if (!keyPath.isAbsolute()) {
133 keyPath = Paths.get(System.getProperty("user.home"))
134 .resolve(keyPath)
135 .toAbsolutePath();
136 }
137 if (Files.isRegularFile(keyPath)) {
138 if (Files.size(keyPath) < MAX_SIZE) {
139 return Files.readAllBytes(keyPath);
140 } else {
141 throw new IOException("Refusing to load key " + keyPath + "; is larger than 16KB");
142 }
143 }
144 return null;
145 }
146
147 @Override
148 public byte[] loadKeyFingerprint(RepositorySystemSession session) {
149 if (keyFingerprint != null) {
150 if (keyFingerprint.trim().length() == 40) {
151 return Hex.decode(keyFingerprint);
152 } else {
153 throw new IllegalArgumentException(
154 "Key fingerprint configuration is wrong (hex encoded, 40 characters)");
155 }
156 }
157 return null;
158 }
159 }
160
161 public final class GpgAgentPasswordLoader implements Loader {
162 @Override
163 public char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
164 if (!useAgent) {
165 return null;
166 }
167 List<String> socketLocations = Arrays.stream(agentSocketLocations.split(","))
168 .filter(s -> s != null && !s.isEmpty())
169 .collect(Collectors.toList());
170 for (String socketLocation : socketLocations) {
171 try {
172 Path socketLocationPath = Paths.get(socketLocation);
173 if (!socketLocationPath.isAbsolute()) {
174 socketLocationPath = Paths.get(System.getProperty("user.home"))
175 .resolve(socketLocationPath)
176 .toAbsolutePath();
177 }
178 String pw = load(fingerprint, socketLocationPath);
179 if (pw != null) {
180 return pw.toCharArray();
181 }
182 } catch (SocketException e) {
183
184 }
185 }
186 return null;
187 }
188
189 private String load(byte[] fingerprint, Path socketPath) throws IOException {
190 try (AFUNIXSocket sock = AFUNIXSocket.newInstance()) {
191 sock.connect(AFUNIXSocketAddress.of(socketPath));
192 try (BufferedReader in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
193 OutputStream os = sock.getOutputStream()) {
194
195 expectOK(in);
196 String display = System.getenv("DISPLAY");
197 if (display != null) {
198 os.write(("OPTION display=" + display + "\n").getBytes());
199 os.flush();
200 expectOK(in);
201 }
202 String term = System.getenv("TERM");
203 if (term != null) {
204 os.write(("OPTION ttytype=" + term + "\n").getBytes());
205 os.flush();
206 expectOK(in);
207 }
208 String hexKeyFingerprint = Hex.toHexString(fingerprint);
209 String displayFingerprint = hexKeyFingerprint.toUpperCase(Locale.ROOT);
210
211 String instruction = "GET_PASSPHRASE "
212 + (!isInteractive ? "--no-ask " : "")
213 + hexKeyFingerprint
214 + " "
215 + "X "
216 + "GnuPG+Passphrase "
217 + "Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key+with+fingerprint:+"
218 + displayFingerprint
219 + "+to+use+it+for+signing+Maven+Artifacts\n";
220 os.write((instruction).getBytes());
221 os.flush();
222 String pw = mayExpectOK(in);
223 if (pw != null) {
224 return new String(Hex.decode(pw.trim()));
225 }
226 return null;
227 }
228 }
229 }
230
231 private void expectOK(BufferedReader in) throws IOException {
232 String response = in.readLine();
233 if (!response.startsWith("OK")) {
234 throw new IOException("Expected OK but got this instead: " + response);
235 }
236 }
237
238 private String mayExpectOK(BufferedReader in) throws IOException {
239 String response = in.readLine();
240 if (response.startsWith("ERR")) {
241 return null;
242 } else if (!response.startsWith("OK")) {
243 throw new IOException("Expected OK/ERR but got this instead: " + response);
244 }
245 return response.substring(Math.min(response.length(), 3));
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 PGPSignatureSubpacketGenerator subPacketGenerator = new PGPSignatureSubpacketGenerator();
358 subPacketGenerator.setIssuerFingerprint(false, secretKey);
359 this.hashSubPackets = subPacketGenerator.generate();
360 } catch (PGPException | IOException e) {
361 throw new MojoFailureException(e);
362 }
363 }
364
365 @Override
366 public String getKeyInfo() {
367 Iterator<String> userIds = secretKey.getPublicKey().getUserIDs();
368 if (userIds.hasNext()) {
369 return userIds.next();
370 }
371 return Hex.toHexString(secretKey.getPublicKey().getFingerprint());
372 }
373
374 @Override
375 protected void generateSignatureForFile(File file, File signature) throws MojoExecutionException {
376 try (InputStream in = Files.newInputStream(file.toPath());
377 OutputStream out = new CachingOutputStream(signature.toPath())) {
378 PGPSignatureGenerator sGen = new PGPSignatureGenerator(
379 new BcPGPContentSignerBuilder(secretKey.getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA512));
380 sGen.init(PGPSignature.BINARY_DOCUMENT, privateKey);
381 sGen.setHashedSubpackets(hashSubPackets);
382 int len;
383 byte[] buffer = new byte[8 * 1024];
384 while ((len = in.read(buffer)) >= 0) {
385 sGen.update(buffer, 0, len);
386 }
387 try (BCPGOutputStream bcpgOutputStream = new BCPGOutputStream(new ArmoredOutputStream(out))) {
388 sGen.generate().encode(bcpgOutputStream);
389 }
390 } catch (PGPException | IOException e) {
391 throw new MojoExecutionException(e);
392 }
393 }
394 }