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 
DflCache(String source)114     public DflCache (String source) {
115         this.source = source;
116     }
117 
defaultPath()118     private static String defaultPath() {
119         return GetPropertyAction.privilegedGetProperty("java.io.tmpdir");
120     }
121 
defaultFile(String server)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 
getFileName(String source, String server)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
checkAndStore(KerberosTime currTime, AuthTimeWithHash time)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 
checkAndStore0(KerberosTime currTime, AuthTimeWithHash time)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")
create(Path p)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 
makeMine(Path p)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 
createNoClose(Path p)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 
expunge(Path p, KerberosTime currTime)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;
loadAndCheck(Path p, AuthTimeWithHash time, KerberosTime currTime)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 
readHeader(SeekableByteChannel chan)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 
append(AuthTimeWithHash at)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
close()353         public void close() throws IOException {
354             if (chan != null) chan.close();
355             chan = null;
356         }
357     }
358 }
359