Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app_sealed.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func (b sealedBackend) GetUnlockAttemptsRemaining() int { return b.store.GetUnlo
func (b sealedBackend) GetUnlockLockoutSeconds() int { return b.store.GetUnlockLockoutSeconds() }
func (b sealedBackend) IsEncryptionEnabled() bool { return b.store.IsEncryptionEnabled() }
func (b sealedBackend) IsEncryptionLocked() bool { return b.store.IsEncryptionLocked() }
func (b sealedBackend) IsDatabaseLeftClear() bool { return b.store.IsDatabaseLeftClear() }
func (b sealedBackend) UnlockDatabase(password string) error {
return b.store.UnlockDatabase(password)
}
Expand Down
5 changes: 5 additions & 0 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,11 @@ destinations' credentials and the sealed inbox key. The password itself is never
change of password, or the creation of a recovery key, replaces its key, which encrypts
everything again once.
- Once this version has written a database, versions before 1.8 can no longer open it.
- While SyslogStudio runs, the database it works on is decrypted; it is encrypted again when
SyslogStudio closes. If SyslogStudio stops without closing — a crash, a forced stop, a power cut —
the database is found unencrypted at the next start. SyslogStudio then says so on the unlock
screen and asks for the password as usual: the database opens as it was, and is encrypted again
when SyslogStudio closes. (Before 1.11, it was opened without the password and stayed unencrypted.)

### The recovery key

Expand Down
14 changes: 13 additions & 1 deletion frontend/src/components/UnlockScreen.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { _ } from 'svelte-i18n';
import { unlockDatabase, getUnlockLockoutSeconds } from '../lib/api';
import { unlockDatabase, getUnlockLockoutSeconds, isDatabaseLeftClear } from '../lib/api';
import { errorText, getRecoveryKey, resetEncryptionPassword, selectRecoveryKeyFile,
unlockWithRecoveryKeyFile } from '../lib/backup';

Expand All @@ -19,6 +19,9 @@

let password = '';
let error = '';
// Found in the clear: a session ended without encrypting it again.
let leftClear = false;
isDatabaseLeftClear().then(v => (leftClear = v)).catch(() => {});
let loading = false;
let lockedOut = false;
let lockoutSeconds = 0;
Expand Down Expand Up @@ -206,6 +209,9 @@
</div>
{:else}
<p class="subtitle">{$_('encryption.unlockMessage')}</p>
{#if leftClear}
<p class="left-clear">{$_('encryption.leftClear')}</p>
{/if}

<div class="input-group">
<!-- svelte-ignore a11y-autofocus -->
Expand Down Expand Up @@ -304,6 +310,12 @@
margin: 0;
}

.left-clear {
margin: 0 0 12px; padding: 8px 10px; max-width: 420px;
font-size: 12px; line-height: 1.5; text-align: left;
color: var(--text-primary); background: var(--warning-bg);
border: 1px solid var(--warning); border-radius: 6px;
}
.subtitle {
font-size: 13px;
color: var(--text-secondary);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const clearDatabase = (): Promise<void> => callGo('ClearDatabase');
// --- Encryption ---
export const isEncryptionEnabled = (): Promise<boolean> => callGo('IsEncryptionEnabled');
export const isEncryptionLocked = (): Promise<boolean> => callGo('IsEncryptionLocked');
export const isDatabaseLeftClear = (): Promise<boolean> => callGo('IsDatabaseLeftClear');
export const unlockDatabase = (password: string): Promise<void> => callGo('UnlockDatabase', password);
export const getUnlockLockoutSeconds = (): Promise<number> => callGo('GetUnlockLockoutSeconds');
export const enableEncryption = (password: string): Promise<void> => callGo('EnableEncryption', password);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@
"disableSuccess": "Verschlüsselung deaktiviert.",
"unlockTitle": "Datenbank gesperrt",
"unlockMessage": "Die Protokolldatenbank ist verschlüsselt. Geben Sie Ihr Passwort zum Entsperren ein.",
"leftClear": "Diese Datenbank wurde unverschlüsselt vorgefunden: SyslogStudio hatte sie entschlüsselt und wurde dann ohne Schließen beendet — Absturz, erzwungenes Beenden, Stromausfall. Geben Sie Ihr Passwort ein: Es wird geprüft, die Datenbank öffnet sich wie zuvor und wird beim Schließen von SyslogStudio wieder verschlüsselt.",
"unlock": "Entsperren",
"unlocking": "Wird entsperrt …",
"lockedOutSeconds": "Zu viele Fehlversuche. Gesperrt für {seconds}s.",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@
"disableSuccess": "Encryption disabled.",
"unlockTitle": "Database Locked",
"unlockMessage": "The log database is encrypted. Enter your password to unlock.",
"leftClear": "This database was found unencrypted: SyslogStudio had decrypted it and then stopped without closing — a crash, a forced stop, a power cut. Type your password: it is checked, the database opens as it was, and it is encrypted again when SyslogStudio closes.",
"unlock": "Unlock",
"unlocking": "Unlocking...",
"warningTitle": "Important — Read carefully",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@
"disableSuccess": "Cifrado desactivado.",
"unlockTitle": "Base de datos bloqueada",
"unlockMessage": "La base de datos de registros está cifrada. Introduce tu contraseña para desbloquear.",
"leftClear": "Esta base de datos se encontró sin cifrar: SyslogStudio la había descifrado y se detuvo sin cerrarse — un fallo, un cierre forzado, un corte de luz. Escriba su contraseña: se comprueba, la base se abre tal como estaba y se vuelve a cifrar al cerrar SyslogStudio.",
"unlock": "Desbloquear",
"unlocking": "Desbloqueando…",
"lockedOutSeconds": "Demasiados intentos fallidos. Bloqueado durante {seconds}s.",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@
"disableSuccess": "Chiffrement désactivé.",
"unlockTitle": "Base de données verrouillée",
"unlockMessage": "La base de données est chiffrée. Entrez votre mot de passe pour la déverrouiller.",
"leftClear": "Cette base a été trouvée non chiffrée : SyslogStudio l’avait déchiffrée puis s’est arrêté sans se fermer — plantage, arrêt forcé, coupure de courant. Saisissez votre mot de passe : il est vérifié, la base s’ouvre telle qu’elle était et elle est chiffrée à nouveau à la fermeture de SyslogStudio.",
"unlock": "Déverrouiller",
"unlocking": "Déverrouillage...",
"warningTitle": "Important — Lisez attentivement",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@
"disableSuccess": "Crittografia disabilitata.",
"unlockTitle": "Database bloccato",
"unlockMessage": "Il database dei log è cifrato. Inserisci la password per sbloccarlo.",
"leftClear": "Questo database è stato trovato non cifrato: SyslogStudio lo aveva decifrato e si è fermato senza chiudersi — un crash, un arresto forzato, un’interruzione di corrente. Inserisci la password: viene verificata, il database si apre com’era e viene cifrato di nuovo alla chiusura di SyslogStudio.",
"unlock": "Sblocca",
"unlocking": "Sblocco in corso…",
"lockedOutSeconds": "Troppi tentativi falliti. Bloccato per {seconds}s.",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@
"disableSuccess": "暗号化を無効にしました。",
"unlockTitle": "データベースがロックされています",
"unlockMessage": "ログデータベースは暗号化されています。ロックを解除するにはパスワードを入力してください。",
"leftClear": "このデータベースは暗号化されていない状態で見つかりました。SyslogStudio が復号したあと、正常に終了せずに停止しました (クラッシュ、強制終了、停電など)。パスワードを入力してください。パスワードが確認されるとデータベースはそのまま開き、SyslogStudio の終了時に再び暗号化されます。",
"unlock": "ロック解除",
"unlocking": "ロック解除中…",
"lockedOutSeconds": "失敗回数が多すぎます。{seconds}秒間ロックされます。",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@
"disableSuccess": "Criptografia desativada.",
"unlockTitle": "Banco de dados bloqueado",
"unlockMessage": "O banco de dados de logs está criptografado. Digite sua senha para desbloquear.",
"leftClear": "Esta base de dados foi encontrada sem cifra: o SyslogStudio tinha-a decifrado e parou sem fechar — uma falha, uma paragem forçada, um corte de energia. Introduza a sua palavra-passe: é verificada, a base abre-se como estava e volta a ser cifrada quando o SyslogStudio fechar.",
"unlock": "Desbloquear",
"unlocking": "Desbloqueando…",
"lockedOutSeconds": "Muitas tentativas com falha. Bloqueado por {seconds}s.",
Expand Down
1 change: 1 addition & 0 deletions frontend/src/lib/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@
"disableSuccess": "已禁用加密。",
"unlockTitle": "数据库已锁定",
"unlockMessage": "日志数据库已加密。请输入密码以解锁。",
"leftClear": "发现此数据库未加密:SyslogStudio 解密后未正常关闭就停止了(崩溃、强制停止或断电)。请输入密码:密码验证通过后,数据库按原样打开,并在 SyslogStudio 关闭时重新加密。",
"unlock": "解锁",
"unlocking": "正在解锁…",
"lockedOutSeconds": "失败次数过多。已锁定 {seconds} 秒。",
Expand Down
2 changes: 2 additions & 0 deletions frontend/wailsjs/go/main/App.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ export function InstallSystemService(arg1:boolean,arg2:boolean):Promise<void>;

export function IsCAKeyUnencrypted():Promise<boolean>;

export function IsDatabaseLeftClear():Promise<boolean>;

export function IsEncryptionEnabled():Promise<boolean>;

export function IsEncryptionLocked():Promise<boolean>;
Expand Down
4 changes: 4 additions & 0 deletions frontend/wailsjs/go/main/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ export function IsCAKeyUnencrypted() {
return window['go']['main']['App']['IsCAKeyUnencrypted']();
}

export function IsDatabaseLeftClear() {
return window['go']['main']['App']['IsDatabaseLeftClear']();
}

export function IsEncryptionEnabled() {
return window['go']['main']['App']['IsEncryptionEnabled']();
}
Expand Down
15 changes: 15 additions & 0 deletions internal/engine/encryption.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ func (e *Engine) IsEncryptionEnabled() bool {
return e.configStore.LoadStorage().EncryptionEnabled
}

// IsDatabaseLeftClear says the locked database was found in the clear, left
// so by a session that ended without closing: the password encrypts it again.
func (e *Engine) IsDatabaseLeftClear() bool {
return e.logStore != nil && e.logStore.LeftClear()
}

// IsEncryptionLocked returns true if the database is encrypted and not yet unlocked.
func (e *Engine) IsEncryptionLocked() bool {
return e.logStore != nil && e.logStore.IsLocked()
Expand Down Expand Up @@ -113,6 +119,15 @@ func (e *Engine) UnlockDatabase(password string) error {
var prev string
var usedPrev bool
master, legacy, err := e.masterFor(password)
if err == nil && legacy && e.logStore.LeftClear() {
// No keyring to check it, and no encrypted database to fail on: the
// password is about to encrypt everything again, and a mistyped one
// would lock its owner out. The other files it encrypts say whether
// it is the one.
if rerr := e.readableWith(password); rerr != nil {
err = fmt.Errorf("wrong password: %w", rerr)
}
}
if err == nil {
prev, usedPrev, err = e.unlockStore(master)
}
Expand Down
136 changes: 136 additions & 0 deletions internal/engine/leftclear_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package engine

import (
"io"
"os"
"path/filepath"
"testing"
"time"

"SyslogStudio/internal/event"
"SyslogStudio/internal/models"
"SyslogStudio/internal/storage"
)

// copyDir copies what a running engine has on disk, as a process killed
// mid-session leaves it: the database in the clear, its WAL, the keyring and
// the configuration. Not the engine's lock, which dies with its process.
func copyDir(t *testing.T, from, to string) {
t.Helper()
entries, err := os.ReadDir(from)
if err != nil {
t.Fatal(err)
}
for _, e := range entries {
if e.IsDir() || e.Name() == "engine.lock" {
continue
}
src, err := os.Open(filepath.Join(from, e.Name()))
if err != nil {
t.Fatal(err)
}
dst, err := os.Create(filepath.Join(to, e.Name()))
if err != nil {
src.Close()
t.Fatal(err)
}
_, err = io.Copy(dst, src)
src.Close()
dst.Close()
if err != nil {
t.Fatal(err)
}
}
}

func openAt(t *testing.T, dir string) *Engine {
t.Helper()
e := New(storage.NewConfigStoreAt(dir))
if err := e.Open(event.NewMockEventEmitter()); err != nil {
t.Fatal(err)
}
return e
}

// An encrypted database decrypted by a session that was then killed — a
// crash, a forced stop, the power — was found in the clear at the next start,
// opened without its password, and never encrypted again. It now waits for
// the password, opens as it was, and is encrypted again when closed.
func TestEncryption_LeftInTheClearByAKilledSession(t *testing.T) {
const password = "correct horse battery"
at := time.Now().Add(-time.Hour).UTC()
var msgs []models.SyslogMessage
for _, id := range []string{"a", "b", "c"} {
msgs = append(msgs, models.SyslogMessage{ID: id, Timestamp: at, ReceivedAt: at, Severity: models.SevNotice,
SeverityLabel: "Notice", FacilityLabel: "user", Message: "message " + id, RawMessage: "message " + id,
SourceIP: "10.0.0.1", Protocol: "UDP"})
}

encrypted, _ := openEngine(t, msgs)
dir := encrypted.DataDir()
if err := encrypted.EnableEncryption(password); err != nil {
t.Fatal(err)
}
encrypted.Close()

// Unlocked, then killed: copied while it runs.
running := openAt(t, dir)
if err := running.UnlockDatabase(password); err != nil {
t.Fatal(err)
}
crashed := t.TempDir()
copyDir(t, dir, crashed)
running.Close()
dbPath := storage.DBPathIn(crashed)
if _, err := os.Stat(dbPath + ".enc"); !os.IsNotExist(err) {
t.Fatalf("the killed session's copy still has its encrypted file: %v", err)
}

e := openAt(t, crashed)
if !e.IsEncryptionLocked() || !e.IsDatabaseLeftClear() {
t.Fatalf("found in the clear and opened: locked %v, left clear %v", e.IsEncryptionLocked(), e.IsDatabaseLeftClear())
}
if err := e.UnlockDatabase("not the password"); err == nil {
t.Fatal("a wrong password opened it")
}
if err := e.UnlockDatabase(password); err != nil {
t.Fatal(err)
}
if e.IsDatabaseLeftClear() {
t.Error("still said to be left in the clear once unlocked")
}
if got := e.QueryMessages(models.QueryOptions{PageSize: 10}).Total; got != len(msgs) {
t.Fatalf("%d messages after unlocking, want %d", got, len(msgs))
}
e.Close()
if _, err := os.Stat(dbPath + ".enc"); err != nil {
t.Fatalf("not encrypted again when closed: %v", err)
}
if _, err := os.Stat(dbPath); !os.IsNotExist(err) {
t.Fatalf("still in the clear after closing: %v", err)
}

// And it opens as any encrypted database does.
again := openAt(t, crashed)
defer again.Close()
if !again.IsEncryptionLocked() || again.IsDatabaseLeftClear() {
t.Fatalf("after closing: locked %v, left clear %v", again.IsEncryptionLocked(), again.IsDatabaseLeftClear())
}
if err := again.UnlockDatabase(password); err != nil {
t.Fatal(err)
}
if got := again.QueryMessages(models.QueryOptions{PageSize: 10}).Total; got != len(msgs) {
t.Fatalf("%d messages at the next unlock, want %d", got, len(msgs))
}
}

// A database in the clear with encryption off is just a database: it opens.
func TestEncryption_ClearDatabaseWithEncryptionOffOpens(t *testing.T) {
e, dir := openEngine(t, nil)
e.Close()
again := openAt(t, dir)
defer again.Close()
if again.IsEncryptionLocked() || again.IsDatabaseLeftClear() {
t.Fatal("a database never encrypted waits for a password")
}
}
1 change: 1 addition & 0 deletions internal/remote/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ type Backend interface {
GetUnlockLockoutSeconds() int
IsEncryptionEnabled() bool
IsEncryptionLocked() bool
IsDatabaseLeftClear() bool
UnlockDatabase(password string) error
EnableEncryption(password string) error
DisableEncryption(password string) error
Expand Down
6 changes: 6 additions & 0 deletions internal/remote/client_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions internal/remote/switch_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading