Rebrand as TPSchedule and refine clear liquid glass dock
This commit is contained in:
@@ -1,15 +1,32 @@
|
||||
param([string]$ToolchainRoot = 'C:/Users/david/Desktop/app/.tools')
|
||||
param(
|
||||
[string]$ToolchainRoot = 'C:/Users/david/Desktop/app/.tools',
|
||||
[ValidateSet('debug', 'release')][string]$Mode = 'release'
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$env:HEPAN_TOOLCHAIN_ROOT=$ToolchainRoot
|
||||
. "$PSScriptRoot/android-env.ps1"
|
||||
$projectRoot=Split-Path $PSScriptRoot -Parent
|
||||
Push-Location "$projectRoot/mobile"
|
||||
try {
|
||||
flutter.bat build apk --debug
|
||||
# Refresh local dependency metadata without network. The build then regenerates
|
||||
# plugin registration for its mode, excluding test-only plugins from release.
|
||||
flutter.bat pub get --offline
|
||||
if($LASTEXITCODE -ne 0) { throw '本地依赖准备失败,未开始构建' }
|
||||
flutter.bat build apk "--$Mode"
|
||||
if($LASTEXITCODE -ne 0) { throw 'Android 构建失败,未复制 APK' }
|
||||
$built=Join-Path (Get-Location) 'build/app/outputs/flutter-apk/app-debug.apk'
|
||||
$built=Join-Path (Get-Location) "build/app/outputs/flutter-apk/app-$Mode.apk"
|
||||
if(!(Test-Path -LiteralPath $built)) { throw '本次构建未生成 APK' }
|
||||
$taskSigner = Join-Path $env:ANDROID_HOME 'build-tools/36.0.0/apksigner.bat'
|
||||
$taskSignature = & $taskSigner verify --print-certs $built
|
||||
if ($LASTEXITCODE -ne 0) { throw 'APK 签名验证失败,未复制 APK' }
|
||||
$taskExpectedCertificate = 'AF5C21B1BA1E48E461D8EA38DF0FE1C3653F64EB7E8E7728BAE6E5D988BA0D0A'
|
||||
$taskSignatureText = $taskSignature -join "`n"
|
||||
if ($taskSignatureText -notmatch 'certificate SHA-256 digest:\s*([a-fA-F0-9]{64})' -or
|
||||
$Matches[1].ToUpperInvariant() -ne $taskExpectedCertificate) {
|
||||
throw '签名证书与原证书不一致,未复制 APK。请保留原有签名文件,不要重新生成。'
|
||||
}
|
||||
New-Item -ItemType Directory -Force "$projectRoot/dist" | Out-Null
|
||||
Copy-Item -LiteralPath $built -Destination "$projectRoot/dist/RSchedule-debug.apk" -Force
|
||||
Get-Item -LiteralPath "$projectRoot/dist/RSchedule-debug.apk" | Select-Object FullName,Length,LastWriteTime
|
||||
$taskArtifact = if ($Mode -eq 'release') { 'TPSchedule.apk' } else { 'TPSchedule-debug.apk' }
|
||||
Copy-Item -LiteralPath $built -Destination "$projectRoot/dist/$taskArtifact" -Force
|
||||
Get-Item -LiteralPath "$projectRoot/dist/$taskArtifact" | Select-Object FullName,Length,LastWriteTime
|
||||
} finally { Pop-Location }
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Export public APK signing information and verify continuity. No private keys."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import struct
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.serialization import pkcs7
|
||||
from cryptography import x509
|
||||
|
||||
|
||||
def length_prefixed(data, offset=0):
|
||||
size = struct.unpack_from('<I', data, offset)[0]
|
||||
end = offset + 4 + size
|
||||
if end > len(data):
|
||||
raise ValueError('Malformed APK signing record.')
|
||||
return data[offset + 4:end], end
|
||||
|
||||
|
||||
def modern_certificate(apk):
|
||||
# APK v2/v3 certificates live before the ZIP central directory, not META-INF.
|
||||
with open(apk, 'rb') as stream:
|
||||
stream.seek(0, 2)
|
||||
size = stream.tell()
|
||||
stream.seek(max(0, size - 65557))
|
||||
tail = stream.read()
|
||||
eocd = tail.rfind(b'PK\x05\x06')
|
||||
if eocd < 0:
|
||||
raise ValueError('Missing ZIP directory.')
|
||||
central = struct.unpack_from('<I', tail, eocd + 16)[0]
|
||||
stream.seek(central - 24)
|
||||
footer = stream.read(24)
|
||||
if footer[8:] != b'APK Sig Block 42':
|
||||
return None
|
||||
block_size = struct.unpack_from('<Q', footer)[0]
|
||||
if block_size < 24 or block_size + 8 > central:
|
||||
raise ValueError('Malformed APK signing block.')
|
||||
stream.seek(central - block_size - 8)
|
||||
block = stream.read(block_size + 8)
|
||||
if struct.unpack_from('<Q', block)[0] != block_size:
|
||||
raise ValueError('APK signing block sizes differ.')
|
||||
offset = 8
|
||||
while offset < len(block) - 24:
|
||||
pair_size = struct.unpack_from('<Q', block, offset)[0]
|
||||
offset += 8
|
||||
pair = block[offset:offset + pair_size]
|
||||
if pair_size < 4 or len(pair) != pair_size:
|
||||
raise ValueError('Malformed APK signer pair.')
|
||||
scheme = struct.unpack_from('<I', pair)[0]
|
||||
if scheme in (0x7109871A, 0xF05368C0, 0x1B93AD61):
|
||||
signers, _ = length_prefixed(pair[4:])
|
||||
signer, signer_end = length_prefixed(signers)
|
||||
if signer_end != len(signers):
|
||||
raise ValueError('Multiple APK signers are not supported by this report.')
|
||||
signed_data, _ = length_prefixed(signer)
|
||||
_, digest_end = length_prefixed(signed_data)
|
||||
certificates, _ = length_prefixed(signed_data, digest_end)
|
||||
cert_der, _ = length_prefixed(certificates)
|
||||
return x509.load_der_x509_certificate(cert_der)
|
||||
offset += pair_size
|
||||
return None
|
||||
|
||||
|
||||
def certificate(apk):
|
||||
modern = modern_certificate(apk)
|
||||
if modern is not None:
|
||||
return modern
|
||||
with zipfile.ZipFile(apk) as archive:
|
||||
blocks = [name for name in archive.namelist()
|
||||
if name.upper().startswith('META-INF/')
|
||||
and name.upper().endswith(('.RSA', '.EC', '.DSA'))]
|
||||
if len(blocks) != 1:
|
||||
raise ValueError('Expected one v1 signing certificate block; verify APK with apksigner.')
|
||||
certificates = pkcs7.load_der_pkcs7_certificates(archive.read(blocks[0]))
|
||||
if len(certificates) != 1:
|
||||
raise ValueError('Expected one signing certificate, not a certificate chain.')
|
||||
return certificates[0]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--previous', required=True)
|
||||
parser.add_argument('--current', required=True)
|
||||
parser.add_argument('--output-dir', required=True)
|
||||
args = parser.parse_args()
|
||||
previous = certificate(args.previous)
|
||||
current = certificate(args.current)
|
||||
old_der = previous.public_bytes(serialization.Encoding.DER)
|
||||
der = current.public_bytes(serialization.Encoding.DER)
|
||||
public = current.public_key().public_bytes(
|
||||
serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo)
|
||||
if old_der != der:
|
||||
raise ValueError('Signing certificate changed. Existing signing material must be preserved.')
|
||||
output = Path(args.output_dir)
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
details = {
|
||||
'appName': 'TPSchedule',
|
||||
'applicationId': 'com.tpschedule.app',
|
||||
'certificateUnchanged': True,
|
||||
'subject': current.subject.rfc4514_string(),
|
||||
'certificateSHA256': hashlib.sha256(der).hexdigest().upper(),
|
||||
'certificateSHA1': hashlib.sha1(der).hexdigest().upper(),
|
||||
'certificateMD5': hashlib.md5(der).hexdigest().upper(),
|
||||
'publicKeySHA256': hashlib.sha256(public).hexdigest().upper(),
|
||||
'publicKeyMD5': hashlib.md5(public).hexdigest().upper(),
|
||||
'publicKeySPKIHex': public.hex().upper(),
|
||||
'publicKeyBits': current.public_key().key_size,
|
||||
'certificateNote': 'Existing Android Debug certificate preserved; store/filing acceptance must be checked.',
|
||||
}
|
||||
(output / 'TPSchedule-signing-info.json').write_text(
|
||||
json.dumps(details, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
(output / 'TPSchedule-signing-certificate.pem').write_bytes(
|
||||
current.public_bytes(serialization.Encoding.PEM))
|
||||
(output / 'TPSchedule-public-key.pem').write_bytes(
|
||||
current.public_key().public_bytes(serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo))
|
||||
print(json.dumps({'certificateUnchanged': True,
|
||||
'certificateSHA256': details['certificateSHA256'],
|
||||
'publicFiles': str(output)}, ensure_ascii=True))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user