Enable obfuscation of resource names, with whitelisting support.
Test: Built aapt2, ran optimize on gmail apk with sample whitelist
config file, and flags enabled. Added two unit tests to TableFlattener
covering obfuscation logic.
Change-Id: Iad6329d75ff440121bf1a2cdf09c5f4bf4199d9d
diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp
index 2bf91a5..3b90637 100644
--- a/tools/aapt2/cmd/Optimize.cpp
+++ b/tools/aapt2/cmd/Optimize.cpp
@@ -17,6 +17,7 @@
#include <memory>
#include <vector>
+#include "android-base/file.h"
#include "android-base/stringprintf.h"
#include "androidfw/ResourceTypes.h"
@@ -47,6 +48,7 @@
using ::aapt::configuration::PostProcessingConfiguration;
using ::android::ResTable_config;
using ::android::StringPiece;
+using ::android::base::ReadFileToString;
using ::android::base::StringAppendF;
using ::android::base::StringPrintf;
@@ -279,6 +281,20 @@
OptimizeContext* context_;
};
+bool ExtractWhitelistFromConfig(const std::string& path, OptimizeContext* context,
+ OptimizeOptions* options) {
+ std::string contents;
+ if (!ReadFileToString(path, &contents, true)) {
+ context->GetDiagnostics()->Error(DiagMessage()
+ << "failed to parse whitelist from config file: " << path);
+ return false;
+ }
+ for (const StringPiece& resource_name : util::Tokenize(contents, ',')) {
+ options->table_flattener_options.whitelisted_resources.insert(resource_name.to_string());
+ }
+ return true;
+}
+
bool ExtractAppDataFromManifest(OptimizeContext* context, const LoadedApk* apk,
OptimizeOptions* out_options) {
const xml::XmlResource* manifest = apk->GetManifest();
@@ -302,6 +318,7 @@
OptimizeContext context;
OptimizeOptions options;
Maybe<std::string> config_path;
+ Maybe<std::string> whitelist_path;
Maybe<std::string> target_densities;
Maybe<std::string> target_abis;
std::vector<std::string> configs;
@@ -320,6 +337,10 @@
"All the resources that would be unused on devices of the given densities will be \n"
"removed from the APK.",
&target_densities)
+ .OptionalFlag("--whitelist-config-path",
+ "Path to the whitelist.cfg file containing whitelisted resources \n"
+ "whose names should not be altered in final resource tables.",
+ &whitelist_path)
.OptionalFlag(
"--target-abis",
"Comma separated list of the CPU ABIs that the APK will be optimized for.\n"
@@ -339,6 +360,9 @@
"Enables encoding sparse entries using a binary search tree.\n"
"This decreases APK size at the cost of resource retrieval performance.",
&options.table_flattener_options.use_sparse_entries)
+ .OptionalSwitch("--enable-resource-obfuscation",
+ "Enables obfuscation of key string pool to single value",
+ &options.table_flattener_options.collapse_key_stringpool)
.OptionalSwitch("-v", "Enables verbose logging", &verbose);
if (!flags.Parse("aapt2 optimize", args, &std::cerr)) {
@@ -425,6 +449,15 @@
return 1;
}
+ if (options.table_flattener_options.collapse_key_stringpool) {
+ if (whitelist_path) {
+ std::string& path = whitelist_path.value();
+ if (!ExtractWhitelistFromConfig(path, &context, &options)) {
+ return 1;
+ }
+ }
+ }
+
if (!ExtractAppDataFromManifest(&context, apk.get(), &options)) {
return 1;
}
diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp
index 2a51df3..a3034df 100644
--- a/tools/aapt2/format/binary/TableFlattener.cpp
+++ b/tools/aapt2/format/binary/TableFlattener.cpp
@@ -220,12 +220,15 @@
class PackageFlattener {
public:
PackageFlattener(IAaptContext* context, ResourceTablePackage* package,
- const std::map<size_t, std::string>* shared_libs, bool use_sparse_entries)
+ const std::map<size_t, std::string>* shared_libs, bool use_sparse_entries,
+ bool collapse_key_stringpool, const std::set<std::string>& whitelisted_resources)
: context_(context),
diag_(context->GetDiagnostics()),
package_(package),
shared_libs_(shared_libs),
- use_sparse_entries_(use_sparse_entries) {
+ use_sparse_entries_(use_sparse_entries),
+ collapse_key_stringpool_(collapse_key_stringpool),
+ whitelisted_resources_(whitelisted_resources) {
}
bool FlattenPackage(BigBuffer* buffer) {
@@ -494,13 +497,23 @@
// configuration available. Here we reverse this to match the binary
// table.
std::map<ConfigDescription, std::vector<FlatEntry>> config_to_entry_list_map;
- for (ResourceEntry* entry : sorted_entries) {
- const uint32_t key_index = (uint32_t)key_pool_.MakeRef(entry->name).index();
+ // hardcoded string uses characters which make it an invalid resource name
+ const std::string obfuscated_resource_name = "0_resource_name_obfuscated";
+
+ for (ResourceEntry* entry : sorted_entries) {
+ uint32_t local_key_index;
+ if (!collapse_key_stringpool_ ||
+ whitelisted_resources_.find(entry->name) != whitelisted_resources_.end()) {
+ local_key_index = (uint32_t)key_pool_.MakeRef(entry->name).index();
+ } else {
+ // resource isn't whitelisted, add it as obfuscated value
+ local_key_index = (uint32_t)key_pool_.MakeRef(obfuscated_resource_name).index();
+ }
// Group values by configuration.
for (auto& config_value : entry->values) {
config_to_entry_list_map[config_value->config].push_back(
- FlatEntry{entry, config_value->value.get(), key_index});
+ FlatEntry{entry, config_value->value.get(), local_key_index});
}
}
@@ -549,6 +562,8 @@
bool use_sparse_entries_;
StringPool type_pool_;
StringPool key_pool_;
+ bool collapse_key_stringpool_;
+ const std::set<std::string>& whitelisted_resources_;
};
} // namespace
@@ -593,7 +608,8 @@
}
PackageFlattener flattener(context, package.get(), &table->included_packages_,
- options_.use_sparse_entries);
+ options_.use_sparse_entries, options_.collapse_key_stringpool,
+ options_.whitelisted_resources);
if (!flattener.FlattenPackage(&package_buffer)) {
return false;
}
diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h
index 88cbddf..c2e1d4b 100644
--- a/tools/aapt2/format/binary/TableFlattener.h
+++ b/tools/aapt2/format/binary/TableFlattener.h
@@ -35,6 +35,14 @@
// This is only available on platforms O+ and will only be respected when
// minSdk is O+.
bool use_sparse_entries = false;
+
+ // When true, the key string pool in the final ResTable
+ // is collapsed to a single entry. All resource entries
+ // have name indices that point to this single value
+ bool collapse_key_stringpool = false;
+
+ // Set of whitelisted resource names to avoid altering in key stringpool
+ std::set<std::string> whitelisted_resources;
};
class TableFlattener : public IResourceTableConsumer {
diff --git a/tools/aapt2/format/binary/TableFlattener_test.cpp b/tools/aapt2/format/binary/TableFlattener_test.cpp
index e11890b..f0b80d2 100644
--- a/tools/aapt2/format/binary/TableFlattener_test.cpp
+++ b/tools/aapt2/format/binary/TableFlattener_test.cpp
@@ -127,6 +127,15 @@
<< StringPiece16(actual_name.name, actual_name.nameLen) << "'";
}
+ ResourceName actual_res_name(resName.value());
+
+ if (expected_res_name.entry != actual_res_name.entry ||
+ expected_res_name.package != actual_res_name.package ||
+ expected_res_name.type != actual_res_name.type) {
+ return ::testing::AssertionFailure() << "expected resource '" << expected_res_name.to_string()
+ << "' but got '" << actual_res_name.to_string() << "'";
+ }
+
if (expected_config != config) {
return ::testing::AssertionFailure() << "expected config '" << expected_config
<< "' but got '" << ConfigDescription(config) << "'";
@@ -450,4 +459,113 @@
ASSERT_FALSE(Flatten(context.get(), {}, table.get(), &result));
}
+TEST_F(TableFlattenerTest, ObfuscatingResourceNamesNoWhitelistSucceeds) {
+ std::unique_ptr<ResourceTable> table =
+ test::ResourceTableBuilder()
+ .SetPackageId("com.app.test", 0x7f)
+ .AddSimple("com.app.test:id/one", ResourceId(0x7f020000))
+ .AddSimple("com.app.test:id/two", ResourceId(0x7f020001))
+ .AddValue("com.app.test:id/three", ResourceId(0x7f020002),
+ test::BuildReference("com.app.test:id/one", ResourceId(0x7f020000)))
+ .AddValue("com.app.test:integer/one", ResourceId(0x7f030000),
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u))
+ .AddValue("com.app.test:integer/one", test::ParseConfigOrDie("v1"),
+ ResourceId(0x7f030000),
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u))
+ .AddString("com.app.test:string/test", ResourceId(0x7f040000), "foo")
+ .AddString("com.app.test:layout/bar", ResourceId(0x7f050000), "res/layout/bar.xml")
+ .Build();
+
+ TableFlattenerOptions options;
+ options.collapse_key_stringpool = true;
+
+ ResTable res_table;
+
+ ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+ ResourceId(0x7f020000), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+ ResourceId(0x7f020001), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+ ResourceId(0x7f020002), {}, Res_value::TYPE_REFERENCE, 0x7f020000u, 0u));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+ ResourceId(0x7f030000), {}, Res_value::TYPE_INT_DEC, 1u,
+ ResTable_config::CONFIG_VERSION));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+ ResourceId(0x7f030000), test::ParseConfigOrDie("v1"), Res_value::TYPE_INT_DEC,
+ 2u, ResTable_config::CONFIG_VERSION));
+
+ std::u16string foo_str = u"foo";
+ ssize_t idx = res_table.getTableStringBlock(0)->indexOfString(foo_str.data(), foo_str.size());
+ ASSERT_GE(idx, 0);
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:string/0_resource_name_obfuscated",
+ ResourceId(0x7f040000), {}, Res_value::TYPE_STRING, (uint32_t)idx, 0u));
+
+ std::u16string bar_path = u"res/layout/bar.xml";
+ idx = res_table.getTableStringBlock(0)->indexOfString(bar_path.data(), bar_path.size());
+ ASSERT_GE(idx, 0);
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:layout/0_resource_name_obfuscated",
+ ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)idx, 0u));
+}
+
+TEST_F(TableFlattenerTest, ObfuscatingResourceNamesWithWhitelistSucceeds) {
+ std::unique_ptr<ResourceTable> table =
+ test::ResourceTableBuilder()
+ .SetPackageId("com.app.test", 0x7f)
+ .AddSimple("com.app.test:id/one", ResourceId(0x7f020000))
+ .AddSimple("com.app.test:id/two", ResourceId(0x7f020001))
+ .AddValue("com.app.test:id/three", ResourceId(0x7f020002),
+ test::BuildReference("com.app.test:id/one", ResourceId(0x7f020000)))
+ .AddValue("com.app.test:integer/one", ResourceId(0x7f030000),
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u))
+ .AddValue("com.app.test:integer/one", test::ParseConfigOrDie("v1"),
+ ResourceId(0x7f030000),
+ util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u))
+ .AddString("com.app.test:string/test", ResourceId(0x7f040000), "foo")
+ .AddString("com.app.test:layout/bar", ResourceId(0x7f050000), "res/layout/bar.xml")
+ .Build();
+
+ TableFlattenerOptions options;
+ options.collapse_key_stringpool = true;
+ options.whitelisted_resources.insert("test");
+ options.whitelisted_resources.insert("three");
+ ResTable res_table;
+
+ ASSERT_TRUE(Flatten(context_.get(), options, table.get(), &res_table));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+ ResourceId(0x7f020000), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/0_resource_name_obfuscated",
+ ResourceId(0x7f020001), {}, Res_value::TYPE_INT_BOOLEAN, 0u, 0u));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:id/three", ResourceId(0x7f020002), {},
+ Res_value::TYPE_REFERENCE, 0x7f020000u, 0u));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+ ResourceId(0x7f030000), {}, Res_value::TYPE_INT_DEC, 1u,
+ ResTable_config::CONFIG_VERSION));
+
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:integer/0_resource_name_obfuscated",
+ ResourceId(0x7f030000), test::ParseConfigOrDie("v1"), Res_value::TYPE_INT_DEC,
+ 2u, ResTable_config::CONFIG_VERSION));
+
+ std::u16string foo_str = u"foo";
+ ssize_t idx = res_table.getTableStringBlock(0)->indexOfString(foo_str.data(), foo_str.size());
+ ASSERT_GE(idx, 0);
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:string/test", ResourceId(0x7f040000), {},
+ Res_value::TYPE_STRING, (uint32_t)idx, 0u));
+
+ std::u16string bar_path = u"res/layout/bar.xml";
+ idx = res_table.getTableStringBlock(0)->indexOfString(bar_path.data(), bar_path.size());
+ ASSERT_GE(idx, 0);
+ EXPECT_TRUE(Exists(&res_table, "com.app.test:layout/0_resource_name_obfuscated",
+ ResourceId(0x7f050000), {}, Res_value::TYPE_STRING, (uint32_t)idx, 0u));
+}
+
} // namespace aapt
diff --git a/tools/aapt2/text/Unicode_test.cpp b/tools/aapt2/text/Unicode_test.cpp
index a8e797c..16bc2e8 100644
--- a/tools/aapt2/text/Unicode_test.cpp
+++ b/tools/aapt2/text/Unicode_test.cpp
@@ -63,6 +63,7 @@
EXPECT_FALSE(IsValidResourceEntryName("Føø/Bar"));
EXPECT_FALSE(IsValidResourceEntryName("Føø:Bar"));
EXPECT_FALSE(IsValidResourceEntryName("Føø;Bar"));
+ EXPECT_FALSE(IsValidResourceEntryName("0_resource_name_obfuscated"));
}
} // namespace text