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