build: signature release (keystore partagé patient+admin) + doc de gestion
Keystore release créé : /opt/Kazeia/keystore/kazeia-release.jks (RSA 4096, alias kazeia, valide 2056), credentials dans keystore/credentials.properties (chmod 600, hors git — la liste blanche du .gitignore racine l'exclut d'office). Les deux modules lisent credentials.properties : présent → assembleRelease signe en release ; absent (autre machine/CI) → repli debug + warning, le build ne casse pas. MÊME clé pour com.kazeia et com.kazeia.admin → permettra la signature-permission sur le ContentProvider (plan admin). Validé : assembleRelease des deux apps OK, apksigner confirme le certificat CN=Kazeia (SHA-256 4b408d9d…) sur les deux APK. docs/RELEASE_SIGNING.md : gestion complète — câblage, procédure de release via catalogue (bump versionCode obligatoire), SAUVEGARDE du keystore (le point qui ne pardonne pas), scénarios de panne (perte clé/mdp/machine/compromission), migration one-shot des tablettes debug→release, dev quotidien reste en debug. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
fd9e04a0cb
commit
93f09c87d3
|
|
@ -0,0 +1,118 @@
|
||||||
|
# Signature release Kazeia — gestion du keystore
|
||||||
|
|
||||||
|
> **Résumé en 3 lignes** : les deux APK (patient + admin) sont signées en release avec
|
||||||
|
> **une seule clé** : `/opt/Kazeia/keystore/kazeia-release.jks`. Si cette clé est perdue,
|
||||||
|
> **aucune mise à jour in-place n'est plus possible** (désinstallation forcée = perte des
|
||||||
|
> données patient). → **Sauvegarder le dossier `keystore/` hors machine, maintenant.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ce qui existe
|
||||||
|
|
||||||
|
| Fichier | Rôle |
|
||||||
|
|---|---|
|
||||||
|
| `/opt/Kazeia/keystore/kazeia-release.jks` | Keystore (RSA 4096, alias `kazeia`, valide jusqu'en **2056**) |
|
||||||
|
| `/opt/Kazeia/keystore/credentials.properties` | Chemin + mots de passe, lu par Gradle (chmod 600) |
|
||||||
|
| `docs/RELEASE_SIGNING.md` | Ce document |
|
||||||
|
|
||||||
|
Empreinte du certificat (référence pour vérifier qu'on signe avec la bonne clé) :
|
||||||
|
```
|
||||||
|
SHA256: 4B:40:8D:9D:40:09:FD:68:F7:FD:5B:E0:BD:64:2B:D3:EE:B8:F8:BD:7A:84:D3:9C:84:90:B8:63:21:AF:E4:90
|
||||||
|
```
|
||||||
|
|
||||||
|
Le dossier `keystore/` est **hors git** (le `.gitignore` racine est en liste blanche : tout
|
||||||
|
ce qui n'est pas explicitement inclus est ignoré). C'est voulu : un keystore + ses mots de
|
||||||
|
passe ne vont jamais dans un dépôt.
|
||||||
|
|
||||||
|
## 2. Comment c'est câblé
|
||||||
|
|
||||||
|
`app/build.gradle.kts` et `app-admin/build.gradle.kts` lisent
|
||||||
|
`/opt/Kazeia/keystore/credentials.properties` :
|
||||||
|
|
||||||
|
- **Présent** → `assembleRelease` signe avec la clé release.
|
||||||
|
- **Absent** (autre machine, CI sans secrets) → le build **ne casse pas** : release signé
|
||||||
|
en **debug** avec un warning `⚠ keystore release absent`. Ces APK de repli ne doivent
|
||||||
|
JAMAIS être distribuées.
|
||||||
|
|
||||||
|
**Une seule clé pour les deux apps** (patient `com.kazeia` + admin `com.kazeia.admin`) :
|
||||||
|
même identité → permettra de protéger le ContentProvider par une
|
||||||
|
`signature`-permission (prévu au plan admin, pas encore fait).
|
||||||
|
|
||||||
|
## 3. Construire et distribuer une release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/Kazeia/kazeia-android
|
||||||
|
|
||||||
|
# 1) Bumper versionCode (OBLIGATOIRE sinon l'OTA ne se déclenche pas)
|
||||||
|
# app/build.gradle.kts : versionCode = N+1
|
||||||
|
# (app-admin pareil si distribuée)
|
||||||
|
|
||||||
|
# 2) Build
|
||||||
|
./gradlew :app:assembleRelease # → app/build/outputs/apk/release/app-release.apk
|
||||||
|
|
||||||
|
# 3) Vérifier la signature (DOIT afficher l'empreinte SHA256 ci-dessus)
|
||||||
|
/opt/Kazeia/android-sdk/build-tools/*/apksigner verify --print-certs \
|
||||||
|
app/build/outputs/apk/release/app-release.apk | grep SHA-256
|
||||||
|
|
||||||
|
# 4) Publier via le catalogue Nextcloud
|
||||||
|
cp app/build/outputs/apk/release/app-release.apk /opt/Kazeia/kazeia-dist/apk/kazeia.apk
|
||||||
|
# Éditer kazeia-dist/catalog.spec.json : app.version_code = N+1, bump catalog_version
|
||||||
|
python3 /opt/Kazeia/kazeia-dist-tools/make_catalog.py --dist-root /opt/Kazeia/kazeia-dist --upload
|
||||||
|
```
|
||||||
|
|
||||||
|
Les tablettes voient `catalog.app.version_code > BuildConfig.VERSION_CODE` → téléchargent,
|
||||||
|
vérifient le SHA-256, et proposent l'installation (dialogue système).
|
||||||
|
|
||||||
|
## 4. SAUVEGARDE — le point qui ne pardonne pas
|
||||||
|
|
||||||
|
La clé EST l'identité de l'app. Android n'accepte une mise à jour que si elle est signée
|
||||||
|
par la **même clé** que la version installée.
|
||||||
|
|
||||||
|
**À faire immédiatement et à chaque rotation de machine :**
|
||||||
|
```bash
|
||||||
|
# Copier le dossier ENTIER (jks + credentials) vers AU MOINS DEUX emplacements hors machine :
|
||||||
|
tar czf kazeia-keystore-$(date +%Y%m%d).tar.gz -C /opt/Kazeia keystore/
|
||||||
|
# → Nextcloud (dossier PRIVÉ, pas le partage soft/), clé USB au coffre, gestionnaire de mots de passe…
|
||||||
|
```
|
||||||
|
Recommandé : déposer l'archive sur `box.kazeia.com` dans un dossier **privé** + une copie
|
||||||
|
hors-ligne (USB). Vérifier la restauration une fois (`tar xzf` + `keytool -list`).
|
||||||
|
|
||||||
|
**Sur une nouvelle machine de build** : restaurer `keystore/` dans `/opt/Kazeia/`,
|
||||||
|
vérifier `chmod 600`, builder — rien d'autre à configurer.
|
||||||
|
|
||||||
|
## 5. Scénarios de panne
|
||||||
|
|
||||||
|
| Scénario | Conséquence | Procédure |
|
||||||
|
|---|---|---|
|
||||||
|
| **Keystore perdu (aucune sauvegarde)** | Plus AUCUNE mise à jour in-place possible | Générer une nouvelle clé, désinstaller/réinstaller sur chaque tablette → **perte des données patient** (SQLCipher lié à l'install). Exporter l'historique via l'admin AVANT si possible. À éviter à tout prix → §4. |
|
||||||
|
| **Mot de passe perdu (jks présent)** | Identique à la perte du keystore | Aucun recouvrement possible sur un .jks. Le mot de passe est dans `credentials.properties` — sauvegardé AVEC le .jks. |
|
||||||
|
| **Machine de build morte (sauvegarde OK)** | Aucun impact | Restaurer `keystore/` sur la nouvelle machine (§4). |
|
||||||
|
| **Clé compromise (fuite)** | Un tiers peut signer des mises à jour | Distribution étant privée (Nextcloud authentifié), changer aussi les identifiants WebDAV ; planifier une migration de clé (réinstallation contrôlée). |
|
||||||
|
|
||||||
|
## 6. Migration des tablettes existantes (one-shot)
|
||||||
|
|
||||||
|
Les installs actuelles sont **debug-signées** → la première APK release sera refusée en
|
||||||
|
mise à jour (mismatch). **Une fois par tablette** :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) (si données à garder) export de l'historique via l'app admin
|
||||||
|
# 2) Désinstaller les builds debug
|
||||||
|
adb uninstall com.kazeia ; adb uninstall com.kazeia.admin
|
||||||
|
# 3) Installer les release
|
||||||
|
adb install app/build/outputs/apk/release/app-release.apk
|
||||||
|
adb install app-admin/build/outputs/apk/release/app-admin-release.apk
|
||||||
|
# 4) Re-provisionner (les modèles sur stockage externe/`/data/local/tmp` SURVIVENT à la
|
||||||
|
# désinstallation ; profils/conversations/config in-app sont PERDUS → re-créer le profil)
|
||||||
|
```
|
||||||
|
Après cette migration, toutes les mises à jour passent par l'OTA catalogue, sans perte.
|
||||||
|
|
||||||
|
⚠ **Le dev quotidien reste en debug** (`assembleDebug` + `adb install -r`) : debug et
|
||||||
|
release sont signés différemment, on ne peut pas écraser l'un par l'autre. La tablette
|
||||||
|
de dev reste en debug ; les tablettes patient passent en release.
|
||||||
|
|
||||||
|
## 7. Rotation / clés multiples (futur)
|
||||||
|
|
||||||
|
- Pas d'expiration avant 2056 ; pas de rotation préventive nécessaire pour une
|
||||||
|
distribution privée.
|
||||||
|
- Si un jour publication sur un store : Google Play App Signing prendra cette clé comme
|
||||||
|
« upload key » et gérera la clé de signature côté store (le risque de perte disparaît).
|
||||||
|
|
@ -1,13 +1,35 @@
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("org.jetbrains.kotlin.plugin.compose")
|
id("org.jetbrains.kotlin.plugin.compose")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MÊME keystore release que l'app patient (permet à terme la permission de
|
||||||
|
// signature sur le ContentProvider). Voir docs/RELEASE_SIGNING.md.
|
||||||
|
val releaseKeystoreProps = Properties().apply {
|
||||||
|
val f = file("/opt/Kazeia/keystore/credentials.properties")
|
||||||
|
if (f.exists()) f.inputStream().use { load(it) }
|
||||||
|
}
|
||||||
|
val hasReleaseKeystore = releaseKeystoreProps.getProperty("storeFile")
|
||||||
|
?.let { file(it).exists() } == true
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.kazeia.admin"
|
namespace = "com.kazeia.admin"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
if (hasReleaseKeystore) {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file(releaseKeystoreProps.getProperty("storeFile"))
|
||||||
|
storePassword = releaseKeystoreProps.getProperty("storePassword")
|
||||||
|
keyAlias = releaseKeystoreProps.getProperty("keyAlias")
|
||||||
|
keyPassword = releaseKeystoreProps.getProperty("keyPassword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.kazeia.admin"
|
applicationId = "com.kazeia.admin"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
|
|
@ -27,6 +49,12 @@ android {
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
|
if (hasReleaseKeystore) {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
} else {
|
||||||
|
logger.warn("⚠ keystore release absent (/opt/Kazeia/keystore/) — release signé en DEBUG. Voir docs/RELEASE_SIGNING.md")
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,32 @@ val localProps = Properties().apply {
|
||||||
fun localProp(key: String, default: String = ""): String =
|
fun localProp(key: String, default: String = ""): String =
|
||||||
localProps.getProperty(key) ?: default
|
localProps.getProperty(key) ?: default
|
||||||
|
|
||||||
|
// Keystore release — identifiants lus depuis /opt/Kazeia/keystore/credentials.properties
|
||||||
|
// (hors git, à SAUVEGARDER hors machine). Voir docs/RELEASE_SIGNING.md.
|
||||||
|
// Absent (autre machine) → le build release retombe sur la clé debug avec un warning.
|
||||||
|
val releaseKeystoreProps = Properties().apply {
|
||||||
|
val f = file("/opt/Kazeia/keystore/credentials.properties")
|
||||||
|
if (f.exists()) f.inputStream().use { load(it) }
|
||||||
|
}
|
||||||
|
val hasReleaseKeystore = releaseKeystoreProps.getProperty("storeFile")
|
||||||
|
?.let { file(it).exists() } == true
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.kazeia"
|
namespace = "com.kazeia"
|
||||||
compileSdk = 36
|
compileSdk = 36
|
||||||
ndkVersion = "27.3.13750724"
|
ndkVersion = "27.3.13750724"
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
if (hasReleaseKeystore) {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file(releaseKeystoreProps.getProperty("storeFile"))
|
||||||
|
storePassword = releaseKeystoreProps.getProperty("storePassword")
|
||||||
|
keyAlias = releaseKeystoreProps.getProperty("keyAlias")
|
||||||
|
keyPassword = releaseKeystoreProps.getProperty("keyPassword")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.kazeia"
|
applicationId = "com.kazeia"
|
||||||
minSdk = 28
|
minSdk = 28
|
||||||
|
|
@ -57,6 +78,12 @@ android {
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
|
if (hasReleaseKeystore) {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
} else {
|
||||||
|
logger.warn("⚠ keystore release absent (/opt/Kazeia/keystore/) — release signé en DEBUG. Voir docs/RELEASE_SIGNING.md")
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue