Compare commits

...
4 changed files with 159 additions and 2 deletions

No files matched your search

+38
View File
@@ -53,6 +53,44 @@ npm run android:sync --workspaces=false
üretim imzası içermez ve kurulabilir sürüm olarak paylaşılmamalıdır. Özel üretim
imza anahtarı oluşturulmadı; mağaza hesabı veya ücretli hizmet açılmadı.
Yayın adayı için sürüm ve Android sürüm kodu birlikte açıkça verilir:
```powershell
.\android\gradlew.bat -p android assembleRelease '-PaiturkReleaseVersion=0.1.4-beta.1' '-PaiturkReleaseVersionCode=6'
```
Bu komut yine **imzasız** APK üretir; üretim imzasını veya cihaz kabulünü atlamaz.
İki parametreden yalnız biri, `-dev` etiketi veya 5 ve daha küçük sürüm kodu
verilirse derleme durur. Parametresiz geliştirici derlemesi `0.1.4-dev` / 5 kalır.
Sonraki yayınlarda sürüm kodunu son dağıtılmış APK'dan daha büyük seçin; buradaki
alt sınır yalnız mevcut geliştirme sürümünü korur, uzak yayın envanterini sorgulamaz.
12 Eylül 2026: gerçek Gradle `assembleRelease` çalışması bu parametrelerle 55
saniyede tamamlandı; `aapt2` uygulama kimliği, `0.1.4-beta.1` / 6 ve debug
bayrağının olmadığını doğruladı. Test çıktısının SHA-256 değeri
`a93b93b3080ec484cc998ef316528ff7b947fa366b2ff5de375386acc66fffc4`.
`apksigner verify` imzasız çıktıyı reddetti (beklenen sonuç). Ayrı gerçek Gradle
çalışmaları eksik sürüm kodunu, `0.1.4-dev` yayın etiketini ve sürüm kodu 5'i
reddetti. Bu çıktı yayımlanmadı; imza ve cihaz kabulü değildir.
### Dağıtım APK'sının doğrulanması
`scripts/verify-android-release.mjs` dağıtım öncesi salt okunur APK kontrolüdür.
`--apk`, `--java`, `--apksigner` (Android SDK içindeki apksigner.jar), `--aapt2`,
`--certificate` (bağımsız olarak belirlenen üretim sertifikası SHA-256 değeri),
`--version` ve `--version-code` parametrelerinin tamamını ister. Sertifika pinini
kontrol edilen APK'dan otomatik türetmeyin. Bu araç anahtar üretmez veya APK imzalamaz.
Gerçek apksigner doğrulaması, v2/v3 imza, tek beklenen sertifika, uygulama kimliği
`online.turkservis.aiturk.mobile`, tam sürüm eşleşmesi ve debug bayrağı kontrol edilir.
Dosya doğrulama boyunca değişirse sonuç reddedilir. Debug sertifikası ve `-dev`
sürümü dağıtım için kabul edilmez. Başarılı rapor APK hash'ini verir fakat açıkça
`deviceAcceptanceVerified: false` içerir; fiziksel telefon kabulünün yerine geçmez.
12 Eylül 2026: mevcut mutual-identity geliştirici APK'sı gerçek SDK araçlarıyla
kontrol edildi ve beklenildiği gibi `debug-signing-certificate` ile reddedildi.
Üretim sertifikalı pozitif APK doğrulaması henüz yapılmadı.
## Uyumlu IDE ve bağlantı servisi
Bu kaynak ağacındaki masaüstü paketini normal masaüstü build süreciyle derleyin.
+19 -2
View File
@@ -1,5 +1,22 @@
apply plugin: 'com.android.application'
// Release identity is explicit; ordinary developer builds retain the -dev label.
def releaseVersion = providers.gradleProperty('aiturkReleaseVersion').orNull
def releaseCode = providers.gradleProperty('aiturkReleaseVersionCode').orNull
if ((releaseVersion == null) != (releaseCode == null)) {
throw new GradleException('Both aiturkReleaseVersion and aiturkReleaseVersionCode are required')
}
def selectedVersionCode = 5
if (releaseVersion != null) {
if (!(releaseVersion ==~ /\d+\.\d+\.\d+(?:-beta\.\d+)?/)) {
throw new GradleException('Release version must be X.Y.Z or X.Y.Z-beta.N; -dev is not a release')
}
if (!(releaseCode ==~ /[1-9]\d{0,9}/) || releaseCode.toLong() <= 5 || releaseCode.toLong() > 2100000000L) {
throw new GradleException('Release version code must be greater than 5 and at most 2100000000')
}
selectedVersionCode = releaseCode.toInteger()
}
android {
namespace = "online.turkservis.aiturk.mobile"
compileSdk = rootProject.ext.compileSdkVersion
@@ -7,8 +24,8 @@ android {
applicationId "online.turkservis.aiturk.mobile"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 5
versionName "0.1.4-dev"
versionCode selectedVersionCode
versionName releaseVersion ?: "0.1.4-dev"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -0,0 +1,77 @@
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { lstat } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
import { parseArgs } from 'node:util'
import { pathToFileURL } from 'node:url'
export function validateAndroidRelease({ signature, badging, certificate, version, versionCode }) {
const expected = certificate.trim().replaceAll(':', '').toLowerCase()
if (!/^[a-f0-9]{64}$/.test(expected)) throw new Error('certificate-pin-invalid')
if (!/^\d+\.\d+\.\d+(?:-beta\.\d+)?$/.test(version)) throw new Error('release-version-invalid')
if (!Number.isSafeInteger(versionCode) || versionCode <= 0) throw new Error('version-code-invalid')
const signers = [...signature.matchAll(/^Signer #\d+ certificate SHA-256 digest: ([a-fA-F0-9]{64})\s*$/gm)]
if (signers.length !== 1 || signers[0][1].toLowerCase() !== expected) throw new Error('signing-certificate-mismatch')
if (/^Signer #\d+ certificate DN:.*\bCN=Android Debug\b/im.test(signature)) throw new Error('debug-signing-certificate')
if (!/^Verified using v[23](?:\.\d+)? scheme[^\r\n]*: true\s*$/m.test(signature)) throw new Error('modern-apk-signature-required')
const packages = [...badging.matchAll(/^package: name='([^']+)' versionCode='([^']+)' versionName='([^']+)'/gm)]
if (packages.length !== 1 || packages[0][1] !== 'online.turkservis.aiturk.mobile') throw new Error('application-id-mismatch')
if (packages[0][2] !== String(versionCode) || packages[0][3] !== version) throw new Error('apk-version-mismatch')
if (/^application-debuggable\b/m.test(badging)) throw new Error('debuggable-apk')
return { applicationId: packages[0][1], version, versionCode, certificateSha256: expected }
}
async function regularFile(path) {
let current = path
while (true) {
const info = await lstat(current)
if (info.isSymbolicLink()) throw new Error('apk-reparse-path')
if (current === path && !info.isFile()) throw new Error('apk-not-regular')
const parent = dirname(current)
if (parent === current) return
current = parent
}
}
async function digest(path) {
const hash = createHash('sha256')
for await (const chunk of createReadStream(path)) hash.update(chunk)
return hash.digest('hex')
}
function tool(executable, args) {
const env = { ...process.env }
delete env.JAVA_TOOL_OPTIONS
delete env._JAVA_OPTIONS
delete env.JDK_JAVA_OPTIONS
const result = spawnSync(executable, args, { env, encoding: 'utf8', windowsHide: true, timeout: 60_000, maxBuffer: 1024 * 1024 })
if (result.error || result.status !== 0) throw new Error('android-verification-tool-failed')
return result.stdout
}
export async function verifyAndroidRelease(options) {
const apk = resolve(options.apk)
await regularFile(apk)
const before = await lstat(apk)
const sha256 = await digest(apk)
const signature = tool(resolve(options.java), ['-jar', resolve(options.apksigner), 'verify', '--verbose', '--print-certs', apk])
const badging = tool(resolve(options.aapt2), ['dump', 'badging', apk])
const identity = validateAndroidRelease({ ...options, signature, badging })
await regularFile(apk)
const after = await lstat(apk)
if (before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs || await digest(apk) !== sha256) throw new Error('apk-changed-during-verification')
return { verified: true, ...identity, size: after.size, sha256, deviceAcceptanceVerified: false }
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
const { values } = parseArgs({ options: Object.fromEntries(['apk', 'java', 'apksigner', 'aapt2', 'certificate', 'version', 'version-code'].map(name => [name, { type: 'string' }])) })
try {
if (Object.keys(values).length !== 7) throw new Error('all-release-verification-arguments-required')
console.log(JSON.stringify(await verifyAndroidRelease({ ...values, versionCode: Number(values['version-code']) })))
} catch (error) {
const code = /^[a-z][a-z0-9-]{1,95}$/.test(error.message) ? error.message : 'android-release-verification-failed'
console.error(JSON.stringify({ verified: false, code }))
process.exitCode = 1
}
}
@@ -0,0 +1,25 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { validateAndroidRelease } from '../scripts/verify-android-release.mjs'
const certificate = 'a'.repeat(64)
const fixture = {
certificate, version: '0.1.4-beta.1', versionCode: 6,
signature: `Verified using v2 scheme (APK Signature Scheme v2): true\nSigner #1 certificate DN: CN=AITURK Release\nSigner #1 certificate SHA-256 digest: ${certificate}\n`,
badging: "package: name='online.turkservis.aiturk.mobile' versionCode='6' versionName='0.1.4-beta.1'\n",
}
test('APK release identity requires a pinned certificate and exact version', () => {
assert.equal(validateAndroidRelease(fixture).certificateSha256, certificate)
assert.throws(() => validateAndroidRelease({ ...fixture, certificate: 'b'.repeat(64) }), /certificate-mismatch/)
assert.throws(() => validateAndroidRelease({ ...fixture, versionCode: 7 }), /version-mismatch/)
assert.throws(() => validateAndroidRelease({ ...fixture, version: '0.1.4-dev' }), /release-version-invalid/)
})
test('debug and ambiguous APK identities are never release-ready', () => {
assert.throws(() => validateAndroidRelease({ ...fixture, signature: fixture.signature.replace('AITURK Release', 'Android Debug') }), /debug-signing/)
assert.throws(() => validateAndroidRelease({ ...fixture, badging: fixture.badging + 'application-debuggable\n' }), /debuggable-apk/)
assert.throws(() => validateAndroidRelease({ ...fixture, badging: fixture.badging.replace('online.turkservis.aiturk.mobile', 'other.application') }), /application-id/)
assert.throws(() => validateAndroidRelease({ ...fixture, signature: fixture.signature + `Signer #2 certificate SHA-256 digest: ${certificate}\n` }), /certificate-mismatch/)
assert.throws(() => validateAndroidRelease({ ...fixture, signature: fixture.signature.replace(': true', ': false') }), /modern-apk-signature/)
})