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 }