View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.eclipse.aether.generator.gnupg.loaders;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.io.BufferedReader;
25  import java.io.IOException;
26  import java.io.InputStreamReader;
27  import java.io.OutputStream;
28  import java.net.SocketException;
29  import java.net.StandardProtocolFamily;
30  import java.net.UnixDomainSocketAddress;
31  import java.nio.channels.Channels;
32  import java.nio.channels.SocketChannel;
33  import java.nio.file.Path;
34  import java.nio.file.Paths;
35  import java.util.Arrays;
36  import java.util.List;
37  import java.util.Locale;
38  import java.util.stream.Collectors;
39  
40  import org.bouncycastle.util.encoders.Hex;
41  import org.eclipse.aether.ConfigurationProperties;
42  import org.eclipse.aether.RepositorySystemSession;
43  import org.eclipse.aether.generator.gnupg.GnupgSignatureArtifactGeneratorFactory;
44  import org.eclipse.aether.util.ConfigUtils;
45  import org.eclipse.sisu.Priority;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  
49  import static org.eclipse.aether.generator.gnupg.GnupgConfigurationKeys.CONFIG_PROP_AGENT_SOCKET_LOCATIONS;
50  import static org.eclipse.aether.generator.gnupg.GnupgConfigurationKeys.CONFIG_PROP_USE_AGENT;
51  import static org.eclipse.aether.generator.gnupg.GnupgConfigurationKeys.DEFAULT_AGENT_SOCKET_LOCATIONS;
52  import static org.eclipse.aether.generator.gnupg.GnupgConfigurationKeys.DEFAULT_USE_AGENT;
53  
54  /**
55   * Password loader that uses GnuPG Agent. Is interactive.
56   */
57  @Singleton
58  @Named(GpgAgentPasswordLoader.NAME)
59  @Priority(10)
60  @SuppressWarnings("checkstyle:magicnumber")
61  public final class GpgAgentPasswordLoader implements GnupgSignatureArtifactGeneratorFactory.Loader {
62      public static final String NAME = "agent";
63      private final Logger logger = LoggerFactory.getLogger(getClass());
64  
65      @Override
66      public char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
67          if (!ConfigUtils.getBoolean(session, DEFAULT_USE_AGENT, CONFIG_PROP_USE_AGENT)) {
68              return null;
69          }
70          String socketLocationsStr =
71                  ConfigUtils.getString(session, DEFAULT_AGENT_SOCKET_LOCATIONS, CONFIG_PROP_AGENT_SOCKET_LOCATIONS);
72          boolean interactive = ConfigUtils.getBoolean(
73                  session, ConfigurationProperties.DEFAULT_INTERACTIVE, ConfigurationProperties.INTERACTIVE);
74          List<String> socketLocations = Arrays.stream(socketLocationsStr.split(","))
75                  .filter(s -> s != null && !s.isEmpty())
76                  .collect(Collectors.toList());
77          for (String socketLocation : socketLocations) {
78              try {
79                  Path socketLocationPath = Paths.get(socketLocation);
80                  if (!socketLocationPath.isAbsolute()) {
81                      socketLocationPath = Paths.get(System.getProperty("user.home"))
82                              .resolve(socketLocationPath)
83                              .toAbsolutePath();
84                  }
85                  return load(fingerprint, socketLocationPath, interactive);
86              } catch (SocketException e) {
87                  // try next location
88                  logger.debug("Problem communicating with agent on socket: {}", socketLocation, e);
89              }
90          }
91          logger.warn("Could not connect to agent on any of the configured sockets: {}", socketLocations);
92          return null;
93      }
94  
95      private char[] load(byte[] fingerprint, Path socketPath, boolean interactive) throws IOException {
96          try (SocketChannel sock = SocketChannel.open(StandardProtocolFamily.UNIX)) {
97              sock.connect(UnixDomainSocketAddress.of(socketPath));
98              try (BufferedReader in = new BufferedReader(new InputStreamReader(Channels.newInputStream(sock)));
99                      OutputStream os = Channels.newOutputStream(sock)) {
100 
101                 expectOK(in);
102                 String display = System.getenv("DISPLAY");
103                 if (display != null) {
104                     os.write(("OPTION display=" + display + "\n").getBytes());
105                     os.flush();
106                     expectOK(in);
107                 }
108                 String term = System.getenv("TERM");
109                 if (term != null) {
110                     os.write(("OPTION ttytype=" + term + "\n").getBytes());
111                     os.flush();
112                     expectOK(in);
113                 }
114                 String hexKeyFingerprint = Hex.toHexString(fingerprint);
115                 String displayFingerprint = hexKeyFingerprint.toUpperCase(Locale.ROOT);
116                 // https://unix.stackexchange.com/questions/71135/how-can-i-find-out-what-keys-gpg-agent-has-cached-like-how-ssh-add-l-shows-yo
117                 String instruction = "GET_PASSPHRASE "
118                         + (!interactive ? "--no-ask " : "")
119                         + hexKeyFingerprint
120                         + " "
121                         + "X "
122                         + "GnuPG+Passphrase "
123                         + "Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key+with+fingerprint:+"
124                         + displayFingerprint
125                         + "+to+use+it+for+signing+Maven+Artifacts\n";
126                 os.write((instruction).getBytes());
127                 os.flush();
128                 return mayExpectOK(in);
129             }
130         }
131     }
132 
133     private void expectOK(BufferedReader in) throws IOException {
134         String response = in.readLine();
135         if (!response.startsWith("OK")) {
136             throw new IOException("Expected OK but got this instead: " + response);
137         }
138     }
139 
140     private char[] mayExpectOK(BufferedReader in) throws IOException {
141         String response = in.readLine();
142         if (response.startsWith("ERR")) {
143             return null;
144         } else if (!response.startsWith("OK")) {
145             throw new IOException("Expected OK/ERR but got this instead: " + response);
146         }
147         return new String(Hex.decode(
148                         response.substring(Math.min(response.length(), 3)).trim()))
149                 .toCharArray();
150     }
151 }