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  public final class GpgAgentPasswordLoader implements GnupgSignatureArtifactGeneratorFactory.Loader {
61      public static final String NAME = "agent";
62      private final Logger logger = LoggerFactory.getLogger(getClass());
63  
64      @Override
65      public char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
66          if (!ConfigUtils.getBoolean(session, DEFAULT_USE_AGENT, CONFIG_PROP_USE_AGENT)) {
67              return null;
68          }
69          String socketLocationsStr =
70                  ConfigUtils.getString(session, DEFAULT_AGENT_SOCKET_LOCATIONS, CONFIG_PROP_AGENT_SOCKET_LOCATIONS);
71          boolean interactive = ConfigUtils.getBoolean(
72                  session, ConfigurationProperties.DEFAULT_INTERACTIVE, ConfigurationProperties.INTERACTIVE);
73          List<String> socketLocations = Arrays.stream(socketLocationsStr.split(","))
74                  .filter(s -> s != null && !s.isEmpty())
75                  .collect(Collectors.toList());
76          for (String socketLocation : socketLocations) {
77              try {
78                  Path socketLocationPath = Paths.get(socketLocation);
79                  if (!socketLocationPath.isAbsolute()) {
80                      socketLocationPath = Paths.get(System.getProperty("user.home"))
81                              .resolve(socketLocationPath)
82                              .toAbsolutePath();
83                  }
84                  return load(fingerprint, socketLocationPath, interactive);
85              } catch (SocketException e) {
86                  // try next location
87                  logger.debug("Problem communicating with agent on socket: {}", socketLocation, e);
88              }
89          }
90          logger.warn("Could not connect to agent on any of the configured sockets: {}", socketLocations);
91          return null;
92      }
93  
94      private char[] load(byte[] fingerprint, Path socketPath, boolean interactive) throws IOException {
95          try (SocketChannel sock = SocketChannel.open(StandardProtocolFamily.UNIX)) {
96              sock.connect(UnixDomainSocketAddress.of(socketPath));
97              try (BufferedReader in = new BufferedReader(new InputStreamReader(Channels.newInputStream(sock)));
98                      OutputStream os = Channels.newOutputStream(sock)) {
99  
100                 expectOK(in);
101                 String display = System.getenv("DISPLAY");
102                 if (display != null) {
103                     os.write(("OPTION display=" + display + "\n").getBytes());
104                     os.flush();
105                     expectOK(in);
106                 }
107                 String term = System.getenv("TERM");
108                 if (term != null) {
109                     os.write(("OPTION ttytype=" + term + "\n").getBytes());
110                     os.flush();
111                     expectOK(in);
112                 }
113                 String hexKeyFingerprint = Hex.toHexString(fingerprint);
114                 String displayFingerprint = hexKeyFingerprint.toUpperCase(Locale.ROOT);
115                 // https://unix.stackexchange.com/questions/71135/how-can-i-find-out-what-keys-gpg-agent-has-cached-like-how-ssh-add-l-shows-yo
116                 String instruction = "GET_PASSPHRASE "
117                         + (!interactive ? "--no-ask " : "")
118                         + hexKeyFingerprint
119                         + " "
120                         + "X "
121                         + "GnuPG+Passphrase "
122                         + "Please+enter+the+passphrase+to+unlock+the+OpenPGP+secret+key+with+fingerprint:+"
123                         + displayFingerprint
124                         + "+to+use+it+for+signing+Maven+Artifacts\n";
125                 os.write((instruction).getBytes());
126                 os.flush();
127                 return mayExpectOK(in);
128             }
129         }
130     }
131 
132     private void expectOK(BufferedReader in) throws IOException {
133         String response = in.readLine();
134         if (!response.startsWith("OK")) {
135             throw new IOException("Expected OK but got this instead: " + response);
136         }
137     }
138 
139     private char[] mayExpectOK(BufferedReader in) throws IOException {
140         String response = in.readLine();
141         if (response.startsWith("ERR")) {
142             return null;
143         } else if (!response.startsWith("OK")) {
144             throw new IOException("Expected OK/ERR but got this instead: " + response);
145         }
146         return new String(Hex.decode(
147                         response.substring(Math.min(response.length(), 3)).trim()))
148                 .toCharArray();
149     }
150 }