forked from tnj/android_apk
-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support scheme v3.1 #122
Merged
Merged
Support scheme v3.1 #122
Changes from 6 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
00201ef
fix: Use apksigner properly to get signatures and lineages
jmatsu 851bcce
test: Add app_signature's test
jmatsu f67ade3
test: test concatenating fingerprints
jmatsu 1a10583
test: Add rotated? method and test it
jmatsu e3c982a
chore: Reject unmeaningful lines
jmatsu 0251d33
feat: Add update verification and its testcases
jmatsu 9e7c1ce
refactor: separate get_target_certificate into judging the autority a…
jmatsu 31ecc26
chore: Check only sha256 due to the performance concern
jmatsu 7a2e7af
feat: Skip unsigned span instead of keeping it
jmatsu 1cf3bc0
test: Modified expections cuz they don't need to care about nil span
jmatsu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
# frozen_string_literal: true | ||
|
||
class AndroidApk | ||
module ApkSigner | ||
# [0]: entire, [1]: \d+, [2]: \d+ | ||
TARGET_SDK_PART_REGEX = /\(minSdkVersion=(\d+),\s*maxSdkVersion=(\d+)\)/i.freeze | ||
|
||
# @param filepath [String] a file path of an apk file | ||
# @return [Array<Array<String>>] a list of lines for each signer | ||
module_function def lineage(filepath:) | ||
args = [ | ||
"apksigner", | ||
"lineage", | ||
"--in", | ||
filepath, | ||
"--print-certs" | ||
] | ||
|
||
stdout, _, exit_status = Open3.capture3(*args) | ||
|
||
if exit_status.success? | ||
split_linage_signer_hunks(stdout: stdout) | ||
else | ||
[] | ||
end | ||
end | ||
|
||
# @param filepath [String] a file path of an apk file | ||
# @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:) | ||
# Don't add -v because it will print pub keys too. | ||
args = [ | ||
"apksigner", | ||
"verify", | ||
"--min-sdk-version", | ||
min_sdk_version.to_s, | ||
"--max-sdk-version", | ||
max_sdk_version.to_s, | ||
"--print-certs", | ||
filepath | ||
] | ||
stdout, stderr, exit_status = Open3.capture3(*args) | ||
|
||
if exit_status.success? | ||
split_verify_signer_hunks(stdout: stdout, min_sdk_version: min_sdk_version, max_sdk_version: max_sdk_version) | ||
else | ||
return {} if stderr.downcase.include?("does not verify") | ||
|
||
raise AndroidApk::ApkSignerExecutionError, "this file is a malformed apk" | ||
end | ||
end | ||
|
||
module_function def split_linage_signer_hunks(stdout:) | ||
signers = [] | ||
signer_index = 0 | ||
|
||
stdout.split("\n").each do |line| | ||
unless (signer_number = line[/\ASigner\s#(\d+)\s/, 1]&.to_i).nil? | ||
signer_index = signer_number - 1 | ||
end | ||
|
||
signers[signer_index] ||= [] | ||
|
||
if line.start_with?("Has") || !line.include?("DN: CN") | ||
signers[signer_index].push(line) | ||
end | ||
end | ||
|
||
signers.compact | ||
end | ||
|
||
module_function def split_verify_signer_hunks(stdout:, min_sdk_version:, max_sdk_version:) | ||
signers = {} | ||
|
||
lines = stdout.split("\n").reject { |line| line.include?("WARNING") || line.include?("DN: CN") } | ||
|
||
if lines[0].include?("minSdkVersion=") | ||
lines.each do |line| | ||
next if (m = line.match(TARGET_SDK_PART_REGEX)).nil? | ||
|
||
sdk_versions = [m[1], m[2]] | ||
signers[sdk_versions] ||= [] | ||
signers[sdk_versions].push(line) | ||
end | ||
else | ||
# TODO: support multiple signers | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Multiple key signing is not supported by Google Play or the general usecase of App Bundle, so this has been to TODOs. |
||
lines.each do |line| | ||
if !(signer_number = line[/\ASigner\s#(\d+)\s/, 1]&.to_i).nil? && (signer_number >= 2) | ||
raise "Signer ##{signer_number} is found but it has not been supported yet" | ||
end | ||
end | ||
|
||
sdk_versions = [min_sdk_version.to_s, max_sdk_version.to_s] | ||
|
||
signers[sdk_versions] = lines | ||
end | ||
|
||
signers.compact | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
# frozen_string_literal: true | ||
|
||
class AndroidApk | ||
# @!attribute [r] fingerprints | ||
# @return [Array<Hash<String => Any>>] | ||
# @!attribute [r] lineages | ||
# @return [Array<Hash<String => Any>>] | ||
class AppSignature | ||
V2_SCHEME_SDK_INT = 24 | ||
V3_SCHEME_SDK_INT = 28 | ||
V3_1_SCHEME_SDK_INT = 33 | ||
|
||
class << self | ||
# @param [String] filepath to an apk file | ||
# @param [Integer, String] min_sdk_version | ||
# @return [AppSignature] a new signature | ||
def parse(filepath:, min_sdk_version:) | ||
lineages = ::AndroidApk::SignatureLineageReader.read(filepath: filepath) | ||
fingerprints = ::AndroidApk::SignatureVerifier.verify(filepath: filepath, min_sdk_version: min_sdk_version) | ||
|
||
AppSignature.new( | ||
lineages: lineages, | ||
fingerprints: fingerprints | ||
) | ||
end | ||
|
||
# @param certificate_from [Hash<String => Any>, nil] | ||
# @param app_signature_to [AppSignature] an signature object of the target apk | ||
# @param sdk_version [Integer] the sdk version of the device | ||
# @return [Hash<String => Any>, nil] | ||
def get_target_certificate(certificate_from:, lineages_from:, app_signature_to:, sdk_version:) | ||
# SHA1 or SHA256 is good | ||
digest_method = ::AndroidApk::SignatureDigest::SHA1 | ||
|
||
# Deny if any fingerprint is unavailable for the sdk version | ||
fingerprint_to = app_signature_to.get_fingerprint(sdk_version: sdk_version) or return | ||
|
||
current_app_signing_digest = certificate_from[digest_method] | ||
target_app_signing_digest = fingerprint_to[digest_method] | ||
|
||
# Direct update | ||
if target_app_signing_digest == current_app_signing_digest | ||
return fingerprint_to.slice(::AndroidApk::SignatureDigest::MD5, ::AndroidApk::SignatureDigest::SHA1, ::AndroidApk::SignatureDigest::SHA256) | ||
end | ||
|
||
# rollback is a special case | ||
if !(rollback_index = lineages_from.index { |lineage| lineage[digest_method] == target_app_signing_digest }).nil? && lineages_from[rollback_index]["rollback"] | ||
return fingerprint_to.slice(::AndroidApk::SignatureDigest::MD5, ::AndroidApk::SignatureDigest::SHA1, ::AndroidApk::SignatureDigest::SHA256) | ||
end | ||
|
||
return nil if sdk_version < V3_SCHEME_SDK_INT | ||
|
||
# Return true if the target apk contains the current signing signature. | ||
jmatsu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
certificate = app_signature_to.lineages.find do |lineage| | ||
lineage[digest_method] == target_app_signing_digest | ||
end | ||
|
||
certificate&.slice(::AndroidApk::SignatureDigest::MD5, ::AndroidApk::SignatureDigest::SHA1, ::AndroidApk::SignatureDigest::SHA256) | ||
end | ||
end | ||
|
||
attr_reader :fingerprints, :lineages | ||
|
||
def initialize(lineages:, fingerprints:) | ||
@fingerprints = merge_fingerprints(fingerprints: fingerprints).freeze | ||
@lineages = @fingerprints.empty? ? [].freeze : lineages.freeze # drop if unsigned | ||
raise "lineages must be an empty or a list of 2 or more elements" if @lineages.size == 1 | ||
end | ||
|
||
# @param [Integer] sdk_version | ||
# @return [Hash<String => Any>, nil] | ||
def get_fingerprint(sdk_version:) | ||
# Ruby doesn't have TreeMap... | ||
@fingerprints.find do |cert| | ||
cert.fetch("min_sdk_version") <= sdk_version && sdk_version <= cert.fetch("max_sdk_version") | ||
end | ||
end | ||
|
||
def unsigned? | ||
@fingerprints.empty? | ||
end | ||
|
||
def rotated? | ||
[email protected]? | ||
end | ||
|
||
private def merge_fingerprints(fingerprints:) | ||
merged_fingerprints = fingerprints.each_with_object([]) do |fingerprint, acc| | ||
if !(last_entry = acc.last).nil? && (last_entry.fetch(::AndroidApk::SignatureDigest::SHA256) == fingerprint.fetch(::AndroidApk::SignatureDigest::SHA256)) | ||
last_max_sdk_version = last_entry.fetch("max_sdk_version") | ||
min_sdk_version = fingerprint.fetch("min_sdk_version") | ||
|
||
if last_max_sdk_version + 1 == min_sdk_version || min_sdk_version <= last_max_sdk_version | ||
last_entry["max_sdk_version"] = fingerprint["max_sdk_version"] | ||
next | ||
end | ||
end | ||
|
||
acc.push(fingerprint) | ||
end | ||
|
||
return [] if merged_fingerprints.size == 1 && merged_fingerprints[0].fetch(::AndroidApk::SignatureDigest::SHA256).nil? | ||
|
||
merged_fingerprints | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# frozen_string_literal: true | ||
|
||
class AndroidApk | ||
module SignatureDigest | ||
MD5 = "md5" | ||
SHA1 = "sha1" | ||
SHA256 = "sha256" | ||
|
||
DIGEST_REGEX = /\A[0-9a-zA-Z]{32,}\z/.freeze | ||
|
||
# @param digest [String] | ||
# @return [String] digest method | ||
def self.judge(digest:) | ||
raise "only hex-digest is supported" unless digest =~ DIGEST_REGEX | ||
|
||
case digest.length | ||
when 32 | ||
::AndroidApk::SignatureDigest::MD5 | ||
when 40 | ||
::AndroidApk::SignatureDigest::SHA1 | ||
when 64 | ||
::AndroidApk::SignatureDigest::SHA256 | ||
else | ||
raise "#{digest.length}-length digest is not supported" | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This exposes v1, v2, v3, v3.1 compatible interfaces.