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.codehaus.plexus.components.secdispatcher.internal.sources;
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.StandardProtocolFamily;
29  import java.net.UnixDomainSocketAddress;
30  import java.nio.channels.Channels;
31  import java.nio.channels.SocketChannel;
32  import java.nio.file.Files;
33  import java.nio.file.Path;
34  import java.nio.file.Paths;
35  import java.util.ArrayList;
36  import java.util.HashMap;
37  import java.util.HexFormat;
38  import java.util.List;
39  import java.util.Optional;
40  
41  import org.codehaus.plexus.components.secdispatcher.MasterSourceMeta;
42  import org.codehaus.plexus.components.secdispatcher.SecDispatcher;
43  import org.codehaus.plexus.components.secdispatcher.SecDispatcherException;
44  
45  /**
46   * Password source that uses GnuPG Agent.
47   * <p>
48   * Config: {@code gpg-agent:$agentSocketPath[?non-interactive]}
49   */
50  @Singleton
51  @Named(GpgAgentMasterSource.NAME)
52  public final class GpgAgentMasterSource extends PrefixMasterSourceSupport implements MasterSourceMeta {
53      public static final String NAME = "gpg-agent";
54  
55      public GpgAgentMasterSource() {
56          super(NAME + ":");
57      }
58  
59      @Override
60      public String description() {
61          return "GPG Agent (agent socket path should be edited)";
62      }
63  
64      @Override
65      public Optional<String> configTemplate() {
66          return Optional.of(NAME + ":$agentSocketPath");
67      }
68  
69      @Override
70      protected String doHandle(String transformed) throws SecDispatcherException {
71          String extra = "";
72          if (transformed.contains("?")) {
73              extra = transformed.substring(transformed.indexOf("?"));
74              transformed = transformed.substring(0, transformed.indexOf("?"));
75          }
76          String socketLocation = transformed;
77          boolean interactive = !extra.contains("non-interactive");
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(socketLocationPath, interactive);
86          } catch (IOException e) {
87              throw new SecDispatcherException(e.getMessage(), e);
88          }
89      }
90  
91      @Override
92      protected SecDispatcher.ValidationResponse doValidateConfiguration(String transformed) {
93          HashMap<SecDispatcher.ValidationResponse.Level, List<String>> report = new HashMap<>();
94          boolean valid = false;
95  
96          String extra = "";
97          if (transformed.contains("?")) {
98              extra = transformed.substring(transformed.indexOf("?"));
99              transformed = transformed.substring(0, transformed.indexOf("?"));
100         }
101         Path socketLocation = Paths.get(transformed);
102         if (!socketLocation.isAbsolute()) {
103             socketLocation = Paths.get(System.getProperty("user.home"))
104                     .resolve(socketLocation)
105                     .toAbsolutePath();
106         }
107         if (Files.exists(socketLocation)) {
108             report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.ERROR, k -> new ArrayList<>())
109                     .add("Unix domain socket for GPG Agent does not exist. Maybe you need to start gpg-agent?");
110         } else {
111             report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.INFO, k -> new ArrayList<>())
112                     .add("Unix domain socket for GPG Agent exist");
113             valid = true;
114         }
115         boolean interactive = !extra.contains("non-interactive");
116         if (!interactive) {
117             report.computeIfAbsent(SecDispatcher.ValidationResponse.Level.WARNING, k -> new ArrayList<>())
118                     .add(
119                             "Non-interactive flag found, gpg-agent will not ask for passphrase, it can use only cached ones");
120         }
121         return new SecDispatcher.ValidationResponse(getClass().getSimpleName(), valid, report, List.of());
122     }
123 
124     private String load(Path socketPath, boolean interactive) throws IOException {
125         try (SocketChannel sock = SocketChannel.open(StandardProtocolFamily.UNIX)) {
126             sock.connect(UnixDomainSocketAddress.of(socketPath));
127             try (BufferedReader in = new BufferedReader(new InputStreamReader(Channels.newInputStream(sock)));
128                     OutputStream os = Channels.newOutputStream(sock)) {
129 
130                 expectOK(in);
131                 String display = System.getenv("DISPLAY");
132                 if (display != null) {
133                     os.write(("OPTION display=" + display + "\n").getBytes());
134                     os.flush();
135                     expectOK(in);
136                 }
137                 String term = System.getenv("TERM");
138                 if (term != null) {
139                     os.write(("OPTION ttytype=" + term + "\n").getBytes());
140                     os.flush();
141                     expectOK(in);
142                 }
143                 // https://unix.stackexchange.com/questions/71135/how-can-i-find-out-what-keys-gpg-agent-has-cached-like-how-ssh-add-l-shows-yo
144                 String instruction = "GET_PASSPHRASE "
145                         + (!interactive ? "--no-ask " : "")
146                         + "plexus:secDispatcherMasterPassword"
147                         + " "
148                         + "X "
149                         + "Maven+Master+Password "
150                         + "Please+enter+your+Maven+master+password"
151                         + "+to+use+it+for+decrypting+Maven+Settings\n";
152                 os.write((instruction).getBytes());
153                 os.flush();
154                 return mayExpectOK(in);
155             }
156         }
157     }
158 
159     private void expectOK(BufferedReader in) throws IOException {
160         String response = in.readLine();
161         if (!response.startsWith("OK")) {
162             throw new IOException("Expected OK but got this instead: " + response);
163         }
164     }
165 
166     private String mayExpectOK(BufferedReader in) throws IOException {
167         String response = in.readLine();
168         if (response.startsWith("ERR")) {
169             return null;
170         } else if (!response.startsWith("OK")) {
171             throw new IOException("Expected OK/ERR but got this instead: " + response);
172         }
173         return new String(HexFormat.of()
174                 .parseHex(response.substring(Math.min(response.length(), 3)).trim()));
175     }
176 }