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