001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.generator.gnupg.loaders;
020
021import javax.inject.Named;
022import javax.inject.Singleton;
023
024import java.io.BufferedReader;
025import java.io.IOException;
026import java.io.InputStreamReader;
027import java.io.OutputStream;
028import java.net.SocketException;
029import java.net.StandardProtocolFamily;
030import java.net.UnixDomainSocketAddress;
031import java.nio.channels.Channels;
032import java.nio.channels.SocketChannel;
033import java.nio.file.Path;
034import java.nio.file.Paths;
035import java.util.Arrays;
036import java.util.List;
037import java.util.Locale;
038import java.util.stream.Collectors;
039
040import org.bouncycastle.util.encoders.Hex;
041import org.eclipse.aether.ConfigurationProperties;
042import org.eclipse.aether.RepositorySystemSession;
043import org.eclipse.aether.generator.gnupg.GnupgSignatureArtifactGeneratorFactory;
044import org.eclipse.aether.util.ConfigUtils;
045import org.eclipse.sisu.Priority;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048
049import static org.eclipse.aether.generator.gnupg.GnupgConfigurationKeys.CONFIG_PROP_AGENT_SOCKET_LOCATIONS;
050import static org.eclipse.aether.generator.gnupg.GnupgConfigurationKeys.CONFIG_PROP_USE_AGENT;
051import static org.eclipse.aether.generator.gnupg.GnupgConfigurationKeys.DEFAULT_AGENT_SOCKET_LOCATIONS;
052import static org.eclipse.aether.generator.gnupg.GnupgConfigurationKeys.DEFAULT_USE_AGENT;
053
054/**
055 * Password loader that uses GnuPG Agent. Is interactive.
056 */
057@Singleton
058@Named(GpgAgentPasswordLoader.NAME)
059@Priority(10)
060public final class GpgAgentPasswordLoader implements GnupgSignatureArtifactGeneratorFactory.Loader {
061    public static final String NAME = "agent";
062    private final Logger logger = LoggerFactory.getLogger(getClass());
063
064    @Override
065    public char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
066        if (!ConfigUtils.getBoolean(session, DEFAULT_USE_AGENT, CONFIG_PROP_USE_AGENT)) {
067            return null;
068        }
069        String socketLocationsStr =
070                ConfigUtils.getString(session, DEFAULT_AGENT_SOCKET_LOCATIONS, CONFIG_PROP_AGENT_SOCKET_LOCATIONS);
071        boolean interactive = ConfigUtils.getBoolean(
072                session, ConfigurationProperties.DEFAULT_INTERACTIVE, ConfigurationProperties.INTERACTIVE);
073        List<String> socketLocations = Arrays.stream(socketLocationsStr.split(","))
074                .filter(s -> s != null && !s.isEmpty())
075                .collect(Collectors.toList());
076        for (String socketLocation : socketLocations) {
077            try {
078                Path socketLocationPath = Paths.get(socketLocation);
079                if (!socketLocationPath.isAbsolute()) {
080                    socketLocationPath = Paths.get(System.getProperty("user.home"))
081                            .resolve(socketLocationPath)
082                            .toAbsolutePath();
083                }
084                return load(fingerprint, socketLocationPath, interactive);
085            } catch (SocketException e) {
086                // try next location
087                logger.debug("Problem communicating with agent on socket: {}", socketLocation, e);
088            }
089        }
090        logger.warn("Could not connect to agent on any of the configured sockets: {}", socketLocations);
091        return null;
092    }
093
094    private char[] load(byte[] fingerprint, Path socketPath, boolean interactive) throws IOException {
095        try (SocketChannel sock = SocketChannel.open(StandardProtocolFamily.UNIX)) {
096            sock.connect(UnixDomainSocketAddress.of(socketPath));
097            try (BufferedReader in = new BufferedReader(new InputStreamReader(Channels.newInputStream(sock)));
098                    OutputStream os = Channels.newOutputStream(sock)) {
099
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}