368 lines
13 KiB
Java
368 lines
13 KiB
Java
/*
|
|
* Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
|
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
|
*
|
|
* This code is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License version 2 only, as
|
|
* published by the Free Software Foundation. Oracle designates this
|
|
* particular file as subject to the "Classpath" exception as provided
|
|
* by Oracle in the LICENSE file that accompanied this code.
|
|
*
|
|
* This code is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* version 2 for more details (a copy is included in the LICENSE file that
|
|
* accompanied this code).
|
|
*
|
|
* You should have received a copy of the GNU General Public License version
|
|
* 2 along with this work; if not, write to the Free Software Foundation,
|
|
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*
|
|
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
|
* or visit www.oracle.com if you need additional information or have any
|
|
* questions.
|
|
*/
|
|
|
|
|
|
package sun.security.krb5.internal.rcache;
|
|
|
|
import java.io.*;
|
|
import java.nio.BufferUnderflowException;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.ByteOrder;
|
|
import java.nio.channels.SeekableByteChannel;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Path;
|
|
import java.nio.file.StandardCopyOption;
|
|
import java.nio.file.StandardOpenOption;
|
|
import java.nio.file.attribute.PosixFilePermission;
|
|
import java.security.AccessController;
|
|
import java.util.*;
|
|
|
|
import sun.security.action.GetPropertyAction;
|
|
import sun.security.krb5.internal.KerberosTime;
|
|
import sun.security.krb5.internal.Krb5;
|
|
import sun.security.krb5.internal.KrbApErrException;
|
|
import sun.security.krb5.internal.ReplayCache;
|
|
|
|
|
|
/**
|
|
* A dfl file is used to sustores AuthTime entries when the system property
|
|
* sun.security.krb5.rcache is set to
|
|
*
|
|
* dfl(|:path/|:path/name|:name)
|
|
*
|
|
* The file will be path/name. If path is not given, it will be
|
|
*
|
|
* System.getProperty("java.io.tmpdir")
|
|
*
|
|
* If name is not given, it will be
|
|
*
|
|
* service_euid
|
|
*
|
|
* Java does not have a method to get euid, so uid is used instead. This
|
|
* should normally to be since a Java program is seldom used as a setuid app.
|
|
*
|
|
* The file has a header:
|
|
*
|
|
* i16 0x0501 (KRB5_RC_VNO) in network order
|
|
* i32 number of seconds for lifespan (in native order, same below)
|
|
*
|
|
* followed by cache entries concatenated, which can be encoded in
|
|
* 2 styles:
|
|
*
|
|
* The traditional style is:
|
|
*
|
|
* LC of client principal
|
|
* LC of server principal
|
|
* i32 cusec of Authenticator
|
|
* i32 ctime of Authenticator
|
|
*
|
|
* The new style has a hash:
|
|
*
|
|
* LC of ""
|
|
* LC of "HASH:%s %lu:%s %lu:%s" of (hash, clientlen, client, serverlen,
|
|
* server) where msghash is 32 char (lower case) text mode md5sum
|
|
* of the ciphertext of authenticator.
|
|
* i32 cusec of Authenticator
|
|
* i32 ctime of Authenticator
|
|
*
|
|
* where LC of a string means
|
|
*
|
|
* i32 strlen(string) + 1
|
|
* octets of string, with the \0x00 ending
|
|
*
|
|
* The old style block is always created by MIT krb5 used even if a new style
|
|
* is available, which means there can be 2 entries for a single Authenticator.
|
|
* Java also does this way.
|
|
*
|
|
* See src/lib/krb5/rcache/rc_io.c and src/lib/krb5/rcache/rc_dfl.c.
|
|
*
|
|
* Update: New version can use other hash algorithms.
|
|
*/
|
|
public class DflCache extends ReplayCache {
|
|
|
|
private static final int KRB5_RV_VNO = 0x501;
|
|
private static final int EXCESSREPS = 30; // if missed-hit>this, recreate
|
|
|
|
private final String source;
|
|
|
|
private static int uid;
|
|
static {
|
|
try {
|
|
// Available on Solaris, Linux and Mac. Otherwise, no _euid suffix
|
|
Class<?> clazz = Class.forName("com.sun.security.auth.module.UnixSystem");
|
|
uid = (int)(long)(Long)
|
|
clazz.getMethod("getUid").invoke(clazz.newInstance());
|
|
} catch (Exception e) {
|
|
uid = -1;
|
|
}
|
|
}
|
|
|
|
public DflCache (String source) {
|
|
this.source = source;
|
|
}
|
|
|
|
private static String defaultPath() {
|
|
return AccessController.doPrivileged(
|
|
new GetPropertyAction("java.io.tmpdir"));
|
|
}
|
|
|
|
private static String defaultFile(String server) {
|
|
// service/host@REALM -> service
|
|
int slash = server.indexOf('/');
|
|
if (slash == -1) {
|
|
// A normal principal? say, dummy@REALM
|
|
slash = server.indexOf('@');
|
|
}
|
|
if (slash != -1) {
|
|
// Should not happen, but be careful
|
|
server= server.substring(0, slash);
|
|
}
|
|
if (uid != -1) {
|
|
server += "_" + uid;
|
|
}
|
|
return server;
|
|
}
|
|
|
|
private static Path getFileName(String source, String server) {
|
|
String path, file;
|
|
if (source.equals("dfl")) {
|
|
path = defaultPath();
|
|
file = defaultFile(server);
|
|
} else if (source.startsWith("dfl:")) {
|
|
source = source.substring(4);
|
|
int pos = source.lastIndexOf('/');
|
|
int pos1 = source.lastIndexOf('\\');
|
|
if (pos1 > pos) pos = pos1;
|
|
if (pos == -1) {
|
|
// Only file name
|
|
path = defaultPath();
|
|
file = source;
|
|
} else if (new File(source).isDirectory()) {
|
|
// Only path
|
|
path = source;
|
|
file = defaultFile(server);
|
|
} else {
|
|
// Full pathname
|
|
path = null;
|
|
file = source;
|
|
}
|
|
} else {
|
|
throw new IllegalArgumentException();
|
|
}
|
|
return new File(path, file).toPath();
|
|
}
|
|
|
|
@Override
|
|
public void checkAndStore(KerberosTime currTime, AuthTimeWithHash time)
|
|
throws KrbApErrException {
|
|
try {
|
|
checkAndStore0(currTime, time);
|
|
} catch (IOException ioe) {
|
|
KrbApErrException ke = new KrbApErrException(Krb5.KRB_ERR_GENERIC);
|
|
ke.initCause(ioe);
|
|
throw ke;
|
|
}
|
|
}
|
|
|
|
private synchronized void checkAndStore0(KerberosTime currTime, AuthTimeWithHash time)
|
|
throws IOException, KrbApErrException {
|
|
Path p = getFileName(source, time.server);
|
|
int missed = 0;
|
|
try (Storage s = new Storage()) {
|
|
try {
|
|
missed = s.loadAndCheck(p, time, currTime);
|
|
} catch (IOException ioe) {
|
|
// Non-existing or invalid file
|
|
Storage.create(p);
|
|
missed = s.loadAndCheck(p, time, currTime);
|
|
}
|
|
s.append(time);
|
|
}
|
|
if (missed > EXCESSREPS) {
|
|
Storage.expunge(p, currTime);
|
|
}
|
|
}
|
|
|
|
|
|
private static class Storage implements Closeable {
|
|
// Static methods
|
|
@SuppressWarnings("try")
|
|
private static void create(Path p) throws IOException {
|
|
try (SeekableByteChannel newChan = createNoClose(p)) {
|
|
// Do nothing, wait for close
|
|
}
|
|
makeMine(p);
|
|
}
|
|
|
|
private static void makeMine(Path p) throws IOException {
|
|
// chmod to owner-rw only, otherwise MIT krb5 rejects
|
|
try {
|
|
Set<PosixFilePermission> attrs = new HashSet<>();
|
|
attrs.add(PosixFilePermission.OWNER_READ);
|
|
attrs.add(PosixFilePermission.OWNER_WRITE);
|
|
Files.setPosixFilePermissions(p, attrs);
|
|
} catch (UnsupportedOperationException uoe) {
|
|
// No POSIX permission. That's OK.
|
|
}
|
|
}
|
|
|
|
private static SeekableByteChannel createNoClose(Path p)
|
|
throws IOException {
|
|
SeekableByteChannel newChan = Files.newByteChannel(
|
|
p, StandardOpenOption.CREATE,
|
|
StandardOpenOption.TRUNCATE_EXISTING,
|
|
StandardOpenOption.WRITE);
|
|
ByteBuffer buffer = ByteBuffer.allocate(6);
|
|
buffer.putShort((short)KRB5_RV_VNO);
|
|
buffer.order(ByteOrder.nativeOrder());
|
|
buffer.putInt(KerberosTime.getDefaultSkew());
|
|
buffer.flip();
|
|
newChan.write(buffer);
|
|
return newChan;
|
|
}
|
|
|
|
private static void expunge(Path p, KerberosTime currTime)
|
|
throws IOException {
|
|
Path p2 = Files.createTempFile(p.getParent(), "rcache", null);
|
|
try (SeekableByteChannel oldChan = Files.newByteChannel(p);
|
|
SeekableByteChannel newChan = createNoClose(p2)) {
|
|
long timeLimit = currTime.getSeconds() - readHeader(oldChan);
|
|
while (true) {
|
|
try {
|
|
AuthTime at = AuthTime.readFrom(oldChan);
|
|
if (at.ctime > timeLimit) {
|
|
ByteBuffer bb = ByteBuffer.wrap(at.encode(true));
|
|
newChan.write(bb);
|
|
}
|
|
} catch (BufferUnderflowException e) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
makeMine(p2);
|
|
Files.move(p2, p,
|
|
StandardCopyOption.REPLACE_EXISTING,
|
|
StandardCopyOption.ATOMIC_MOVE);
|
|
}
|
|
|
|
// Instance methods
|
|
SeekableByteChannel chan;
|
|
private int loadAndCheck(Path p, AuthTimeWithHash time,
|
|
KerberosTime currTime)
|
|
throws IOException, KrbApErrException {
|
|
int missed = 0;
|
|
if (Files.isSymbolicLink(p)) {
|
|
throw new IOException("Symlink not accepted");
|
|
}
|
|
try {
|
|
Set<PosixFilePermission> perms =
|
|
Files.getPosixFilePermissions(p);
|
|
if (uid != -1 &&
|
|
(Integer)Files.getAttribute(p, "unix:uid") != uid) {
|
|
throw new IOException("Not mine");
|
|
}
|
|
if (perms.contains(PosixFilePermission.GROUP_READ) ||
|
|
perms.contains(PosixFilePermission.GROUP_WRITE) ||
|
|
perms.contains(PosixFilePermission.GROUP_EXECUTE) ||
|
|
perms.contains(PosixFilePermission.OTHERS_READ) ||
|
|
perms.contains(PosixFilePermission.OTHERS_WRITE) ||
|
|
perms.contains(PosixFilePermission.OTHERS_EXECUTE)) {
|
|
throw new IOException("Accessible by someone else");
|
|
}
|
|
} catch (UnsupportedOperationException uoe) {
|
|
// No POSIX permissions? Ignore it.
|
|
}
|
|
chan = Files.newByteChannel(p, StandardOpenOption.WRITE,
|
|
StandardOpenOption.READ);
|
|
|
|
long timeLimit = currTime.getSeconds() - readHeader(chan);
|
|
|
|
long pos = 0;
|
|
boolean seeNewButNotSame = false;
|
|
while (true) {
|
|
try {
|
|
pos = chan.position();
|
|
AuthTime a = AuthTime.readFrom(chan);
|
|
if (a instanceof AuthTimeWithHash) {
|
|
if (time.equals(a)) {
|
|
// Exact match, must be a replay
|
|
throw new KrbApErrException(Krb5.KRB_AP_ERR_REPEAT);
|
|
} else if (time.sameTimeDiffHash((AuthTimeWithHash)a)) {
|
|
// Two different authenticators in the same second.
|
|
// Remember it
|
|
seeNewButNotSame = true;
|
|
}
|
|
} else {
|
|
if (time.isSameIgnoresHash(a)) {
|
|
// Two authenticators in the same second. Considered
|
|
// same if we haven't seen a new style version of it
|
|
if (!seeNewButNotSame) {
|
|
throw new KrbApErrException(Krb5.KRB_AP_ERR_REPEAT);
|
|
}
|
|
}
|
|
}
|
|
if (a.ctime < timeLimit) {
|
|
missed++;
|
|
} else {
|
|
missed--;
|
|
}
|
|
} catch (BufferUnderflowException e) {
|
|
// Half-written file?
|
|
chan.position(pos);
|
|
break;
|
|
}
|
|
}
|
|
return missed;
|
|
}
|
|
|
|
private static int readHeader(SeekableByteChannel chan)
|
|
throws IOException {
|
|
ByteBuffer bb = ByteBuffer.allocate(6);
|
|
chan.read(bb);
|
|
if (bb.getShort(0) != KRB5_RV_VNO) {
|
|
throw new IOException("Not correct rcache version");
|
|
}
|
|
bb.order(ByteOrder.nativeOrder());
|
|
return bb.getInt(2);
|
|
}
|
|
|
|
private void append(AuthTimeWithHash at) throws IOException {
|
|
// Write an entry with hash, to be followed by one without it,
|
|
// for the benefit of old implementations.
|
|
ByteBuffer bb;
|
|
bb = ByteBuffer.wrap(at.encode(true));
|
|
chan.write(bb);
|
|
bb = ByteBuffer.wrap(at.encode(false));
|
|
chan.write(bb);
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
if (chan != null) chan.close();
|
|
chan = null;
|
|
}
|
|
}
|
|
}
|