1 /* 2 * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 27 package sun.security.krb5.internal.rcache; 28 29 import java.io.*; 30 import java.nio.BufferUnderflowException; 31 import java.nio.ByteBuffer; 32 import java.nio.ByteOrder; 33 import java.nio.channels.SeekableByteChannel; 34 import java.nio.file.Files; 35 import java.nio.file.Path; 36 import java.nio.file.StandardCopyOption; 37 import java.nio.file.StandardOpenOption; 38 import java.nio.file.attribute.PosixFilePermission; 39 import java.security.AccessController; 40 import java.util.*; 41 42 import sun.security.action.GetPropertyAction; 43 import sun.security.krb5.internal.KerberosTime; 44 import sun.security.krb5.internal.Krb5; 45 import sun.security.krb5.internal.KrbApErrException; 46 import sun.security.krb5.internal.ReplayCache; 47 48 49 /** 50 * A dfl file is used to sustores AuthTime entries when the system property 51 * sun.security.krb5.rcache is set to 52 * 53 * dfl(|:path/|:path/name|:name) 54 * 55 * The file will be path/name. If path is not given, it will be 56 * 57 * System.getProperty("java.io.tmpdir") 58 * 59 * If name is not given, it will be 60 * 61 * service_euid 62 * 63 * in which euid is available as jdk.internal.misc.VM.geteuid(). 64 * 65 * The file has a header: 66 * 67 * i16 0x0501 (KRB5_RC_VNO) in network order 68 * i32 number of seconds for lifespan (in native order, same below) 69 * 70 * followed by cache entries concatenated, which can be encoded in 71 * 2 styles: 72 * 73 * The traditional style is: 74 * 75 * LC of client principal 76 * LC of server principal 77 * i32 cusec of Authenticator 78 * i32 ctime of Authenticator 79 * 80 * The new style has a hash: 81 * 82 * LC of "" 83 * LC of "HASH:%s %lu:%s %lu:%s" of (hash, clientlen, client, serverlen, 84 * server) where msghash is 32 char (lower case) text mode md5sum 85 * of the ciphertext of authenticator. 86 * i32 cusec of Authenticator 87 * i32 ctime of Authenticator 88 * 89 * where LC of a string means 90 * 91 * i32 strlen(string) + 1 92 * octets of string, with the \0x00 ending 93 * 94 * The old style block is always created by MIT krb5 used even if a new style 95 * is available, which means there can be 2 entries for a single Authenticator. 96 * Java also does this way. 97 * 98 * See src/lib/krb5/rcache/rc_io.c and src/lib/krb5/rcache/rc_dfl.c. 99 * 100 * Update: New version can use other hash algorithms. 101 */ 102 public class DflCache extends ReplayCache { 103 104 private static final int KRB5_RV_VNO = 0x501; 105 private static final int EXCESSREPS = 30; // if missed-hit>this, recreate 106 107 private final String source; 108 109 private static long uid; 110 static { 111 // Available on Solaris, Linux and Mac. Otherwise, -1 and no _euid suffix 112 uid = jdk.internal.misc.VM.geteuid(); 113 } 114 115 public DflCache (String source) { 116 this.source = source; 117 } 118 119 private static String defaultPath() { 120 return AccessController.doPrivileged( 121 new GetPropertyAction("java.io.tmpdir")); 122 } 123 124 private static String defaultFile(String server) { 125 // service/host@REALM -> service 126 int slash = server.indexOf('/'); 127 if (slash == -1) { 128 // A normal principal? say, dummy@REALM 129 slash = server.indexOf('@'); 130 } 131 if (slash != -1) { 132 // Should not happen, but be careful 133 server= server.substring(0, slash); 134 } 135 if (uid != -1) { 136 server += "_" + uid; 137 } 138 return server; 139 } 140 141 private static Path getFileName(String source, String server) { 142 String path, file; 143 if (source.equals("dfl")) { 144 path = defaultPath(); 145 file = defaultFile(server); 146 } else if (source.startsWith("dfl:")) { 147 source = source.substring(4); 148 int pos = source.lastIndexOf('/'); 149 int pos1 = source.lastIndexOf('\\'); 150 if (pos1 > pos) pos = pos1; 151 if (pos == -1) { 152 // Only file name 153 path = defaultPath(); 154 file = source; 155 } else if (new File(source).isDirectory()) { 156 // Only path 157 path = source; 158 file = defaultFile(server); 159 } else { 160 // Full pathname 161 path = null; 162 file = source; 163 } 164 } else { 165 throw new IllegalArgumentException(); 166 } 167 return new File(path, file).toPath(); 168 } 169 170 @Override 171 public void checkAndStore(KerberosTime currTime, AuthTimeWithHash time) 172 throws KrbApErrException { 173 try { 174 checkAndStore0(currTime, time); 175 } catch (IOException ioe) { 176 KrbApErrException ke = new KrbApErrException(Krb5.KRB_ERR_GENERIC); 177 ke.initCause(ioe); 178 throw ke; 179 } 180 } 181 182 private synchronized void checkAndStore0(KerberosTime currTime, AuthTimeWithHash time) 183 throws IOException, KrbApErrException { 184 Path p = getFileName(source, time.server); 185 int missed = 0; 186 try (Storage s = new Storage()) { 187 try { 188 missed = s.loadAndCheck(p, time, currTime); 189 } catch (IOException ioe) { 190 // Non-existing or invalid file 191 Storage.create(p); 192 missed = s.loadAndCheck(p, time, currTime); 193 } 194 s.append(time); 195 } 196 if (missed > EXCESSREPS) { 197 Storage.expunge(p, currTime); 198 } 199 } 200 201 202 private static class Storage implements Closeable { 203 // Static methods 204 @SuppressWarnings("try") 205 private static void create(Path p) throws IOException { 206 try (SeekableByteChannel newChan = createNoClose(p)) { 207 // Do nothing, wait for close 208 } 209 makeMine(p); 210 } 211 212 private static void makeMine(Path p) throws IOException { 213 // chmod to owner-rw only, otherwise MIT krb5 rejects 214 try { 215 Set<PosixFilePermission> attrs = new HashSet<>(); 216 attrs.add(PosixFilePermission.OWNER_READ); 217 attrs.add(PosixFilePermission.OWNER_WRITE); 218 Files.setPosixFilePermissions(p, attrs); 219 } catch (UnsupportedOperationException uoe) { 220 // No POSIX permission. That's OK. 221 } 222 } 223 224 private static SeekableByteChannel createNoClose(Path p) 225 throws IOException { 226 SeekableByteChannel newChan = Files.newByteChannel( 227 p, StandardOpenOption.CREATE, 228 StandardOpenOption.TRUNCATE_EXISTING, 229 StandardOpenOption.WRITE); 230 ByteBuffer buffer = ByteBuffer.allocate(6); 231 buffer.putShort((short)KRB5_RV_VNO); 232 buffer.order(ByteOrder.nativeOrder()); 233 buffer.putInt(KerberosTime.getDefaultSkew()); 234 buffer.flip(); 235 newChan.write(buffer); 236 return newChan; 237 } 238 239 private static void expunge(Path p, KerberosTime currTime) 240 throws IOException { 241 Path p2 = Files.createTempFile(p.getParent(), "rcache", null); 242 try (SeekableByteChannel oldChan = Files.newByteChannel(p); 243 SeekableByteChannel newChan = createNoClose(p2)) { 244 long timeLimit = currTime.getSeconds() - readHeader(oldChan); 245 while (true) { 246 try { 247 AuthTime at = AuthTime.readFrom(oldChan); 248 if (at.ctime > timeLimit) { 249 ByteBuffer bb = ByteBuffer.wrap(at.encode(true)); 250 newChan.write(bb); 251 } 252 } catch (BufferUnderflowException e) { 253 break; 254 } 255 } 256 } 257 makeMine(p2); 258 Files.move(p2, p, 259 StandardCopyOption.REPLACE_EXISTING, 260 StandardCopyOption.ATOMIC_MOVE); 261 } 262 263 // Instance methods 264 SeekableByteChannel chan; 265 private int loadAndCheck(Path p, AuthTimeWithHash time, 266 KerberosTime currTime) 267 throws IOException, KrbApErrException { 268 int missed = 0; 269 if (Files.isSymbolicLink(p)) { 270 throw new IOException("Symlink not accepted"); 271 } 272 try { 273 Set<PosixFilePermission> perms = 274 Files.getPosixFilePermissions(p); 275 if (uid != -1 && 276 (Integer)Files.getAttribute(p, "unix:uid") != uid) { 277 throw new IOException("Not mine"); 278 } 279 if (perms.contains(PosixFilePermission.GROUP_READ) || 280 perms.contains(PosixFilePermission.GROUP_WRITE) || 281 perms.contains(PosixFilePermission.GROUP_EXECUTE) || 282 perms.contains(PosixFilePermission.OTHERS_READ) || 283 perms.contains(PosixFilePermission.OTHERS_WRITE) || 284 perms.contains(PosixFilePermission.OTHERS_EXECUTE)) { 285 throw new IOException("Accessible by someone else"); 286 } 287 } catch (UnsupportedOperationException uoe) { 288 // No POSIX permissions? Ignore it. 289 } 290 chan = Files.newByteChannel(p, StandardOpenOption.WRITE, 291 StandardOpenOption.READ); 292 293 long timeLimit = currTime.getSeconds() - readHeader(chan); 294 295 long pos = 0; 296 boolean seeNewButNotSame = false; 297 while (true) { 298 try { 299 pos = chan.position(); 300 AuthTime a = AuthTime.readFrom(chan); 301 if (a instanceof AuthTimeWithHash) { 302 if (time.equals(a)) { 303 // Exact match, must be a replay 304 throw new KrbApErrException(Krb5.KRB_AP_ERR_REPEAT); 305 } else if (time.sameTimeDiffHash((AuthTimeWithHash)a)) { 306 // Two different authenticators in the same second. 307 // Remember it 308 seeNewButNotSame = true; 309 } 310 } else { 311 if (time.isSameIgnoresHash(a)) { 312 // Two authenticators in the same second. Considered 313 // same if we haven't seen a new style version of it 314 if (!seeNewButNotSame) { 315 throw new KrbApErrException(Krb5.KRB_AP_ERR_REPEAT); 316 } 317 } 318 } 319 if (a.ctime < timeLimit) { 320 missed++; 321 } else { 322 missed--; 323 } 324 } catch (BufferUnderflowException e) { 325 // Half-written file? 326 chan.position(pos); 327 break; 328 } 329 } 330 return missed; 331 } 332 333 private static int readHeader(SeekableByteChannel chan) 334 throws IOException { 335 ByteBuffer bb = ByteBuffer.allocate(6); 336 chan.read(bb); 337 if (bb.getShort(0) != KRB5_RV_VNO) { 338 throw new IOException("Not correct rcache version"); 339 } 340 bb.order(ByteOrder.nativeOrder()); 341 return bb.getInt(2); 342 } 343 344 private void append(AuthTimeWithHash at) throws IOException { 345 // Write an entry with hash, to be followed by one without it, 346 // for the benefit of old implementations. 347 ByteBuffer bb; 348 bb = ByteBuffer.wrap(at.encode(true)); 349 chan.write(bb); 350 bb = ByteBuffer.wrap(at.encode(false)); 351 chan.write(bb); 352 } 353 354 @Override 355 public void close() throws IOException { 356 if (chan != null) chan.close(); 357 chan = null; 358 } 359 } 360 }