Merge "Use fs-verity for protecting updatable font files."
This commit is contained in:
committed by
Android (Google) Code Review
commit
4fc6b1a86b
@@ -50,9 +50,11 @@ public final class SystemFonts {
|
||||
private static final String DEFAULT_FAMILY = "sans-serif";
|
||||
|
||||
private static final String FONTS_XML = "/system/etc/fonts.xml";
|
||||
private static final String SYSTEM_FONT_DIR = "/system/fonts/";
|
||||
/** @hide */
|
||||
public static final String SYSTEM_FONT_DIR = "/system/fonts/";
|
||||
private static final String OEM_XML = "/product/etc/fonts_customization.xml";
|
||||
private static final String OEM_FONT_DIR = "/product/fonts/";
|
||||
/** @hide */
|
||||
public static final String OEM_FONT_DIR = "/product/fonts/";
|
||||
|
||||
private SystemFonts() {} // Do not instansiate.
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ import android.util.Slog;
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
import com.android.server.LocalServices;
|
||||
import com.android.server.SystemService;
|
||||
import com.android.server.security.FileIntegrityService;
|
||||
import com.android.server.security.VerityUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
@@ -39,6 +41,7 @@ import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.NioUtils;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
/** A service for managing system fonts. */
|
||||
@@ -78,7 +81,17 @@ public final class FontManagerService {
|
||||
|
||||
private static class OtfFontFileParser implements UpdatableFontDir.FontFileParser {
|
||||
@Override
|
||||
public long getVersion(File file) throws IOException {
|
||||
public String getPostScriptName(File file) throws IOException {
|
||||
ByteBuffer buffer = mmap(file);
|
||||
try {
|
||||
return FontFileUtil.getPostScriptName(buffer, 0);
|
||||
} finally {
|
||||
NioUtils.freeDirectBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRevision(File file) throws IOException {
|
||||
ByteBuffer buffer = mmap(file);
|
||||
try {
|
||||
return FontFileUtil.getRevision(buffer, 0);
|
||||
@@ -95,18 +108,48 @@ public final class FontManagerService {
|
||||
}
|
||||
}
|
||||
|
||||
private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil {
|
||||
@Override
|
||||
public boolean hasFsverity(String filePath) {
|
||||
return VerityUtils.hasFsverity(filePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUpFsverity(String filePath, byte[] pkcs7Signature) throws IOException {
|
||||
VerityUtils.setUpFsverity(filePath, pkcs7Signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rename(File src, File dest) {
|
||||
// rename system call preserves fs-verity bit.
|
||||
return src.renameTo(dest);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private final UpdatableFontDir mUpdatableFontDir;
|
||||
|
||||
@GuardedBy("FontManagerService.this")
|
||||
@Nullable SystemFontSettings mCurrentFontSettings = null;
|
||||
@Nullable
|
||||
private SystemFontSettings mCurrentFontSettings = null;
|
||||
|
||||
private FontManagerService() {
|
||||
mUpdatableFontDir = ENABLE_FONT_UPDATES
|
||||
? new UpdatableFontDir(new File(FONT_FILES_DIR), new OtfFontFileParser()) : null;
|
||||
mUpdatableFontDir = createUpdatableFontDir();
|
||||
}
|
||||
|
||||
@NonNull private SystemFontSettings getCurrentFontSettings() {
|
||||
@Nullable
|
||||
private static UpdatableFontDir createUpdatableFontDir() {
|
||||
if (!ENABLE_FONT_UPDATES) return null;
|
||||
// If apk verity is supported, fs-verity should be available.
|
||||
if (!FileIntegrityService.isApkVeritySupported()) return null;
|
||||
return new UpdatableFontDir(new File(FONT_FILES_DIR),
|
||||
Arrays.asList(new File(SystemFonts.SYSTEM_FONT_DIR),
|
||||
new File(SystemFonts.OEM_FONT_DIR)),
|
||||
new OtfFontFileParser(), new FsverityUtilImpl());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private SystemFontSettings getCurrentFontSettings() {
|
||||
synchronized (FontManagerService.this) {
|
||||
if (mCurrentFontSettings == null) {
|
||||
mCurrentFontSettings = SystemFontSettings.create(mUpdatableFontDir);
|
||||
@@ -115,13 +158,14 @@ public final class FontManagerService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean installFontFile(String name, FileDescriptor fd) {
|
||||
// TODO(b/173619554): Expose as API.
|
||||
private boolean installFontFile(FileDescriptor fd, byte[] pkcs7Signature) {
|
||||
if (mUpdatableFontDir == null) return false;
|
||||
synchronized (FontManagerService.this) {
|
||||
try {
|
||||
mUpdatableFontDir.installFontFile(name, fd);
|
||||
mUpdatableFontDir.installFontFile(fd, pkcs7Signature);
|
||||
} catch (IOException e) {
|
||||
Slog.w(TAG, "Failed to install font file: " + name, e);
|
||||
Slog.w(TAG, "Failed to install font file");
|
||||
return false;
|
||||
}
|
||||
// Create updated font map in the next getSerializedSystemFontMap() call.
|
||||
@@ -194,5 +238,5 @@ public final class FontManagerService {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.android.server.graphics.fonts;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.os.FileUtils;
|
||||
import android.util.Base64;
|
||||
import android.util.Slog;
|
||||
@@ -28,71 +29,158 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class UpdatableFontDir {
|
||||
|
||||
private static final String TAG = "UpdatableFontDir";
|
||||
private static final String RANDOM_DIR_PREFIX = "~~";
|
||||
// TODO: Support .otf
|
||||
private static final String ALLOWED_EXTENSION = ".ttf";
|
||||
|
||||
/** Interface to mock font file access in tests. */
|
||||
interface FontFileParser {
|
||||
long getVersion(File file) throws IOException;
|
||||
String getPostScriptName(File file) throws IOException;
|
||||
|
||||
long getRevision(File file) throws IOException;
|
||||
}
|
||||
|
||||
/** Data class to hold font file path and version. */
|
||||
static final class FontFileInfo {
|
||||
final File mFile;
|
||||
final long mVersion;
|
||||
/** Interface to mock fs-verity in tests. */
|
||||
interface FsverityUtil {
|
||||
boolean hasFsverity(String path);
|
||||
|
||||
FontFileInfo(File file, long version) {
|
||||
void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException;
|
||||
|
||||
boolean rename(File src, File dest);
|
||||
}
|
||||
|
||||
/** Data class to hold font file path and revision. */
|
||||
private static final class FontFileInfo {
|
||||
private final File mFile;
|
||||
private final long mRevision;
|
||||
|
||||
FontFileInfo(File file, long revision) {
|
||||
mFile = file;
|
||||
mVersion = version;
|
||||
mRevision = revision;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return mFile;
|
||||
}
|
||||
|
||||
/** Returns the unique randomized font dir containing this font file. */
|
||||
public File getRandomizedFontDir() {
|
||||
return mFile.getParentFile();
|
||||
}
|
||||
|
||||
public long getRevision() {
|
||||
return mRevision;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Root directory for storing updated font files. Each font file is stored in a unique random
|
||||
* dir. The font file path would be {@code mFilesDir/~~{randomStr}/{fontFileName}}.
|
||||
* Root directory for storing updated font files. Each font file is stored in a unique
|
||||
* randomized dir. The font file path would be {@code mFilesDir/~~{randomStr}/{fontFileName}}.
|
||||
*/
|
||||
private final File mFilesDir;
|
||||
private final List<File> mPreinstalledFontDirs;
|
||||
private final FontFileParser mParser;
|
||||
private final FsverityUtil mFsverityUtil;
|
||||
/**
|
||||
* A mutable map containing mapping from font file name (e.g. "NotoColorEmoji.ttf") to {@link
|
||||
* FontFileInfo}. All files in this map are validated, and have higher revision numbers than
|
||||
* corresponding font files in {@link #mPreinstalledFontDirs}.
|
||||
*/
|
||||
@GuardedBy("UpdatableFontDir.this")
|
||||
private final Map<String, FontFileInfo> mFontFileInfoMap = new HashMap<>();
|
||||
|
||||
UpdatableFontDir(File filesDir, FontFileParser parser) {
|
||||
UpdatableFontDir(File filesDir, List<File> preinstalledFontDirs, FontFileParser parser,
|
||||
FsverityUtil fsverityUtil) {
|
||||
mFilesDir = filesDir;
|
||||
mPreinstalledFontDirs = preinstalledFontDirs;
|
||||
mParser = parser;
|
||||
mFsverityUtil = fsverityUtil;
|
||||
loadFontFileMap();
|
||||
}
|
||||
|
||||
private void loadFontFileMap() {
|
||||
// TODO: SIGBUS crash protection
|
||||
synchronized (UpdatableFontDir.this) {
|
||||
boolean success = false;
|
||||
mFontFileInfoMap.clear();
|
||||
File[] dirs = mFilesDir.listFiles();
|
||||
if (dirs == null) return;
|
||||
for (File dir : dirs) {
|
||||
if (!dir.getName().startsWith(RANDOM_DIR_PREFIX)) continue;
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null || files.length != 1) continue;
|
||||
addFileToMapLocked(files[0], true);
|
||||
try {
|
||||
File[] dirs = mFilesDir.listFiles();
|
||||
if (dirs == null) return;
|
||||
for (File dir : dirs) {
|
||||
if (!dir.getName().startsWith(RANDOM_DIR_PREFIX)) return;
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null || files.length != 1) return;
|
||||
FontFileInfo fontFileInfo = validateFontFile(files[0]);
|
||||
if (fontFileInfo == null) {
|
||||
Slog.w(TAG, "Broken file is found. Clearing files.");
|
||||
return;
|
||||
}
|
||||
addFileToMapLocked(fontFileInfo, true /* deleteOldFile */);
|
||||
}
|
||||
success = true;
|
||||
} finally {
|
||||
// Delete all files just in case if we find a problematic file.
|
||||
if (!success) {
|
||||
mFontFileInfoMap.clear();
|
||||
FileUtils.deleteContents(mFilesDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void installFontFile(String name, FileDescriptor fd) throws IOException {
|
||||
// TODO: Validate name.
|
||||
/**
|
||||
* Installs a new font file, or updates an existing font file.
|
||||
*
|
||||
* <p>The new font will be immediately available for new Zygote-forked processes through
|
||||
* {@link #getFontFileMap()}. Old font files will be kept until next system server reboot,
|
||||
* because existing Zygote-forked processes have paths to old font files.
|
||||
*
|
||||
* @param fd A file descriptor to the font file.
|
||||
* @param pkcs7Signature A PKCS#7 detached signature to enable fs-verity for the font file.
|
||||
*/
|
||||
void installFontFile(FileDescriptor fd, byte[] pkcs7Signature) throws IOException {
|
||||
synchronized (UpdatableFontDir.this) {
|
||||
// TODO: proper error handling
|
||||
File newDir = getRandomDir(mFilesDir);
|
||||
if (!newDir.mkdir()) {
|
||||
// TODO: Define and return an error code for API
|
||||
throw new IOException("Failed to create a new dir");
|
||||
}
|
||||
File newFontFile = new File(newDir, name);
|
||||
try (FileOutputStream out = new FileOutputStream(newFontFile)) {
|
||||
FileUtils.copy(fd, out.getFD());
|
||||
boolean success = false;
|
||||
try {
|
||||
File tempNewFontFile = new File(newDir, "font.ttf");
|
||||
try (FileOutputStream out = new FileOutputStream(tempNewFontFile)) {
|
||||
FileUtils.copy(fd, out.getFD());
|
||||
}
|
||||
// Do not parse font file before setting up fs-verity.
|
||||
// setUpFsverity throws IOException if failed.
|
||||
mFsverityUtil.setUpFsverity(tempNewFontFile.getAbsolutePath(), pkcs7Signature);
|
||||
String postScriptName = mParser.getPostScriptName(tempNewFontFile);
|
||||
File newFontFile = new File(newDir, postScriptName + ALLOWED_EXTENSION);
|
||||
if (!mFsverityUtil.rename(tempNewFontFile, newFontFile)) {
|
||||
// TODO: Define and return an error code for API
|
||||
throw new IOException("Failed to rename");
|
||||
}
|
||||
FontFileInfo fontFileInfo = validateFontFile(newFontFile);
|
||||
if (fontFileInfo == null) {
|
||||
// TODO: Define and return an error code for API
|
||||
throw new IllegalArgumentException("Invalid file");
|
||||
}
|
||||
if (!addFileToMapLocked(fontFileInfo, false)) {
|
||||
// TODO: Define and return an error code for API
|
||||
throw new IllegalArgumentException("Version downgrade");
|
||||
}
|
||||
success = true;
|
||||
} finally {
|
||||
if (!success) {
|
||||
FileUtils.deleteContentsAndDir(newDir);
|
||||
}
|
||||
}
|
||||
addFileToMapLocked(newFontFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,27 +202,110 @@ final class UpdatableFontDir {
|
||||
return dir;
|
||||
}
|
||||
|
||||
private void addFileToMapLocked(File file, boolean deleteOldFile) {
|
||||
final long version;
|
||||
/**
|
||||
* Add the given {@link FontFileInfo} to {@link #mFontFileInfoMap} if its font revision is
|
||||
* higher than the currently used font file (either in {@link #mFontFileInfoMap} or {@link
|
||||
* #mPreinstalledFontDirs}).
|
||||
*/
|
||||
private boolean addFileToMapLocked(FontFileInfo fontFileInfo, boolean deleteOldFile) {
|
||||
String name = fontFileInfo.getFile().getName();
|
||||
FontFileInfo existingInfo = mFontFileInfoMap.get(name);
|
||||
final boolean shouldAddToMap;
|
||||
if (existingInfo == null) {
|
||||
// We got a new updatable font. We need to check if it's newer than preinstalled fonts.
|
||||
// Note that getPreinstalledFontRevision() returns -1 if there is no preinstalled font
|
||||
// with 'name'.
|
||||
shouldAddToMap = getPreinstalledFontRevision(name) < fontFileInfo.getRevision();
|
||||
} else {
|
||||
shouldAddToMap = existingInfo.getRevision() < fontFileInfo.getRevision();
|
||||
}
|
||||
if (shouldAddToMap) {
|
||||
if (deleteOldFile && existingInfo != null) {
|
||||
FileUtils.deleteContentsAndDir(existingInfo.getRandomizedFontDir());
|
||||
}
|
||||
mFontFileInfoMap.put(name, fontFileInfo);
|
||||
return true;
|
||||
} else {
|
||||
if (deleteOldFile) {
|
||||
FileUtils.deleteContentsAndDir(fontFileInfo.getRandomizedFontDir());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private long getPreinstalledFontRevision(String name) {
|
||||
long maxRevision = -1;
|
||||
for (File dir : mPreinstalledFontDirs) {
|
||||
File preinstalledFontFile = new File(dir, name);
|
||||
if (!preinstalledFontFile.exists()) continue;
|
||||
long revision = getFontRevision(preinstalledFontFile);
|
||||
if (revision == -1) {
|
||||
Slog.w(TAG, "Invalid preinstalled font file");
|
||||
continue;
|
||||
}
|
||||
if (revision > maxRevision) {
|
||||
maxRevision = revision;
|
||||
}
|
||||
}
|
||||
return maxRevision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the fs-verity protection status of the given font file, validates the file name, and
|
||||
* returns a {@link FontFileInfo} on success. This method does not check if the font revision
|
||||
* is higher than the currently used font.
|
||||
*/
|
||||
@Nullable
|
||||
private FontFileInfo validateFontFile(File file) {
|
||||
if (!mFsverityUtil.hasFsverity(file.getAbsolutePath())) {
|
||||
Slog.w(TAG, "Font validation failed. Fs-verity is not enabled: " + file);
|
||||
return null;
|
||||
}
|
||||
if (!validateFontFileName(file)) {
|
||||
Slog.w(TAG, "Font validation failed. Could not validate font file name: " + file);
|
||||
return null;
|
||||
}
|
||||
long revision = getFontRevision(file);
|
||||
if (revision == -1) {
|
||||
Slog.w(TAG, "Font validation failed. Could not read font revision: " + file);
|
||||
return null;
|
||||
}
|
||||
return new FontFileInfo(file, revision);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the font file's file name matches with the PostScript name metadata in the
|
||||
* font file.
|
||||
*
|
||||
* <p>We check the font file names because apps use file name to look up fonts.
|
||||
* <p>Because PostScript name does not include extension, the extension is appended for
|
||||
* comparison. For example, if the file name is "NotoColorEmoji.ttf", the PostScript name should
|
||||
* be "NotoColorEmoji".
|
||||
*/
|
||||
private boolean validateFontFileName(File file) {
|
||||
String fileName = file.getName();
|
||||
String postScriptName = getPostScriptName(file);
|
||||
return (postScriptName + ALLOWED_EXTENSION).equals(fileName);
|
||||
}
|
||||
|
||||
/** Returns the PostScript name of the given font file, or null. */
|
||||
@Nullable
|
||||
private String getPostScriptName(File file) {
|
||||
try {
|
||||
version = mParser.getVersion(file);
|
||||
return mParser.getPostScriptName(file);
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Failed to read font file", e);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
if (version == -1) {
|
||||
Slog.e(TAG, "Invalid font file");
|
||||
return;
|
||||
}
|
||||
FontFileInfo info = mFontFileInfoMap.get(file.getName());
|
||||
if (info == null) {
|
||||
// TODO: check version of font in /system/fonts and /product/fonts
|
||||
mFontFileInfoMap.put(file.getName(), new FontFileInfo(file, version));
|
||||
} else if (info.mVersion < version) {
|
||||
if (deleteOldFile) {
|
||||
FileUtils.deleteContentsAndDir(info.mFile.getParentFile());
|
||||
}
|
||||
mFontFileInfoMap.put(file.getName(), new FontFileInfo(file, version));
|
||||
}
|
||||
|
||||
/** Returns the non-negative font revision of the given font file, or -1. */
|
||||
private long getFontRevision(File file) {
|
||||
try {
|
||||
return mParser.getRevision(file);
|
||||
} catch (IOException e) {
|
||||
Slog.e(TAG, "Failed to read font file", e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +313,7 @@ final class UpdatableFontDir {
|
||||
Map<String, File> map = new HashMap<>();
|
||||
synchronized (UpdatableFontDir.this) {
|
||||
for (Map.Entry<String, FontFileInfo> entry : mFontFileInfoMap.entrySet()) {
|
||||
map.put(entry.getKey(), entry.getValue().mFile);
|
||||
map.put(entry.getKey(), entry.getValue().getFile());
|
||||
}
|
||||
}
|
||||
return map;
|
||||
|
||||
@@ -60,8 +60,7 @@ public class FileIntegrityService extends SystemService {
|
||||
private final IBinder mService = new IFileIntegrityService.Stub() {
|
||||
@Override
|
||||
public boolean isApkVeritySupported() {
|
||||
return Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.R
|
||||
|| SystemProperties.getInt("ro.apk_verity.mode", 0) == 2;
|
||||
return FileIntegrityService.isApkVeritySupported();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,6 +110,11 @@ public class FileIntegrityService extends SystemService {
|
||||
}
|
||||
};
|
||||
|
||||
public static boolean isApkVeritySupported() {
|
||||
return Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.R
|
||||
|| SystemProperties.getInt("ro.apk_verity.mode", 0) == 2;
|
||||
}
|
||||
|
||||
public FileIntegrityService(final Context context) {
|
||||
super(context);
|
||||
try {
|
||||
|
||||
@@ -73,7 +73,12 @@ abstract public class VerityUtils {
|
||||
if (Files.size(Paths.get(signaturePath)) > MAX_SIGNATURE_FILE_SIZE_BYTES) {
|
||||
throw new SecurityException("Signature file is unexpectedly large: " + signaturePath);
|
||||
}
|
||||
byte[] pkcs7Signature = Files.readAllBytes(Paths.get(signaturePath));
|
||||
setUpFsverity(filePath, Files.readAllBytes(Paths.get(signaturePath)));
|
||||
}
|
||||
|
||||
/** Enables fs-verity for the file with a PKCS#7 detached signature bytes. */
|
||||
public static void setUpFsverity(@NonNull String filePath, @NonNull byte[] pkcs7Signature)
|
||||
throws IOException {
|
||||
// This will fail if the public key is not already in .fs-verity kernel keyring.
|
||||
int errno = enableFsverityNative(filePath, pkcs7Signature);
|
||||
if (errno != 0) {
|
||||
|
||||
@@ -19,6 +19,8 @@ package com.android.server.graphics.fonts;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.common.truth.Truth.assertWithMessage;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.FileUtils;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
@@ -35,7 +37,12 @@ import org.junit.runner.RunWith;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Presubmit
|
||||
@SmallTest
|
||||
@@ -44,17 +51,63 @@ public final class UpdatableFontDirTest {
|
||||
|
||||
/**
|
||||
* A {@link UpdatableFontDir.FontFileParser} for testing. Instead of using real font files,
|
||||
* this test uses fake font files. A fake font file has its version as its file content.
|
||||
* this test uses fake font files. A fake font file has its PostScript naem and revision as the
|
||||
* file content.
|
||||
*/
|
||||
private static class FakeFontFileParser implements UpdatableFontDir.FontFileParser {
|
||||
@Override
|
||||
public long getVersion(File file) throws IOException {
|
||||
return Long.parseLong(FileUtils.readTextFile(file, 100, ""));
|
||||
public String getPostScriptName(File file) throws IOException {
|
||||
String content = FileUtils.readTextFile(file, 100, "");
|
||||
return content.split(",")[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRevision(File file) throws IOException {
|
||||
String content = FileUtils.readTextFile(file, 100, "");
|
||||
return Long.parseLong(content.split(",")[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// FakeFsverityUtil will successfully set up fake fs-verity if the signature is GOOD_SIGNATURE.
|
||||
private static final String GOOD_SIGNATURE = "Good signature";
|
||||
|
||||
/** A fake FsverityUtil to keep fake verity bit in memory. */
|
||||
private static class FakeFsverityUtil implements UpdatableFontDir.FsverityUtil {
|
||||
private final Set<String> mHasFsverityPaths = new HashSet<>();
|
||||
|
||||
public void remove(String name) {
|
||||
mHasFsverityPaths.remove(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasFsverity(String path) {
|
||||
return mHasFsverityPaths.contains(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException {
|
||||
String fakeSignature = new String(pkcs7Signature, StandardCharsets.UTF_8);
|
||||
if (GOOD_SIGNATURE.equals(fakeSignature)) {
|
||||
mHasFsverityPaths.add(path);
|
||||
} else {
|
||||
throw new IOException("Failed to set up fake fs-verity");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rename(File src, File dest) {
|
||||
if (src.renameTo(dest)) {
|
||||
mHasFsverityPaths.remove(src.getAbsolutePath());
|
||||
mHasFsverityPaths.add(dest.getAbsolutePath());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private File mCacheDir;
|
||||
private File mUpdatableFontFilesDir;
|
||||
private List<File> mPreinstalledFontDirs;
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
@Before
|
||||
@@ -65,6 +118,12 @@ public final class UpdatableFontDirTest {
|
||||
mCacheDir.mkdirs();
|
||||
mUpdatableFontFilesDir = new File(mCacheDir, "updatable_fonts");
|
||||
mUpdatableFontFilesDir.mkdir();
|
||||
mPreinstalledFontDirs = new ArrayList<>();
|
||||
mPreinstalledFontDirs.add(new File(mCacheDir, "system_fonts"));
|
||||
mPreinstalledFontDirs.add(new File(mCacheDir, "product_fonts"));
|
||||
for (File dir : mPreinstalledFontDirs) {
|
||||
dir.mkdir();
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -75,19 +134,22 @@ public final class UpdatableFontDirTest {
|
||||
@Test
|
||||
public void construct() throws Exception {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
UpdatableFontDir dirForPreparation = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
|
||||
installFontFile(dirForPreparation, "foo.ttf", "1");
|
||||
installFontFile(dirForPreparation, "bar.ttf", "2");
|
||||
installFontFile(dirForPreparation, "foo.ttf", "3");
|
||||
installFontFile(dirForPreparation, "bar.ttf", "4");
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
UpdatableFontDir dirForPreparation = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "bar,4", GOOD_SIGNATURE);
|
||||
// Four font dirs are created.
|
||||
assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
|
||||
|
||||
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
assertThat(dir.getFontFileMap()).containsKey("foo.ttf");
|
||||
assertThat(parser.getVersion(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(3);
|
||||
assertThat(parser.getRevision(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(3);
|
||||
assertThat(dir.getFontFileMap()).containsKey("bar.ttf");
|
||||
assertThat(parser.getVersion(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(4);
|
||||
assertThat(parser.getRevision(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(4);
|
||||
// Outdated font dir should be deleted.
|
||||
assertThat(mUpdatableFontFilesDir.list()).hasLength(2);
|
||||
}
|
||||
@@ -95,66 +157,189 @@ public final class UpdatableFontDirTest {
|
||||
@Test
|
||||
public void construct_empty() {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
assertThat(dir.getFontFileMap()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void construct_missingFsverity() throws Exception {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
UpdatableFontDir dirForPreparation = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "bar,4", GOOD_SIGNATURE);
|
||||
// Four font dirs are created.
|
||||
assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
|
||||
|
||||
fakeFsverityUtil.remove(
|
||||
dirForPreparation.getFontFileMap().get("foo.ttf").getAbsolutePath());
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
assertThat(dir.getFontFileMap()).isEmpty();
|
||||
// All font dirs (including dir for "bar.ttf") should be deleted.
|
||||
assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void construct_fontNameMismatch() throws Exception {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
UpdatableFontDir dirForPreparation = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "bar,4", GOOD_SIGNATURE);
|
||||
// Four font dirs are created.
|
||||
assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
|
||||
|
||||
// Overwrite "foo.ttf" with wrong contents.
|
||||
FileUtils.stringToFile(dirForPreparation.getFontFileMap().get("foo.ttf"), "bar,4");
|
||||
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
assertThat(dir.getFontFileMap()).isEmpty();
|
||||
// All font dirs (including dir for "bar.ttf") should be deleted.
|
||||
assertThat(mUpdatableFontFilesDir.list()).hasLength(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void construct_olderThanPreinstalledFont() throws Exception {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
UpdatableFontDir dirForPreparation = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE);
|
||||
installFontFile(dirForPreparation, "bar,4", GOOD_SIGNATURE);
|
||||
// Four font dirs are created.
|
||||
assertThat(mUpdatableFontFilesDir.list()).hasLength(4);
|
||||
|
||||
// Add preinstalled fonts.
|
||||
FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "foo.ttf"), "foo,5");
|
||||
FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "bar.ttf"), "bar,1");
|
||||
FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(1), "bar.ttf"), "bar,2");
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
// For foo.ttf, preinstalled font (revision 5) should be used.
|
||||
assertThat(dir.getFontFileMap()).doesNotContainKey("foo.ttf");
|
||||
// For bar.ttf, updated font (revision 4) should be used.
|
||||
assertThat(dir.getFontFileMap()).containsKey("bar.ttf");
|
||||
assertThat(parser.getRevision(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(4);
|
||||
// Outdated font dir should be deleted.
|
||||
// We don't delete bar.ttf in this case, because it's normal that OTA updates preinstalled
|
||||
// fonts.
|
||||
assertThat(mUpdatableFontFilesDir.list()).hasLength(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void installFontFile() throws Exception {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
|
||||
installFontFile(dir, "test.ttf", "1");
|
||||
installFontFile(dir, "test,1", GOOD_SIGNATURE);
|
||||
assertThat(dir.getFontFileMap()).containsKey("test.ttf");
|
||||
assertThat(parser.getVersion(dir.getFontFileMap().get("test.ttf"))).isEqualTo(1);
|
||||
assertThat(parser.getRevision(dir.getFontFileMap().get("test.ttf"))).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void installFontFile_upgrade() throws Exception {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
|
||||
installFontFile(dir, "test.ttf", "1");
|
||||
installFontFile(dir, "test,1", GOOD_SIGNATURE);
|
||||
Map<String, File> mapBeforeUpgrade = dir.getFontFileMap();
|
||||
installFontFile(dir, "test.ttf", "2");
|
||||
installFontFile(dir, "test,2", GOOD_SIGNATURE);
|
||||
assertThat(dir.getFontFileMap()).containsKey("test.ttf");
|
||||
assertThat(parser.getVersion(dir.getFontFileMap().get("test.ttf"))).isEqualTo(2);
|
||||
assertThat(parser.getRevision(dir.getFontFileMap().get("test.ttf"))).isEqualTo(2);
|
||||
assertThat(mapBeforeUpgrade).containsKey("test.ttf");
|
||||
assertWithMessage("Older fonts should not be deleted until next loadFontFileMap")
|
||||
.that(parser.getVersion(mapBeforeUpgrade.get("test.ttf"))).isEqualTo(1);
|
||||
.that(parser.getRevision(mapBeforeUpgrade.get("test.ttf"))).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void installFontFile_downgrade() throws Exception {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
|
||||
installFontFile(dir, "test.ttf", "2");
|
||||
installFontFile(dir, "test.ttf", "1");
|
||||
installFontFile(dir, "test,2", GOOD_SIGNATURE);
|
||||
try {
|
||||
installFontFile(dir, "test,1", GOOD_SIGNATURE);
|
||||
fail("Expect IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Expect
|
||||
}
|
||||
assertThat(dir.getFontFileMap()).containsKey("test.ttf");
|
||||
assertWithMessage("Font should not be downgraded to an older version")
|
||||
.that(parser.getVersion(dir.getFontFileMap().get("test.ttf"))).isEqualTo(2);
|
||||
assertWithMessage("Font should not be downgraded to an older revision")
|
||||
.that(parser.getRevision(dir.getFontFileMap().get("test.ttf"))).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void installFontFile_multiple() throws Exception {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser);
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
|
||||
installFontFile(dir, "foo.ttf", "1");
|
||||
installFontFile(dir, "bar.ttf", "2");
|
||||
installFontFile(dir, "foo,1", GOOD_SIGNATURE);
|
||||
installFontFile(dir, "bar,2", GOOD_SIGNATURE);
|
||||
assertThat(dir.getFontFileMap()).containsKey("foo.ttf");
|
||||
assertThat(parser.getVersion(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(1);
|
||||
assertThat(parser.getRevision(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(1);
|
||||
assertThat(dir.getFontFileMap()).containsKey("bar.ttf");
|
||||
assertThat(parser.getVersion(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(2);
|
||||
assertThat(parser.getRevision(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(2);
|
||||
}
|
||||
|
||||
private void installFontFile(UpdatableFontDir dir, String name, String content)
|
||||
@Test
|
||||
public void installFontFile_invalidSignature() {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
|
||||
try {
|
||||
installFontFile(dir, "test,1", "Invalid signature");
|
||||
fail("Expect IOException");
|
||||
} catch (IOException e) {
|
||||
// Expect
|
||||
}
|
||||
assertThat(dir.getFontFileMap()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void installFontFile_olderThanPreinstalledFont() throws Exception {
|
||||
FakeFontFileParser parser = new FakeFontFileParser();
|
||||
FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil();
|
||||
FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"), "test,1");
|
||||
UpdatableFontDir dir = new UpdatableFontDir(
|
||||
mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil);
|
||||
|
||||
try {
|
||||
installFontFile(dir, "test,1", GOOD_SIGNATURE);
|
||||
fail("Expect IllegalArgumentException");
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Expect
|
||||
}
|
||||
assertThat(dir.getFontFileMap()).isEmpty();
|
||||
}
|
||||
|
||||
private void installFontFile(UpdatableFontDir dir, String content, String signature)
|
||||
throws IOException {
|
||||
File file = File.createTempFile(name, "", mCacheDir);
|
||||
File file = File.createTempFile("font", "ttf", mCacheDir);
|
||||
FileUtils.stringToFile(file, content);
|
||||
try (FileInputStream in = new FileInputStream(file)) {
|
||||
dir.installFontFile(name, in.getFD());
|
||||
dir.installFontFile(in.getFD(), signature.getBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user