1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package davmail.http;
20
21 import davmail.Settings;
22 import davmail.ui.CredentialPromptDialog;
23 import org.apache.log4j.Logger;
24 import org.ietf.jgss.*;
25
26 import javax.security.auth.RefreshFailedException;
27 import javax.security.auth.Subject;
28 import javax.security.auth.callback.*;
29 import javax.security.auth.kerberos.KerberosTicket;
30 import javax.security.auth.login.LoginContext;
31 import javax.security.auth.login.LoginException;
32 import java.awt.*;
33 import java.io.BufferedReader;
34 import java.io.IOException;
35 import java.io.InputStreamReader;
36 import java.security.PrivilegedAction;
37 import java.security.Security;
38
39
40
41
42
43 public class KerberosHelper {
44 protected static final Logger LOGGER = Logger.getLogger(KerberosHelper.class);
45 protected static final Object LOCK = new Object();
46 protected static final KerberosCallbackHandler KERBEROS_CALLBACK_HANDLER;
47 private static LoginContext clientLoginContext;
48
49 static {
50
51 Security.setProperty("login.configuration.provider", "davmail.http.KerberosLoginConfiguration");
52
53 KERBEROS_CALLBACK_HANDLER = new KerberosCallbackHandler();
54 }
55
56 private KerberosHelper() {
57 }
58
59 @SuppressWarnings("UseOfSystemOutOrSystemErr")
60 protected static class KerberosCallbackHandler implements CallbackHandler {
61 String principal;
62 String password;
63
64 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
65 for (Callback callback : callbacks) {
66 if (callback instanceof NameCallback) {
67 if (principal == null) {
68
69 if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
70
71 System.out.print(((NameCallback) callback).getPrompt());
72 BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
73 principal = inReader.readLine();
74 } else {
75 CredentialPromptDialog credentialPromptDialog = new CredentialPromptDialog(((NameCallback) callback).getPrompt());
76 principal = credentialPromptDialog.getPrincipal();
77 password = String.valueOf(credentialPromptDialog.getPassword());
78 }
79 }
80 if (principal == null) {
81 throw new IOException("KerberosCallbackHandler: failed to retrieve principal");
82 }
83 ((NameCallback) callback).setName(principal);
84
85 } else if (callback instanceof PasswordCallback) {
86 if (password == null) {
87
88 if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
89
90 System.out.print(((PasswordCallback) callback).getPrompt());
91 BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
92 password = inReader.readLine();
93 }
94 }
95 if (password == null) {
96 throw new IOException("KerberosCallbackHandler: failed to retrieve password");
97 }
98 ((PasswordCallback) callback).setPassword(password.toCharArray());
99
100 } else {
101 throw new UnsupportedCallbackException(callback);
102 }
103 }
104 }
105 }
106
107
108
109
110
111
112 public static void setClientPrincipal(String principal) {
113 KERBEROS_CALLBACK_HANDLER.principal = principal;
114 }
115
116
117
118
119
120
121 public static void setClientPassword(String password) {
122 KERBEROS_CALLBACK_HANDLER.password = password;
123 }
124
125
126
127
128
129
130
131
132
133
134
135 public static byte[] initSecurityContext(final String protocol, final String host, final byte[] token) throws GSSException, LoginException {
136 return initSecurityContext(protocol, host, null, token);
137 }
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152 public static byte[] initSecurityContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) throws GSSException, LoginException {
153 LOGGER.debug("KerberosHelper.initSecurityContext " + protocol + '@' + host + ' ' + token.length + " bytes token");
154
155 synchronized (LOCK) {
156
157 if (clientLoginContext != null) {
158 for (Object ticket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
159 KerberosTicket kerberosTicket = (KerberosTicket) ticket;
160 if (kerberosTicket.getServer().getName().startsWith("krbtgt") && !kerberosTicket.isCurrent()) {
161 LOGGER.debug("KerberosHelper.clientLogin cached TGT expired, try to relogin");
162 clientLoginContext = null;
163 }
164 }
165 }
166
167 if (clientLoginContext == null) {
168 final LoginContext localLoginContext = new LoginContext("spnego-client", KERBEROS_CALLBACK_HANDLER);
169 localLoginContext.login();
170 clientLoginContext = localLoginContext;
171 }
172
173 for (Object ticket : clientLoginContext.getSubject().getPrivateCredentials(KerberosTicket.class)) {
174 KerberosTicket kerberosTicket = (KerberosTicket) ticket;
175 LOGGER.debug("KerberosHelper.clientLogin ticket for " + kerberosTicket.getServer().getName() + " expires at " + kerberosTicket.getEndTime());
176 if (kerberosTicket.getEndTime().getTime() < System.currentTimeMillis() + 10000) {
177 if (kerberosTicket.isRenewable()) {
178 try {
179 kerberosTicket.refresh();
180 } catch (RefreshFailedException e) {
181 LOGGER.debug("KerberosHelper.clientLogin failed to renew ticket " + kerberosTicket);
182 }
183 } else {
184 LOGGER.debug("KerberosHelper.clientLogin ticket is not renewable");
185 }
186 }
187 }
188
189 Object result = internalInitSecContext(protocol, host, delegatedCredentials, token);
190 if (result instanceof GSSException) {
191 LOGGER.info("KerberosHelper.initSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
192 throw (GSSException) result;
193 }
194
195 LOGGER.debug("KerberosHelper.initSecurityContext return " + ((byte[]) result).length + " bytes token");
196 return (byte[]) result;
197 }
198 }
199
200 protected static Object internalInitSecContext(final String protocol, final String host, final GSSCredential delegatedCredentials, final byte[] token) {
201 return Subject.doAs(clientLoginContext.getSubject(), (PrivilegedAction<Object>) () -> {
202 Object result;
203 GSSContext context = null;
204 try {
205 GSSManager manager = GSSManager.getInstance();
206 GSSName serverName = manager.createName(protocol + '@' + host, GSSName.NT_HOSTBASED_SERVICE);
207
208 Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
209
210 context = manager.createContext(serverName, krb5Oid, delegatedCredentials, GSSContext.DEFAULT_LIFETIME);
211
212
213
214 context.requestCredDeleg(true);
215
216 result = context.initSecContext(token, 0, token.length);
217 } catch (GSSException e) {
218 result = e;
219 } finally {
220 if (context != null) {
221 try {
222 context.dispose();
223 } catch (GSSException e) {
224 LOGGER.debug("KerberosHelper.internalInitSecContext " + e + ' ' + e.getMessage());
225 }
226 }
227 }
228 return result;
229 });
230 }
231
232
233
234
235
236
237
238
239
240 public static LoginContext serverLogin(final String serverPrincipal, final String serverPassword) throws LoginException {
241 LoginContext serverLoginContext = new LoginContext("spnego-server", callbacks -> {
242 for (Callback callback : callbacks) {
243 if (callback instanceof NameCallback) {
244 final NameCallback nameCallback = (NameCallback) callback;
245 nameCallback.setName(serverPrincipal);
246 } else if (callback instanceof PasswordCallback) {
247 final PasswordCallback passCallback = (PasswordCallback) callback;
248 passCallback.setPassword(serverPassword.toCharArray());
249 } else {
250 throw new UnsupportedCallbackException(callback);
251 }
252 }
253
254 });
255 serverLoginContext.login();
256 return serverLoginContext;
257 }
258
259
260
261
262 public static class SecurityContext {
263
264
265
266 public byte[] token;
267
268
269
270 public String principal;
271
272
273
274 public GSSCredential clientCredential;
275 }
276
277
278
279
280
281
282
283
284
285 public static SecurityContext acceptSecurityContext(LoginContext serverLoginContext, final byte[] token) throws GSSException {
286 Object result = Subject.doAs(serverLoginContext.getSubject(), (PrivilegedAction<Object>) () -> {
287 Object innerResult;
288 SecurityContext securityContext = new SecurityContext();
289 GSSContext context = null;
290 try {
291 GSSManager manager = GSSManager.getInstance();
292
293
294 Oid krb5oid = new Oid("1.2.840.113554.1.2.2");
295 GSSCredential serverCreds = manager.createCredential(null,
296 GSSCredential.DEFAULT_LIFETIME,
297 krb5oid,
298 GSSCredential.ACCEPT_ONLY);
299 context = manager.createContext(serverCreds);
300
301 securityContext.token = context.acceptSecContext(token, 0, token.length);
302 if (context.isEstablished()) {
303 securityContext.principal = context.getSrcName().toString();
304 LOGGER.debug("Authenticated user: " + securityContext.principal);
305 if (!context.getCredDelegState()) {
306 LOGGER.debug("Credentials can not be delegated");
307 } else {
308
309 securityContext.clientCredential = context.getDelegCred();
310 }
311 }
312 innerResult = securityContext;
313 } catch (GSSException e) {
314 innerResult = e;
315 } finally {
316 if (context != null) {
317 try {
318 context.dispose();
319 } catch (GSSException e) {
320 LOGGER.debug("KerberosHelper.acceptSecurityContext " + e + ' ' + e.getMessage());
321 }
322 }
323 }
324 return innerResult;
325 });
326 if (result instanceof GSSException) {
327 LOGGER.info("KerberosHelper.acceptSecurityContext exception code " + ((GSSException) result).getMajor() + " minor code " + ((GSSException) result).getMinor() + " message " + ((Throwable) result).getMessage());
328 throw (GSSException) result;
329 }
330 return (SecurityContext) result;
331 }
332 }