1 /* 2 * Copyright (C) 2007 The Guava Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14 15 package com.google.common.io; 16 17 import static com.google.common.base.StandardSystemProperty.JAVA_IO_TMPDIR; 18 import static com.google.common.base.StandardSystemProperty.USER_NAME; 19 import static com.google.common.base.Throwables.throwIfUnchecked; 20 import static java.nio.file.attribute.AclEntryFlag.DIRECTORY_INHERIT; 21 import static java.nio.file.attribute.AclEntryFlag.FILE_INHERIT; 22 import static java.nio.file.attribute.AclEntryType.ALLOW; 23 import static java.nio.file.attribute.PosixFilePermissions.asFileAttribute; 24 import static java.util.Objects.requireNonNull; 25 26 import com.google.common.annotations.GwtIncompatible; 27 import com.google.common.annotations.J2ktIncompatible; 28 import com.google.common.annotations.VisibleForTesting; 29 import com.google.common.collect.ImmutableList; 30 import com.google.j2objc.annotations.J2ObjCIncompatible; 31 import java.io.File; 32 import java.io.IOException; 33 import java.lang.reflect.InvocationTargetException; 34 import java.lang.reflect.Method; 35 import java.nio.file.FileSystems; 36 import java.nio.file.Paths; 37 import java.nio.file.attribute.AclEntry; 38 import java.nio.file.attribute.AclEntryPermission; 39 import java.nio.file.attribute.FileAttribute; 40 import java.nio.file.attribute.PosixFilePermissions; 41 import java.nio.file.attribute.UserPrincipal; 42 import java.util.EnumSet; 43 import java.util.Set; 44 45 /** 46 * Creates temporary files and directories whose permissions are restricted to the current user or, 47 * in the case of Android, the current app. If that is not possible (as is the case under the very 48 * old Android Ice Cream Sandwich release), then this class throws an exception instead of creating 49 * a file or directory that would be more accessible. 50 */ 51 @J2ktIncompatible 52 @GwtIncompatible 53 @J2ObjCIncompatible 54 @ElementTypesAreNonnullByDefault 55 abstract class TempFileCreator { 56 static final TempFileCreator INSTANCE = pickSecureCreator(); 57 58 /** 59 * @throws IllegalStateException if the directory could not be created (to implement the contract 60 * of {@link Files#createTempDir()}, such as if the system does not support creating temporary 61 * directories securely 62 */ createTempDir()63 abstract File createTempDir(); 64 createTempFile(String prefix)65 abstract File createTempFile(String prefix) throws IOException; 66 pickSecureCreator()67 private static TempFileCreator pickSecureCreator() { 68 try { 69 Class.forName("java.nio.file.Path"); 70 return new JavaNioCreator(); 71 } catch (ClassNotFoundException runningUnderAndroid) { 72 // Try another way. 73 } 74 75 try { 76 int version = (int) Class.forName("android.os.Build$VERSION").getField("SDK_INT").get(null); 77 int jellyBean = 78 (int) Class.forName("android.os.Build$VERSION_CODES").getField("JELLY_BEAN").get(null); 79 /* 80 * I assume that this check can't fail because JELLY_BEAN will be present only if we're 81 * running under Jelly Bean or higher. But it seems safest to check. 82 */ 83 if (version < jellyBean) { 84 return new ThrowingCreator(); 85 } 86 87 // Don't merge these catch() blocks, let alone use ReflectiveOperationException directly: 88 // b/65343391 89 } catch (NoSuchFieldException e) { 90 // The JELLY_BEAN field doesn't exist because we're running on a version before Jelly Bean :) 91 return new ThrowingCreator(); 92 } catch (ClassNotFoundException e) { 93 // Should be impossible, but we want to return *something* so that class init succeeds. 94 return new ThrowingCreator(); 95 } catch (IllegalAccessException e) { 96 // ditto 97 return new ThrowingCreator(); 98 } 99 100 // Android isolates apps' temporary directories since Jelly Bean: 101 // https://github.com/google/guava/issues/4011#issuecomment-770020802 102 // So we can create files there with any permissions and still get security from the isolation. 103 return new JavaIoCreator(); 104 } 105 106 /** 107 * Creates the permissions normally used for Windows filesystems, looking up the user afresh, even 108 * if previous calls have initialized the {@code PermissionSupplier} fields. 109 * 110 * <p>This lets us test the effects of different values of the {@code user.name} system property 111 * without needing a separate VM or classloader. 112 */ 113 @IgnoreJRERequirement // used only when Path is available (and only from tests) 114 @VisibleForTesting testMakingUserPermissionsFromScratch()115 static void testMakingUserPermissionsFromScratch() throws IOException { 116 // All we're testing is whether it throws. 117 FileAttribute<?> unused = JavaNioCreator.userPermissions().get(); 118 } 119 120 @IgnoreJRERequirement // used only when Path is available 121 private static final class JavaNioCreator extends TempFileCreator { 122 @Override createTempDir()123 File createTempDir() { 124 try { 125 return java.nio.file.Files.createTempDirectory( 126 Paths.get(JAVA_IO_TMPDIR.value()), /* prefix= */ null, directoryPermissions.get()) 127 .toFile(); 128 } catch (IOException e) { 129 throw new IllegalStateException("Failed to create directory", e); 130 } 131 } 132 133 @Override createTempFile(String prefix)134 File createTempFile(String prefix) throws IOException { 135 return java.nio.file.Files.createTempFile( 136 Paths.get(JAVA_IO_TMPDIR.value()), 137 /* prefix= */ prefix, 138 /* suffix= */ null, 139 filePermissions.get()) 140 .toFile(); 141 } 142 143 @IgnoreJRERequirement // see enclosing class (whose annotation Animal Sniffer ignores here...) 144 private interface PermissionSupplier { get()145 FileAttribute<?> get() throws IOException; 146 } 147 148 private static final PermissionSupplier filePermissions; 149 private static final PermissionSupplier directoryPermissions; 150 151 static { 152 Set<String> views = FileSystems.getDefault().supportedFileAttributeViews(); 153 if (views.contains("posix")) { 154 filePermissions = () -> asFileAttribute(PosixFilePermissions.fromString("rw-------")); 155 directoryPermissions = () -> asFileAttribute(PosixFilePermissions.fromString("rwx------")); 156 } else if (views.contains("acl")) { 157 filePermissions = directoryPermissions = userPermissions(); 158 } else { 159 filePermissions = 160 directoryPermissions = 161 () -> { 162 throw new IOException("unrecognized FileSystem type " + FileSystems.getDefault()); 163 }; 164 } 165 } 166 userPermissions()167 private static PermissionSupplier userPermissions() { 168 try { 169 UserPrincipal user = 170 FileSystems.getDefault() 171 .getUserPrincipalLookupService() 172 .lookupPrincipalByName(getUsername()); 173 ImmutableList<AclEntry> acl = 174 ImmutableList.of( 175 AclEntry.newBuilder() 176 .setType(ALLOW) 177 .setPrincipal(user) 178 .setPermissions(EnumSet.allOf(AclEntryPermission.class)) 179 .setFlags(DIRECTORY_INHERIT, FILE_INHERIT) 180 .build()); 181 FileAttribute<ImmutableList<AclEntry>> attribute = 182 new FileAttribute<ImmutableList<AclEntry>>() { 183 @Override 184 public String name() { 185 return "acl:acl"; 186 } 187 188 @Override 189 public ImmutableList<AclEntry> value() { 190 return acl; 191 } 192 }; 193 return () -> attribute; 194 } catch (IOException e) { 195 // We throw a new exception each time so that the stack trace is right. 196 return () -> { 197 throw new IOException("Could not find user", e); 198 }; 199 } 200 } 201 getUsername()202 private static String getUsername() { 203 /* 204 * https://github.com/google/guava/issues/6634: ProcessHandle has more accurate information, 205 * but that class isn't available under all environments that we support. We use it if 206 * available and fall back if not. 207 */ 208 String fromSystemProperty = requireNonNull(USER_NAME.value()); 209 210 try { 211 Class<?> processHandleClass = Class.forName("java.lang.ProcessHandle"); 212 Class<?> processHandleInfoClass = Class.forName("java.lang.ProcessHandle$Info"); 213 Class<?> optionalClass = Class.forName("java.util.Optional"); 214 /* 215 * We don't *need* to use reflection to access Optional: It's available on all JDKs we 216 * support, and Android code won't get this far, anyway, because ProcessHandle is 217 * unavailable. But given how much other reflection we're using, we might as well use it 218 * here, too, so that we don't need to also suppress an AndroidApiChecker error. 219 */ 220 221 Method currentMethod = processHandleClass.getMethod("current"); 222 Method infoMethod = processHandleClass.getMethod("info"); 223 Method userMethod = processHandleInfoClass.getMethod("user"); 224 Method orElseMethod = optionalClass.getMethod("orElse", Object.class); 225 226 Object current = currentMethod.invoke(null); 227 Object info = infoMethod.invoke(current); 228 Object user = userMethod.invoke(info); 229 return (String) requireNonNull(orElseMethod.invoke(user, fromSystemProperty)); 230 } catch (ClassNotFoundException runningUnderAndroidOrJava8) { 231 /* 232 * I'm not sure that we could actually get here for *Android*: I would expect us to enter 233 * the POSIX code path instead. And if we tried this code path, we'd have trouble unless we 234 * were running under a new enough version of Android to support NIO. 235 * 236 * So this is probably just the "Windows Java 8" case. In that case, if we wanted *another* 237 * layer of fallback before consulting the system property, we could try 238 * com.sun.security.auth.module.NTSystem. 239 * 240 * But for now, we use the value from the system property as our best guess. 241 */ 242 return fromSystemProperty; 243 } catch (InvocationTargetException e) { 244 throwIfUnchecked(e.getCause()); // in case it's an Error or something 245 return fromSystemProperty; // should be impossible 246 } catch (NoSuchMethodException shouldBeImpossible) { 247 return fromSystemProperty; 248 } catch (IllegalAccessException shouldBeImpossible) { 249 /* 250 * We don't merge these into `catch (ReflectiveOperationException ...)` or an equivalent 251 * multicatch because ReflectiveOperationException isn't available under Android: 252 * b/124188803 253 */ 254 return fromSystemProperty; 255 } 256 } 257 } 258 259 private static final class JavaIoCreator extends TempFileCreator { 260 @Override createTempDir()261 File createTempDir() { 262 File baseDir = new File(JAVA_IO_TMPDIR.value()); 263 @SuppressWarnings("GoodTime") // reading system time without TimeSource 264 String baseName = System.currentTimeMillis() + "-"; 265 266 for (int counter = 0; counter < TEMP_DIR_ATTEMPTS; counter++) { 267 File tempDir = new File(baseDir, baseName + counter); 268 if (tempDir.mkdir()) { 269 return tempDir; 270 } 271 } 272 throw new IllegalStateException( 273 "Failed to create directory within " 274 + TEMP_DIR_ATTEMPTS 275 + " attempts (tried " 276 + baseName 277 + "0 to " 278 + baseName 279 + (TEMP_DIR_ATTEMPTS - 1) 280 + ')'); 281 } 282 283 @Override createTempFile(String prefix)284 File createTempFile(String prefix) throws IOException { 285 return File.createTempFile( 286 /* prefix= */ prefix, 287 /* suffix= */ null, 288 /* directory= */ null /* defaults to java.io.tmpdir */); 289 } 290 291 /** Maximum loop count when creating temp directories. */ 292 private static final int TEMP_DIR_ATTEMPTS = 10000; 293 } 294 295 private static final class ThrowingCreator extends TempFileCreator { 296 private static final String MESSAGE = 297 "Guava cannot securely create temporary files or directories under SDK versions before" 298 + " Jelly Bean. You can create one yourself, either in the insecure default directory" 299 + " or in a more secure directory, such as context.getCacheDir(). For more information," 300 + " see the Javadoc for Files.createTempDir()."; 301 302 @Override createTempDir()303 File createTempDir() { 304 throw new IllegalStateException(MESSAGE); 305 } 306 307 @Override createTempFile(String prefix)308 File createTempFile(String prefix) throws IOException { 309 throw new IOException(MESSAGE); 310 } 311 } 312 TempFileCreator()313 private TempFileCreator() {} 314 } 315