mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
Correctifs repris d'AJEAN : chiffrement, preset externe, heartbeat, commande du moteur
- Désactivation du chiffrement (amont cac0cda, 19918c6) : une valeur illisible ne bloque plus tout (quarantaine datée + copie du keyvault), et la reprise au démarrage déchiffre aussi les conversations au lieu de retirer la clé après les seules pages. Propre à Loki : les images de conversation (chatimg/) sont maintenant déchiffrées elles aussi — elles restaient chiffrées sans clé. Les .bak chiffrés devenus illisibles sont retirés. - Preset externe (amont #95) : `loki serve` sort sans erreur (plus de boucle systemd « MODEL non défini »), le pré-vol l'accepte, le superviseur du conteneur ne le prend plus pour un plantage. - Unité systemd du moteur : TimeoutStopSec=5 (bascule de preset bloquée 90 s en pleine génération). - Heartbeat SSE (amont #105) : plus d'écriture après le retour du handler. - Paramètres → Configuration : commande exacte du moteur copiable (clé API masquée, Strata compris) et nombre de couches du modèle à côté de NGL, lu dans l'en-tête GGUF (amont #108, #43) ; aussi dans l'éditeur de preset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4a9a5a1712
commit
127013b32b
16 files changed
+479
-34
No files matched your search
+123
-17
@@ -259,52 +259,158 @@ func DisableMemEncryption() error {
|
||||
// page, et on ne baisse le drapeau qu'à la toute fin. Ainsi une interruption
|
||||
// laisse un état parfaitement lisible et RE-JOUABLE (le drapeau est encore
|
||||
// levé, donc DisableMemEncryption peut être relancé).
|
||||
if err := decryptAllPages(); err != nil {
|
||||
return finishDecryption()
|
||||
}
|
||||
|
||||
// finishDecryption : fichiers (pages, images des conversations), puis
|
||||
// conversations, puis retrait de la clé, dans cet ordre et seulement si tout ce
|
||||
// qui précède a réussi. Sûr à rejouer. Partagé avec la reprise au démarrage, qui
|
||||
// retirait la clé après les seules pages : les conversations restaient
|
||||
// chiffrées sans plus aucun moyen de les rouvrir (amont cac0cda).
|
||||
func finishDecryption() error {
|
||||
bad, err := decryptAllPagesSkipping()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := decryptChatStores(); err != nil {
|
||||
badKV, err := decryptChatStoresSkipping()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Toutes les pages sont en clair et vérifiées : on peut baisser le drapeau,
|
||||
// retirer le keyvault et purger la DEK.
|
||||
bad = append(bad, badKV...)
|
||||
if len(bad) > 0 {
|
||||
// Valeurs illisibles avec la clé actuelle : déjà inaccessibles avant ce
|
||||
// déchiffrement. On les met en quarantaine (toujours chiffrées) avec une
|
||||
// copie du keyvault, pour pouvoir encore les rouvrir plus tard, puis on
|
||||
// les retire de la base pour que le reste fonctionne en clair.
|
||||
if err := quarantineUnreadable(bad); err != nil {
|
||||
return fmt.Errorf("mise en quarantaine des valeurs illisibles impossible, déchiffrement interrompu (rien n'est perdu, il peut être relancé) : %w", err)
|
||||
}
|
||||
for _, u := range bad {
|
||||
if u.Bucket != "fichier" { // un fichier reste en place, sa copie est en quarantaine
|
||||
_ = putBytes(u.Bucket, u.Key, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tout est en clair et vérifié : on peut baisser le drapeau, retirer le
|
||||
// keyvault et purger la DEK.
|
||||
if err := SetConfigKey("MEM_ENCRYPTED", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
removeVault()
|
||||
clearMemDEK()
|
||||
clearMigrationJournal()
|
||||
if n := scrubEncryptedResidue(); n > 0 {
|
||||
fmt.Fprintf(os.Stderr, "[mémoire] %d copie(s) .bak chiffrée(s), illisibles sans la clé, retirée(s)\n", n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decryptAllPages réécrit en clair toutes les pages encore chiffrées, DANS TOUS LES
|
||||
// PROJETS, en vérifiant chacune. Exige la DEK en RAM. Sûr à rejouer (idempotent).
|
||||
func decryptAllPages() error {
|
||||
for _, p := range allMemPageFiles() {
|
||||
// decryptAllPagesSkipping réécrit en clair tous les fichiers encore chiffrés de
|
||||
// la mémoire (tous projets) et des images de conversation, en vérifiant chacun.
|
||||
// Exige la DEK en RAM. Sûr à rejouer. Un fichier indéchiffrable est laissé tel
|
||||
// quel et renvoyé au lieu de tout arrêter.
|
||||
func decryptAllPagesSkipping() ([]unreadableValue, error) {
|
||||
var bad []unreadableValue
|
||||
for _, p := range allMemFilesForDecrypt() {
|
||||
raw, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lecture %s : %w", p, err)
|
||||
return bad, fmt.Errorf("lecture %s : %w", p, err)
|
||||
}
|
||||
if !looksEncrypted(raw) {
|
||||
continue // déjà en clair
|
||||
}
|
||||
plain, err := decodeMemContent(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("déchiffrement %s impossible, on n'écrase rien : %w", p, err)
|
||||
bad = append(bad, unreadableValue{Bucket: "fichier", Key: p, Blob: raw, Error: err.Error()})
|
||||
continue // on n'écrase rien
|
||||
}
|
||||
if old, err := os.ReadFile(p); err == nil {
|
||||
_ = memWriteFileAtomic(p+".bak", old, 0o600)
|
||||
}
|
||||
if err := memWriteFileVerified(p, plain, 0o600); err != nil {
|
||||
return fmt.Errorf("écriture claire %s : %w", p, err)
|
||||
return bad, fmt.Errorf("écriture claire %s : %w", p, err)
|
||||
}
|
||||
back, err := os.ReadFile(p)
|
||||
if err != nil || looksEncrypted(back) || string(back) != string(plain) {
|
||||
return fmt.Errorf("vérification post-déchiffrement de %s échouée", p)
|
||||
return bad, fmt.Errorf("vérification post-déchiffrement de %s échouée", p)
|
||||
}
|
||||
}
|
||||
return bad, nil
|
||||
}
|
||||
|
||||
// allMemFilesForDecrypt : TOUS les fichiers de la mémoire, sous-dossiers compris,
|
||||
// et les images de conversation (chatimg/, chiffrées comme le fil), sauf les
|
||||
// copies .bak et les écritures en cours (.tmp). Ne déchiffrer que les pages .md
|
||||
// laissait le reste chiffré sans plus aucune clé une fois le keyvault retiré.
|
||||
func allMemFilesForDecrypt() []string {
|
||||
var out []string
|
||||
for _, root := range []string{projectsRoot(), chatImgDir()} {
|
||||
_ = filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() || strings.HasSuffix(d.Name(), ".bak") || strings.HasSuffix(d.Name(), ".tmp") {
|
||||
return nil
|
||||
}
|
||||
out = append(out, p)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// quarantineUnreadable écrit les valeurs indéchiffrables, encore chiffrées, dans
|
||||
// un dossier daté de LOKI_HOME, avec une copie du keyvault à côté : elles
|
||||
// restent récupérables si l'ancienne clé est retrouvée.
|
||||
func quarantineUnreadable(bad []unreadableValue) error {
|
||||
dir := filepath.Join(LokiHome(), "chiffre-illisible-"+time.Now().Format("20060102-150405"))
|
||||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := json.MarshalIndent(bad, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := memWriteFileAtomic(filepath.Join(dir, "valeurs.json"), raw, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range []string{vaultPathPrimary(), vaultPathBackup()} {
|
||||
if b, err := os.ReadFile(p); err == nil {
|
||||
_ = memWriteFileAtomic(filepath.Join(dir, filepath.Base(p)), b, 0o600)
|
||||
}
|
||||
}
|
||||
if b := getBytes(bkState, vaultDBKey); len(b) > 0 {
|
||||
_ = memWriteFileAtomic(filepath.Join(dir, "keyvault-base.json"), b, 0o600)
|
||||
}
|
||||
for _, u := range bad {
|
||||
fmt.Fprintf(os.Stderr, "[mémoire] illisible avec la clé actuelle, mis en quarantaine : %s / %s (%s)\n", u.Bucket, u.Key, u.Error)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[mémoire] %d valeur(s) en quarantaine dans %s\n", len(bad), dir)
|
||||
return nil
|
||||
}
|
||||
|
||||
// scrubEncryptedResidue retire les .bak encore CHIFFRÉS de la mémoire une fois
|
||||
// le chiffrement désactivé : la clé est partie avec le keyvault, ils ne pourront
|
||||
// plus jamais être lus, et ce sont des copies de pages qui viennent d'être
|
||||
// remises en clair. Pendant symétrique de scrubPlaintextResidue. Les valeurs en
|
||||
// quarantaine et les snapshots vivent hors de memory/ : jamais touchés. Rien
|
||||
// n'est fait tant que le chiffrement est actif.
|
||||
func scrubEncryptedResidue() int {
|
||||
if memEncActive() {
|
||||
return 0
|
||||
}
|
||||
n := 0
|
||||
for _, root := range []string{projectsRoot(), chatImgDir()} {
|
||||
_ = filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".bak") {
|
||||
return nil
|
||||
}
|
||||
if b, err := os.ReadFile(p); err == nil && looksEncrypted(b) && os.Remove(p) == nil {
|
||||
n++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// resumeMemMigration reprend une migration interrompue au démarrage. Pour une
|
||||
// migration "encrypt", si la DEK est déjà en RAM (déverrouillée), on rejoue le
|
||||
// (re)chiffrement des pages claires ; sinon on laisse le journal en place pour un
|
||||
@@ -328,12 +434,12 @@ func resumeMemMigration() {
|
||||
// encore levé, on termine proprement (écrit le clair restant), puis on
|
||||
// baisse le drapeau. Sans DEK, on laisse le journal : l'état reste lisible
|
||||
// et l'écran de santé signalera le mélange.
|
||||
// Même chemin complet que DisableMemEncryption : la reprise retirait la clé
|
||||
// après les seules pages, en laissant les conversations chiffrées sans
|
||||
// plus aucun moyen de les rouvrir.
|
||||
if memEncActive() && memUnlocked() {
|
||||
if err := decryptAllPages(); err == nil {
|
||||
_ = SetConfigKey("MEM_ENCRYPTED", "")
|
||||
removeVault()
|
||||
clearMemDEK()
|
||||
clearMigrationJournal()
|
||||
if err := finishDecryption(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[mémoire] reprise du déchiffrement : %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user