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}