diff options
author | 2018-11-07 16:49:02 -0500 | |
---|---|---|
committer | 2018-12-20 18:19:25 -0500 | |
commit | c76940363197de1772b761aa38e819b55fb80cb7 (patch) | |
tree | 28c9afa0e073be59b396dbe8aebc6a118deca382 | |
parent | 9da2ff0fdc6af2153c12701ae8344f10f9a26413 (diff) |
Resource Path Obfuscation
This CL allows aapt2 to obfuscate resource paths within the output apk
and move resources to shorter obfuscated paths. This reduces apk size
when there is a large number of resources since the path metadata exists
in 4 places in the apk.
This CL adds two arguments to aapt2, one to enable resource path
obfuscation and one to point to a path to output the path map to (for
later debugging).
Test: make aapt2_tests
Bug: b/75965637
Change-Id: I9cacafe1d17800d673566b2d61b0b88f3fb8d60c
-rw-r--r-- | tools/aapt2/Android.bp | 1 | ||||
-rw-r--r-- | tools/aapt2/LoadedApk.cpp | 14 | ||||
-rw-r--r-- | tools/aapt2/cmd/Optimize.cpp | 26 | ||||
-rw-r--r-- | tools/aapt2/cmd/Optimize.h | 17 | ||||
-rw-r--r-- | tools/aapt2/format/binary/TableFlattener.h | 3 | ||||
-rw-r--r-- | tools/aapt2/optimize/ResourcePathShortener.cpp | 112 | ||||
-rw-r--r-- | tools/aapt2/optimize/ResourcePathShortener.h | 44 | ||||
-rw-r--r-- | tools/aapt2/optimize/ResourcePathShortener_test.cpp | 67 |
8 files changed, 281 insertions, 3 deletions
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index ba498e19f837..a99cfe62c198 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -114,6 +114,7 @@ cc_library_host_static { "optimize/MultiApkGenerator.cpp", "optimize/ResourceDeduper.cpp", "optimize/ResourceFilter.cpp", + "optimize/ResourcePathShortener.cpp", "optimize/VersionCollapser.cpp", "process/SymbolTable.cpp", "split/TableSplitter.cpp", diff --git a/tools/aapt2/LoadedApk.cpp b/tools/aapt2/LoadedApk.cpp index b353ff00a23f..45719ef474cd 100644 --- a/tools/aapt2/LoadedApk.cpp +++ b/tools/aapt2/LoadedApk.cpp @@ -223,8 +223,17 @@ bool LoadedApk::WriteToArchive(IAaptContext* context, ResourceTable* split_table io::IFile* file = iterator->Next(); std::string path = file->GetSource().path; + std::string output_path = path; + bool is_resource = path.find("res/") == 0; + if (is_resource) { + auto it = options.shortened_path_map.find(path); + if (it != options.shortened_path_map.end()) { + output_path = it->second; + } + } + // Skip resources that are not referenced if requested. - if (path.find("res/") == 0 && referenced_resources.find(path) == referenced_resources.end()) { + if (is_resource && referenced_resources.find(output_path) == referenced_resources.end()) { if (context->IsVerbose()) { context->GetDiagnostics()->Note(DiagMessage() << "Removing resource '" << path << "' from APK."); @@ -283,7 +292,8 @@ bool LoadedApk::WriteToArchive(IAaptContext* context, ResourceTable* split_table return false; } } else { - if (!io::CopyFileToArchivePreserveCompression(context, file, path, writer)) { + if (!io::CopyFileToArchivePreserveCompression( + context, file, output_path, writer)) { return false; } } diff --git a/tools/aapt2/cmd/Optimize.cpp b/tools/aapt2/cmd/Optimize.cpp index 328b0beda372..2e6af18c1948 100644 --- a/tools/aapt2/cmd/Optimize.cpp +++ b/tools/aapt2/cmd/Optimize.cpp @@ -41,6 +41,7 @@ #include "optimize/MultiApkGenerator.h" #include "optimize/ResourceDeduper.h" #include "optimize/ResourceFilter.h" +#include "optimize/ResourcePathShortener.h" #include "optimize/VersionCollapser.h" #include "split/TableSplitter.h" #include "util/Files.h" @@ -52,6 +53,7 @@ using ::android::ConfigDescription; using ::android::ResTable_config; using ::android::StringPiece; using ::android::base::ReadFileToString; +using ::android::base::WriteStringToFile; using ::android::base::StringAppendF; using ::android::base::StringPrintf; @@ -143,6 +145,21 @@ class Optimizer { return 1; } + if (options_.shorten_resource_paths) { + ResourcePathShortener shortener(options_.table_flattener_options.shortened_path_map); + if (!shortener.Consume(context_, apk->GetResourceTable())) { + context_->GetDiagnostics()->Error(DiagMessage() << "failed shortening resource paths"); + return 1; + } + if (options_.shortened_paths_map_path + && !WriteShortenedPathsMap(options_.table_flattener_options.shortened_path_map, + options_.shortened_paths_map_path.value())) { + context_->GetDiagnostics()->Error(DiagMessage() + << "failed to write shortened resource paths to file"); + return 1; + } + } + // Adjust the SplitConstraints so that their SDK version is stripped if it is less than or // equal to the minSdk. options_.split_constraints = @@ -264,6 +281,15 @@ class Optimizer { ArchiveEntry::kAlign, writer); } + bool WriteShortenedPathsMap(const std::map<std::string, std::string> &path_map, + const std::string &file_path) { + std::stringstream ss; + for (auto it = path_map.cbegin(); it != path_map.cend(); ++it) { + ss << it->first << " -> " << it->second << "\n"; + } + return WriteStringToFile(ss.str(), file_path); + } + OptimizeOptions options_; OptimizeContext* context_; }; diff --git a/tools/aapt2/cmd/Optimize.h b/tools/aapt2/cmd/Optimize.h index 43bc216382fa..7df9cf788cde 100644 --- a/tools/aapt2/cmd/Optimize.h +++ b/tools/aapt2/cmd/Optimize.h @@ -55,6 +55,12 @@ struct OptimizeOptions { // Set of artifacts to keep when generating multi-APK splits. If the list is empty, all artifacts // are kept and will be written as output. std::unordered_set<std::string> kept_artifacts; + + // Whether or not to shorten resource paths in the APK. + bool shorten_resource_paths; + + // Path to the output map of original resource paths to shortened paths. + Maybe<std::string> shortened_paths_map_path; }; class OptimizeCommand : public Command { @@ -100,6 +106,12 @@ class OptimizeCommand : public Command { AddOptionalSwitch("--enable-resource-obfuscation", "Enables obfuscation of key string pool to single value", &options_.table_flattener_options.collapse_key_stringpool); + AddOptionalSwitch("--enable-resource-path-shortening", + "Enables shortening of the path of the resources inside the APK.", + &options_.shorten_resource_paths); + AddOptionalFlag("--resource-path-shortening-map", + "Path to output the map of old resource paths to shortened paths.", + &options_.shortened_paths_map_path); AddOptionalSwitch("-v", "Enables verbose logging", &verbose_); } @@ -108,6 +120,9 @@ class OptimizeCommand : public Command { private: OptimizeOptions options_; + bool WriteObfuscatedPathsMap(const std::map<std::string, std::string> &path_map, + const std::string &file_path); + Maybe<std::string> config_path_; Maybe<std::string> whitelist_path_; Maybe<std::string> resources_config_path_; @@ -121,4 +136,4 @@ class OptimizeCommand : public Command { }// namespace aapt -#endif //AAPT2_OPTIMIZE_H
\ No newline at end of file +#endif //AAPT2_OPTIMIZE_H diff --git a/tools/aapt2/format/binary/TableFlattener.h b/tools/aapt2/format/binary/TableFlattener.h index 635cb21f514c..71330e3fb74f 100644 --- a/tools/aapt2/format/binary/TableFlattener.h +++ b/tools/aapt2/format/binary/TableFlattener.h @@ -46,6 +46,9 @@ struct TableFlattenerOptions { // When true, sort the entries in the values string pool by priority and configuration. bool sort_stringpool_entries = true; + + // Map from original resource paths to shortened resource paths. + std::map<std::string, std::string> shortened_path_map; }; class TableFlattener : public IResourceTableConsumer { diff --git a/tools/aapt2/optimize/ResourcePathShortener.cpp b/tools/aapt2/optimize/ResourcePathShortener.cpp new file mode 100644 index 000000000000..c5df3dd00db9 --- /dev/null +++ b/tools/aapt2/optimize/ResourcePathShortener.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "optimize/ResourcePathShortener.h" + +#include <math.h> +#include <unordered_set> + +#include "androidfw/StringPiece.h" + +#include "ResourceTable.h" +#include "ValueVisitor.h" + + +static const std::string base64_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789-_"; + +namespace aapt { + +ResourcePathShortener::ResourcePathShortener( + std::map<std::string, std::string>& path_map_out) + : path_map_(path_map_out) { +} + +std::string ShortenFileName(const android::StringPiece& file_path, int output_length) { + std::size_t hash_num = std::hash<android::StringPiece>{}(file_path); + std::string result = ""; + // Convert to (modified) base64 so that it is a proper file path. + for (int i = 0; i < output_length; i++) { + uint8_t sextet = hash_num & 0x3f; + hash_num >>= 6; + result += base64_chars[sextet]; + } + return result; +} + + +// Calculate the optimal hash length such that an average of 10% of resources +// collide in their shortened path. +// Reference: http://matt.might.net/articles/counting-hash-collisions/ +int OptimalShortenedLength(int num_resources) { + int num_chars = 2; + double N = 64*64; // hash space when hash is 2 chars long + double max_collisions = num_resources * 0.1; + while (num_resources - N + N * pow((N - 1) / N, num_resources) > max_collisions) { + N *= 64; + num_chars++; + } + return num_chars; +} + +std::string GetShortenedPath(const android::StringPiece& shortened_filename, + const android::StringPiece& extension, int collision_count) { + std::string shortened_path = "res/" + shortened_filename.to_string(); + if (collision_count > 0) { + shortened_path += std::to_string(collision_count); + } + shortened_path += extension; + return shortened_path; +} + +bool ResourcePathShortener::Consume(IAaptContext* context, ResourceTable* table) { + // used to detect collisions + std::unordered_set<std::string> shortened_paths; + std::unordered_set<FileReference*> file_refs; + for (auto& package : table->packages) { + for (auto& type : package->types) { + for (auto& entry : type->entries) { + for (auto& config_value : entry->values) { + FileReference* file_ref = ValueCast<FileReference>(config_value->value.get()); + if (file_ref) { + file_refs.insert(file_ref); + } + } + } + } + } + int num_chars = OptimalShortenedLength(file_refs.size()); + for (auto& file_ref : file_refs) { + android::StringPiece res_subdir, actual_filename, extension; + util::ExtractResFilePathParts(*file_ref->path, &res_subdir, &actual_filename, &extension); + + std::string shortened_filename = ShortenFileName(*file_ref->path, num_chars); + int collision_count = 0; + std::string shortened_path = GetShortenedPath(shortened_filename, extension, collision_count); + while (shortened_paths.find(shortened_path) != shortened_paths.end()) { + collision_count++; + shortened_path = GetShortenedPath(shortened_filename, extension, collision_count); + } + shortened_paths.insert(shortened_path); + path_map_.insert({*file_ref->path, shortened_path}); + file_ref->path = table->string_pool.MakeRef(shortened_path, file_ref->path.GetContext()); + } + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/optimize/ResourcePathShortener.h b/tools/aapt2/optimize/ResourcePathShortener.h new file mode 100644 index 000000000000..f1074ef083bd --- /dev/null +++ b/tools/aapt2/optimize/ResourcePathShortener.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_OPTIMIZE_RESOURCEPATHSHORTENER_H +#define AAPT_OPTIMIZE_RESOURCEPATHSHORTENER_H + +#include <map> + +#include "android-base/macros.h" + +#include "process/IResourceTableConsumer.h" + +namespace aapt { + +class ResourceTable; + +// Maps resources in the apk to shortened paths. +class ResourcePathShortener : public IResourceTableConsumer { + public: + explicit ResourcePathShortener(std::map<std::string, std::string>& path_map_out); + + bool Consume(IAaptContext* context, ResourceTable* table) override; + + private: + DISALLOW_COPY_AND_ASSIGN(ResourcePathShortener); + std::map<std::string, std::string>& path_map_; +}; + +} // namespace aapt + +#endif // AAPT_OPTIMIZE_RESOURCEPATHSHORTENER_H diff --git a/tools/aapt2/optimize/ResourcePathShortener_test.cpp b/tools/aapt2/optimize/ResourcePathShortener_test.cpp new file mode 100644 index 000000000000..88cadc76c336 --- /dev/null +++ b/tools/aapt2/optimize/ResourcePathShortener_test.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "optimize/ResourcePathShortener.h" + +#include "ResourceTable.h" +#include "test/Test.h" + +using ::aapt::test::GetValue; +using ::testing::Not; +using ::testing::NotNull; +using ::testing::Eq; + +namespace aapt { + +TEST(ResourcePathShortenerTest, FileRefPathsChangedInResourceTable) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder() + .AddFileReference("android:drawable/xmlfile", "res/drawables/xmlfile.xml") + .AddFileReference("android:drawable/xmlfile2", "res/drawables/xmlfile2.xml") + .AddString("android:string/string", "res/should/still/be/the/same.png") + .Build(); + + std::map<std::string, std::string> path_map; + ASSERT_TRUE(ResourcePathShortener(path_map).Consume(context.get(), table.get())); + + // Expect that the path map is populated + ASSERT_THAT(path_map.find("res/drawables/xmlfile.xml"), Not(Eq(path_map.end()))); + ASSERT_THAT(path_map.find("res/drawables/xmlfile2.xml"), Not(Eq(path_map.end()))); + + // The file paths were changed + EXPECT_THAT(path_map.at("res/drawables/xmlfile.xml"), Not(Eq("res/drawables/xmlfile.xml"))); + EXPECT_THAT(path_map.at("res/drawables/xmlfile2.xml"), Not(Eq("res/drawables/xmlfile2.xml"))); + + // Different file paths should remain different + EXPECT_THAT(path_map["res/drawables/xmlfile.xml"], + Not(Eq(path_map["res/drawables/xmlfile2.xml"]))); + + FileReference* ref = + GetValue<FileReference>(table.get(), "android:drawable/xmlfile"); + ASSERT_THAT(ref, NotNull()); + // The map correctly points to the new location of the file + EXPECT_THAT(path_map["res/drawables/xmlfile.xml"], Eq(*ref->path)); + + // Strings should not be affected, only file paths + EXPECT_THAT( + *GetValue<String>(table.get(), "android:string/string")->value, + Eq("res/should/still/be/the/same.png")); + EXPECT_THAT(path_map.find("res/should/still/be/the/same.png"), Eq(path_map.end())); +} + +} // namespace aapt |