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}