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)
060@SuppressWarnings("checkstyle:magicnumber")
061public final class GpgAgentPasswordLoader implements GnupgSignatureArtifactGeneratorFactory.Loader {
062    public static final String NAME = "agent";
063    private final Logger logger = LoggerFactory.getLogger(getClass());
064
065    @Override
066    public char[] loadPassword(RepositorySystemSession session, byte[] fingerprint) throws IOException {
067        if (!ConfigUtils.getBoolean(session, DEFAULT_USE_AGENT, CONFIG_PROP_USE_AGENT)) {
068            return null;
069        }
070        String socketLocationsStr =
071                ConfigUtils.getString(session, DEFAULT_AGENT_SOCKET_LOCATIONS, CONFIG_PROP_AGENT_SOCKET_LOCATIONS);
072        boolean interactive = ConfigUtils.getBoolean(
073                session, ConfigurationProperties.DEFAULT_INTERACTIVE, ConfigurationProperties.INTERACTIVE);
074        List<String> socketLocations = Arrays.stream(socketLocationsStr.split(","))
075                .filter(s -> s != null && !s.isEmpty())
076                .collect(Collectors.toList());
077        for (String socketLocation : socketLocations) {
078            try {
079                Path socketLocationPath = Paths.get(socketLocation);
080                if (!socketLocationPath.isAbsolute()) {
081                    socketLocationPath = Paths.get(System.getProperty("user.home"))
082                            .resolve(socketLocationPath)
083                            .toAbsolutePath();
084                }
085                return load(fingerprint, socketLocationPath, interactive);
086            } catch (SocketException e) {
087                // try next location
088                logger.debug("Problem communicating with agent on socket: {}", socketLocation, e);
089            }
090        }
091        logger.warn("Could not connect to agent on any of the configured sockets: {}", socketLocations);
092        return null;
093    }
094
095    private char[] load(byte[] fingerprint, Path socketPath, boolean interactive) throws IOException {
096        try (SocketChannel sock = SocketChannel.open(StandardProtocolFamily.UNIX)) {
097            sock.connect(UnixDomainSocketAddress.of(socketPath));
098            try (BufferedReader in = new BufferedReader(new InputStreamReader(Channels.newInputStream(sock)));
099                    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}