Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into jmatsu/rollup/build…
Browse files Browse the repository at this point in the history
…tools-version
  • Loading branch information
jmatsu committed Nov 7, 2023
2 parents 39d74f5 + b643747 commit ac7e5ba
Show file tree
Hide file tree
Showing 252 changed files with 7,257 additions and 401 deletions.
8 changes: 8 additions & 0 deletions lib/android_apk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ class AndroidApk

DEFAULT_RESOURCE_CONFIG = "(default)" # very special config

class << self
attr_writer :logger

def logger
@logger ||= Logger.new($stdout)
end
end

# Dump result which was parsed manually
# @deprecated don't expose this field
# @return [Hash] Return a parsed result of aapt dump
Expand Down
2 changes: 1 addition & 1 deletion lib/android_apk/apksigner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ module ApkSigner
# @param min_sdk_version [String, Integer] min sdk version to verify
# @param max_sdk_version [String, Integer] max sdk version to verify
# @return [Hash<String => Array<String>>] a map of lines for each target sdk version range
module_function def verify(filepath:, min_sdk_version:, max_sdk_version:)
module_function def print_certs(filepath:, min_sdk_version:, max_sdk_version:)
# Don't add -v because it will print pub keys too.
args = [
"apksigner",
Expand Down
2 changes: 1 addition & 1 deletion lib/android_apk/app_signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def rotated?
end

private def merge_fingerprints(fingerprints:)
merged_fingerprints = fingerprints.each_with_object([]) do |fingerprint, acc|
merged_fingerprints = fingerprints.sort_by { |f| f.fetch("min_sdk_version") }.each_with_object([]) do |fingerprint, acc|
# SKip unsigned span
next if fingerprint[::AndroidApk::SignatureDigest::SHA256].nil?

Expand Down
23 changes: 14 additions & 9 deletions lib/android_apk/resource_finder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class << self
# @param apk_filepath [String] apk file path
# @param default_icon_path [String, NilClass]
# @return [Hash] keys are dpi human readable names, values are png file paths that are relative
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def resolve_icons_in_arsc(apk_filepath:, default_icon_path:)
return {} if default_icon_path.nil? || default_icon_path.empty?

Expand Down Expand Up @@ -39,7 +40,6 @@ def resolve_icons_in_arsc(apk_filepath:, default_icon_path:)

# lines that start with "spec" are already rejected
index = 0

while index < lines.size

line = lines[index]
Expand All @@ -49,31 +49,36 @@ def resolve_icons_in_arsc(apk_filepath:, default_icon_path:)

# drop until a config block will be found
next unless (config = line.match(/config\s+(?'dpi'.+):/)&.named_captures&.dig("dpi"))
next unless config == "(default)" || config.index("dpi")

while index < lines.size
line = lines[index]
index += 1

if line.index("config ")
index -= 1
break
end
break if line.index("config ")

index += 1

# drop until a line contains <resource_name>
next unless line.index(resource_name)

# Next line contains the filepath and never contain a config block header
# Next line may contain the filepath and never contain a config block header
line = lines[index]
index += 1

png_file_path = line.match(/"(?'path'.+)"/)&.named_captures&.dig("path") # never nil
# if path node is present, it never nil
if (png_file_path = line.match(/\(string\d+\)\s+"(?'path'.+)"/)&.named_captures&.dig("path")).nil?
::AndroidApk.logger.warn("#{line} does not contain a valid filepath")
else
config_hash[config] = png_file_path
end

config_hash[config] = png_file_path
break
end
end

config_hash
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

def dump_resource_values(apk_filepath:)
stdout, _, status = Open3.capture3("aapt", "dump", "--values", "resources", apk_filepath)
Expand Down
49 changes: 19 additions & 30 deletions lib/android_apk/signature_verifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ module SignatureVerifier
# [0]: entire, [1]: digest
SIGNER_SIGNATURE_REGEX = /\ASigner\s.+(?:MD5|SHA-?1|SHA-?256)\sdigest:\s([0-9a-zA-Z]{32,})\s*\z/i.freeze

# [min_sdk(inclusive), max_sdk(inclusive)]
SCHEME_CASE_INPUTS = [
# [min_sdk(inclusive), max_sdk(inclusive)]

# Legacy v1 scheme
[
1,
9,
17
],
# SHA256 with RSA is supported since API 18 but still v1 scheme
Expand All @@ -27,47 +25,38 @@ module SignatureVerifier
]
].freeze

# @param [String] filepath to an apk file
# @param [Integer, String] min_sdk_version of an apk
# @return [Array<Hash<String => Any>>] sdk-ranged certificate information array order by min_sdk_version asc
module_function def verify(filepath:, min_sdk_version:)
min_sdk_version = min_sdk_version.to_i

SCHEME_CASE_INPUTS.each_with_object([]) do |versions, constraints|
min_sdk, max_sdk, = versions
min_sdk_version_for_verification = [min_sdk, min_sdk_version].max

next unless min_sdk_version <= max_sdk

signer_hunks = ::AndroidApk::ApkSigner.verify(
signer_hunks = ::AndroidApk::ApkSigner.print_certs(
filepath: filepath,
min_sdk_version: min_sdk_version_for_verification,
min_sdk_version: [min_sdk, min_sdk_version].max,
max_sdk_version: max_sdk
)

if signer_hunks.empty?
constraints.push(
next if signer_hunks.empty?

signer_hunks.each do |sdk_versions, signer_lines|
# { "sha1" => ..., ... }
signature = signer_lines.each_with_object(
{
"min_sdk_version" => min_sdk_version_for_verification,
"max_sdk_version" => max_sdk,
::AndroidApk::SignatureDigest::MD5 => nil,
::AndroidApk::SignatureDigest::SHA1 => nil,
::AndroidApk::SignatureDigest::SHA256 => nil
"min_sdk_version" => [sdk_versions[0].to_i, min_sdk_version].max,
"max_sdk_version" => sdk_versions[1].to_i
}
)
else
signer_hunks.each do |sdk_versions, signer_lines|
# { "sha1" => ..., ... }
signature = signer_lines.each_with_object(
{
"min_sdk_version" => sdk_versions[0].to_i,
"max_sdk_version" => sdk_versions[1].to_i
}
) do |line, acc|
unless (m = line.match(SIGNER_SIGNATURE_REGEX)).nil?
acc.merge!({ ::AndroidApk::SignatureDigest.judge(digest: m[1]) => m[1] })
end
) do |line, acc|
unless (m = line.match(SIGNER_SIGNATURE_REGEX)).nil?
acc.merge!({ ::AndroidApk::SignatureDigest.judge(digest: m[1]) => m[1] })
end

constraints.push(signature)
end

constraints.push(signature)
end
end
end
Expand Down
125 changes: 117 additions & 8 deletions spec/android_apk/signature_lineage_reader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,55 @@
describe "#read" do
subject { AndroidApk::SignatureLineageReader.read(filepath: apk_filepath) }

context "if an apk has been rotated" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "signatures", "signature-v3", "app-rotated.apk") }
context "if an apk is unsigned" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "scheme-combination-apks", "m-v2required", "unsigned.apk") }

it "returns an empty array" do
expect(subject).to be_empty
end
end

context "if an apk is v1-and-v2 schemes" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "scheme-combination-apks", "m-v2required", "v1-and-v2.apk") }

it "returns an empty array" do
expect(subject).to be_empty
end
end

context "if an apk is v1-and-v3.1 schemes" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "scheme-combination-apks", "m-v2required", "v1-and-v3.1.apk") }

it "returns certificate information order by signing timing asc" do
expect(subject).to match_array(
[
{
"installed data" => true,
"shared uid" => true,
"permission" => true,
"rollback" => false,
"auth" => true,
"md5" => "1406a3ae028053ad27778af3efe6fbd8",
"sha1" => "eb6cbb57f091e97d614cdc773aa2efc66a39a818",
"sha256" => "4ca27e05a684c855ba204c7ee32c1cd0993de95163eae99ba578fc80c28e913f"
},
{
"installed data" => true,
"shared uid" => true,
"permission" => true,
"rollback" => false,
"auth" => true,
"md5" => "4b85af08b8186094d7b90b992b121e8d",
"sha1" => "e9d0dd023bdab7fae9479d1ecbb3275e0fccac20",
"sha256" => "4e8929a7f74291caad2f4c23a547e238d4fd7407a4960af749cf9e38a860e8bc"
}
]
)
end
end

context "if an apk is v1-and-v3 schemes" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "scheme-combination-apks", "m-v2required", "v1-and-v3.apk") }

it "returns certificate information order by signing timing asc" do
expect(subject).to match_array(
Expand Down Expand Up @@ -35,24 +82,86 @@
end
end

context "if an apk is not signed" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "other", "unsigned.apk") }
context "if an apk is v1-scheme" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "scheme-combination-apks", "m-v2required", "v1-only.apk") }

it "returns an empty array" do
expect(subject).to be_empty
end
end

context "if an apk is malformed" do
let(:apk_filepath) { File.join(FIXTURE_DIR, ".gitignore") }
context "if an apk is v2-scheme" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "scheme-combination-apks", "m-v2required", "v2-only.apk") }

it "returns an empty array" do
expect(subject).to be_empty
end
end

context "if an apk is a valid signed apk" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "sample.apk") }
context "if an apk is v3.1-scheme" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "scheme-combination-apks", "m-v2required", "v3.1.apk") }

it "returns certificate information order by signing timing asc" do
expect(subject).to match_array(
[
{
"installed data" => true,
"shared uid" => true,
"permission" => true,
"rollback" => false,
"auth" => true,
"md5" => "1406a3ae028053ad27778af3efe6fbd8",
"sha1" => "eb6cbb57f091e97d614cdc773aa2efc66a39a818",
"sha256" => "4ca27e05a684c855ba204c7ee32c1cd0993de95163eae99ba578fc80c28e913f"
},
{
"installed data" => true,
"shared uid" => true,
"permission" => true,
"rollback" => false,
"auth" => true,
"md5" => "4b85af08b8186094d7b90b992b121e8d",
"sha1" => "e9d0dd023bdab7fae9479d1ecbb3275e0fccac20",
"sha256" => "4e8929a7f74291caad2f4c23a547e238d4fd7407a4960af749cf9e38a860e8bc"
}
]
)
end
end

context "if an apk is v3-scheme" do
let(:apk_filepath) { File.join(FIXTURE_DIR, "scheme-combination-apks", "m-v2required", "v3.apk") }

it "returns certificate information order by signing timing asc" do
expect(subject).to match_array(
[
{
"installed data" => true,
"shared uid" => true,
"permission" => true,
"rollback" => false,
"auth" => true,
"md5" => "1406a3ae028053ad27778af3efe6fbd8",
"sha1" => "eb6cbb57f091e97d614cdc773aa2efc66a39a818",
"sha256" => "4ca27e05a684c855ba204c7ee32c1cd0993de95163eae99ba578fc80c28e913f"
},
{
"installed data" => true,
"shared uid" => true,
"permission" => true,
"rollback" => false,
"auth" => true,
"md5" => "4b85af08b8186094d7b90b992b121e8d",
"sha1" => "e9d0dd023bdab7fae9479d1ecbb3275e0fccac20",
"sha256" => "4e8929a7f74291caad2f4c23a547e238d4fd7407a4960af749cf9e38a860e8bc"
}
]
)
end
end

context "if an apk is malformed" do
let(:apk_filepath) { File.join(FIXTURE_DIR, ".gitignore") }

it "returns an empty array" do
expect(subject).to be_empty
Expand Down
Loading

0 comments on commit ac7e5ba

Please sign in to comment.