From 1acb3e6d78328c980e860eb8f68e7d48ed875167 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 20 Jan 2026 20:02:41 +0100 Subject: [PATCH 01/15] Remove obsolete build flag from main.go --- main.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/main.go b/main.go index be1df12..24e991c 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,3 @@ -//go:build !addtestdata -// +build !addtestdata - // Copyright (c) 2026, Julian Müller (ChaoticByte) package main From 41ffde2ce8248d69417bffab2ead3f3a63b9d1d9 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 20 Jan 2026 20:42:05 +0100 Subject: [PATCH 02/15] Limit entry size to 2^32 - 1 --- data.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data.go b/data.go index 425a119..31c0d76 100644 --- a/data.go +++ b/data.go @@ -229,6 +229,8 @@ func OpenJournalFile(file string, password *memguard.Enclave) (*JournalFile, err } +const MaxEntrySize = 4294967296-1 // 2^32 - 1 + type EncryptedEntry struct { Timestamp uint64 // Unix time in microseconds, works until year 294246 Salt [12]byte @@ -247,6 +249,9 @@ func (e *EncryptedEntry) EtLength() uint32 { func NewEncryptedEntry(text string, password *memguard.Enclave) (*EncryptedEntry, error) { e := EncryptedEntry{} + if len(text) > (MaxEntrySize)-1 { + text = text[:MaxEntrySize+1] + } e.Timestamp = uint64(time.Now().UnixMicro()) ct, s, n, err := EncryptText(password, text, e.Timestamp) if err != nil { From 718b217d3af000dc726f73f10ad65c52cface474 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 20 Jan 2026 21:04:54 +0100 Subject: [PATCH 03/15] MultiChoiceOrCommand: put newline on Ctrl+D (EOF) --- tui.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tui.go b/tui.go index 377367d..7219021 100644 --- a/tui.go +++ b/tui.go @@ -94,7 +94,8 @@ func MultiChoiceOrCommand(choices [][2]string, commands []string, prompt string, for { // read lines until a valid choice or command is entered Out(Am(AC_SET_BOLD, AC_COL_BRIGHT_YELLOW_FG), "> ", Am(AC_RESET_BOLD, AC_COL_RESET_FG)) - a, _ := Readline() + a, err := Readline() + if err == io.EOF { Nl(); continue } for i, c := range choices { if c[0] == a { return i From 85c3693de3b2b4f070fbb15a8d27ffd36ac2c44a Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 20 Jan 2026 21:41:20 +0100 Subject: [PATCH 04/15] Add the dd (delete previous line) command to the editor --- tui.go | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/tui.go b/tui.go index 7219021..2d6eab3 100644 --- a/tui.go +++ b/tui.go @@ -465,33 +465,56 @@ func mainloop(passwd *memguard.Enclave) int { mode = lastMode } - Out(Am(AC_COL_GREEN_FG), - "Write your new entry. Save it by hitting Ctrl+D in an empty line.", - Am(AC_COL_RESET_FG)) - Nnl(2) + header := func () { + Out(Am(AC_COL_GREEN_FG), + "Write a new entry; ", + Am(AC_COL_RESET_FG, AC_SET_DIM), + "Save it by hitting ", Am(AC_RESET_DIM), "Ctrl+D", + Am(AC_SET_DIM), " in an empty line.\n", + "You can delete the previous line with ", + Am(AC_RESET_DIM), "dd", Am(AC_SET_DIM), + " and ", Am(AC_RESET_DIM), "Enter", Am(AC_RESET_DIM), ".") + Nnl(2) + } + + header() // read text from stdin (rune by rune) - builder := strings.Builder{} - reader := bufio.NewReader(os.Stdin) + lines := []string{} for { - r, _, err := reader.ReadRune() - if err == io.EOF { break } - builder.WriteRune(r) - if err != nil { + line, err := Readline() + if err == io.EOF { + break + } else if err != nil { handleErr(err, "Couldn't read terminal input") } + if line == "dd" { + ll := len(lines) + if ll < 1 { + lines = []string{} + } else { + lines = lines[:ll-1] + } + Out(AS_RESET, AS_CUR_HOME) + header() + for _, l := range lines { + Out(l); Nl() + } + } else { + lines = append(lines, line) + } } // Try to create new EncryptedEntry from the input text - e, err := NewEncryptedEntry(strings.Trim(builder.String(), " \n"), passwd) + e, err := NewEncryptedEntry(strings.Trim(strings.Join(lines, "\n"), " \n"), passwd) if err != nil { handleErr(err, "Error creating new entry") continue } // empty input - builder.Reset() + lines = nil err = j.AddEntry(e) if err != nil { From 0ed0ada50352c8168e5825378f4560e1553f35c8 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 20 Jan 2026 21:56:25 +0100 Subject: [PATCH 05/15] Fix MaxEntrySize input limit --- data.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data.go b/data.go index 31c0d76..a9fda4c 100644 --- a/data.go +++ b/data.go @@ -229,7 +229,7 @@ func OpenJournalFile(file string, password *memguard.Enclave) (*JournalFile, err } -const MaxEntrySize = 4294967296-1 // 2^32 - 1 +const MaxEntrySize = uint32(4294967295) // 2^31 type EncryptedEntry struct { Timestamp uint64 // Unix time in microseconds, works until year 294246 @@ -249,8 +249,8 @@ func (e *EncryptedEntry) EtLength() uint32 { func NewEncryptedEntry(text string, password *memguard.Enclave) (*EncryptedEntry, error) { e := EncryptedEntry{} - if len(text) > (MaxEntrySize)-1 { - text = text[:MaxEntrySize+1] + if uint32(len(text)) > MaxEntrySize { + text = text[:MaxEntrySize] } e.Timestamp = uint64(time.Now().UnixMicro()) ct, s, n, err := EncryptText(password, text, e.Timestamp) From 52b9bf410ade7e06d4cd22d6c08d1178a142fcc2 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 20 Jan 2026 22:09:19 +0100 Subject: [PATCH 06/15] Drop support for 32bit --- build.sh | 2 -- 1 file changed, 2 deletions(-) diff --git a/build.sh b/build.sh index d945025..0b6e755 100755 --- a/build.sh +++ b/build.sh @@ -14,7 +14,5 @@ function build() { echo Output dir: ./dist/${VERSION}/ -build linux "386" i386 build linux amd64 amd64 -build linux arm arm build linux arm64 arm64 From 50c372518972b25619be8941a9239d32a13d6590 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 21 Jan 2026 18:40:48 +0100 Subject: [PATCH 07/15] Add tests for encryption and kdf --- encrypt_test.go | 105 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 encrypt_test.go diff --git a/encrypt_test.go b/encrypt_test.go new file mode 100644 index 0000000..b6e8e4d --- /dev/null +++ b/encrypt_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2026, Julian Müller (ChaoticByte) + +package main + +import ( + crand "crypto/rand" + "math/rand" + "slices" + "testing" + "time" + + "github.com/awnumar/memguard" +) + +func TestKeyDerivation(t *testing.T) { + password1 := []byte("test") + password2 := make([]byte, 20) + crand.Read(password2) + salt1 := [12]byte{} + crand.Read(salt1[:]) + salt2 := [12]byte{} + crand.Read(salt2[:]) + // + key1 := derive_key(password1, salt1) + key2 := derive_key(password2, salt2) + // + if key1 == key2 { t.Error("derived key1 == key2!") } + // + key1_salt2 := derive_key(password1, salt2) + key2_salt1 := derive_key(password2, salt1) + if key1 == key1_salt2 { t.Error("derived key1 == (key1 with wrong salt)!") } + if key2 == key2_salt1 { t.Error("derived key2 == (key2 with wrong salt)!") } + // + rekey1 := derive_key(password1, salt1) + rekey2 := derive_key(password2, salt2) + if rekey1 != key1 { t.Error("kdf is non-deterministic! derived key1 != re-key1!") } + if rekey2 != key2 { t.Error("kdf is non-deterministic! derived key2 != re-key2!") } +} + +func trialWithTamperedInput(t *testing.T, what string, password *memguard.Enclave, ciphertext []byte, salt [12]byte, noncePfx [16]byte, time uint64, original string) { + clt_t, err := DecryptText(password, ciphertext, salt, noncePfx, time) + if err == nil || err.Error() != "chacha20poly1305: message authentication failed" { + t.Errorf("Could decrypt with tampered %v; message authentication not functioning properly!", what) + } else if clt_t == original { + t.Errorf("Could decrypt with tampered %v and no error given by aead.Open()! message authentication not functioning properly!", what) + } +} + +func TestCrypto(t *testing.T) { + password1 := memguard.NewEnclave([]byte("test")) + password2_bytes := make([]byte, 20) + crand.Read(password2_bytes) + password2 := memguard.NewEnclave(password2_bytes) + t1 := uint64(time.Now().UnixMicro()) + time.Sleep(time.Duration(1.0 + rand.Float64()) * time.Second) + t2 := uint64(time.Now().UnixMicro()) + cleartext := "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet." + // + cit1, salt1, noncePfx1, err1 := EncryptText(password1, cleartext, t1) + if err1 != nil { t.Fatalf("Could not encrypt with password1, err: %v", err1) } + cit2, salt2, noncePfx2, err2 := EncryptText(password2, cleartext, t2) + if err2 != nil { t.Fatalf("Could not encrypt with password2, err: %v", err2) } + // + if salt1 == salt2 { + t.Error("salt1 and salt2 are the same!") + } + if noncePfx1 == noncePfx2 { + t.Error("random part of nonce 1 and 2 are the same") + } + if slices.Equal(cit1, []byte(cleartext)) { + t.Error("ciphertext1 == cleartext!") + } + if slices.Equal(cit2, []byte(cleartext)) { + t.Error("ciphertext2 == cleartext!") + } + // + clt_decrypted1, err := DecryptText(password1, cit1, salt1, noncePfx1, t1) + if err != nil { + t.Error("Could not decrypt ciphertext1 using password1!") + } + if clt_decrypted1 != cleartext { + t.Error("Decrypted ciphertext1 does not equal original ciphertext!") + } + clt_decrypted2, err := DecryptText(password2, cit2, salt2, noncePfx2, t2) + if err != nil { + t.Error("Could not decrypt ciphertext2 using password1!") + } + if clt_decrypted2 != cleartext { + t.Error("Decrypted ciphertext2 does not equal original ciphertext!") + } + // + trialWithTamperedInput(t, "wrong password", password2, cit1, salt1, noncePfx1, t1, cleartext) + trialWithTamperedInput(t, "wrong password", password1, cit2, salt2, noncePfx2, t2, cleartext) + trialWithTamperedInput(t, "tampered nonce prefix", password1, cit1, salt1, noncePfx2, t1, cleartext) + trialWithTamperedInput(t, "tampered nonce prefix", password2, cit2, salt2, noncePfx1, t2, cleartext) + // + cit1_tampered := make([]byte, len(cit1)) + copy(cit1_tampered, cit1) + if cit1_tampered[3] < 255 { + cit1_tampered[3] += 1 + } else { + cit1_tampered[3] -= 1 + } + trialWithTamperedInput(t, "tampered ciphertext", password1, cit1_tampered, salt1, noncePfx1, t1, cleartext) +} From c190a0cfcd059b0fb078f76cfa75620f771b04c5 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 21 Jan 2026 20:58:59 +0100 Subject: [PATCH 08/15] Add workflow for testing --- .forgejo/workflows/test.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .forgejo/workflows/test.yml diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 0000000..68ce3f4 --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,13 @@ +on: + push: + branches: + - main + paths: + - "*.go" +jobs: + run-tests: + runs-on: default + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + - run: go test From 944cd97c9c884cff946b77a12a1357323d645627 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 21 Jan 2026 21:05:57 +0100 Subject: [PATCH 09/15] Fix runner label in test workflow --- .forgejo/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 68ce3f4..12ad4a2 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -6,7 +6,7 @@ on: - "*.go" jobs: run-tests: - runs-on: default + runs-on: docker steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 From 93a5a6cb1bf1c0dfdb6aba20811239ef8e75efff Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 21 Jan 2026 21:43:49 +0100 Subject: [PATCH 10/15] Allow starting test workflow from the scm webinterface --- .forgejo/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index 12ad4a2..ad7d557 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -4,6 +4,7 @@ on: - main paths: - "*.go" + workflow_dispatch: jobs: run-tests: runs-on: docker From c96c38ec9707739289c77656ead1a783bf9f2ba6 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 21 Jan 2026 21:52:45 +0100 Subject: [PATCH 11/15] Fix test workflow --- .forgejo/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index ad7d557..dd60a5c 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -8,7 +8,10 @@ on: jobs: run-tests: runs-on: docker + container: "node:24-bookworm" steps: - uses: actions/checkout@v6 - uses: actions/setup-go@v6 + with: + go-version: "1.25" - run: go test From 2679297b73316797cffed4ff273ae96eacaa3401 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Thu, 22 Jan 2026 18:35:05 +0100 Subject: [PATCH 12/15] Add -v option to go test in test workflow --- .forgejo/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index dd60a5c..d1cdd07 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -14,4 +14,4 @@ jobs: - uses: actions/setup-go@v6 with: go-version: "1.25" - - run: go test + - run: go test -v From 64ab24aa8a544c71eb3e921154451cee5dbf8cca Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Thu, 22 Jan 2026 18:35:35 +0100 Subject: [PATCH 13/15] Fix misleading comment in data.go --- data.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data.go b/data.go index a9fda4c..a88b9c7 100644 --- a/data.go +++ b/data.go @@ -229,7 +229,7 @@ func OpenJournalFile(file string, password *memguard.Enclave) (*JournalFile, err } -const MaxEntrySize = uint32(4294967295) // 2^31 +const MaxEntrySize = uint32(4294967295) // (2^32)-1 type EncryptedEntry struct { Timestamp uint64 // Unix time in microseconds, works until year 294246 From d63aa0474f181e02283931a762c7c4665e12bc66 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Thu, 22 Jan 2026 19:51:41 +0100 Subject: [PATCH 14/15] Add test for data.go --- data_test.go | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 data_test.go diff --git a/data_test.go b/data_test.go new file mode 100644 index 0000000..738c1cb --- /dev/null +++ b/data_test.go @@ -0,0 +1,119 @@ +// Copyright (c) 2026, Julian Müller (ChaoticByte) + +package main + +import ( + "fmt" + "math/rand" + "os" + "slices" + "strings" + "testing" + "time" + + "github.com/awnumar/memguard" +) + +const JournalTestFile = "/tmp/journal_test" + +func TestDataformat(t *testing.T) { + passwd := memguard.NewEnclave([]byte("secureTestP4ssw0rd!")) + defer memguard.Purge() + defer os.Remove(JournalTestFile) + var j *JournalFile + var err error + t.Run("CreateJournalFile", func(t *testing.T) { + // create test journal + os.Remove(JournalTestFile) // possibly remove old file + j, err = OpenJournalFile(JournalTestFile, passwd) + if err != nil { + t.Error("Could not create test journal; ", err) + } + }) + // define entries + entryTexts := []string { + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.", + "Sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.", + "Sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", + "Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", + "This is just a test", + "Another test", + "test", + "aaaa", + } + tb := strings.Builder{} + for i := range 1024*1024 { // big entry + fmt.Fprintf(&tb, "%x", i) + if err != nil { + t.Error("Unknown error when writing to string builder") + } + } + entryTexts = append(entryTexts, tb.String()) + t.Run("CreateAndAddEntries", func(t *testing.T) { + for i, txt := range entryTexts { + e, err := NewEncryptedEntry(txt, passwd) + if err != nil { + t.Errorf("Could not create entry %v! %v", i, err) + } + err = j.AddEntry(e) + if err != nil { + t.Errorf("Could not add entry %v to journal! %v", i, err) + } + j.Write() + time.Sleep(time.Duration(rand.Float64() + 1.0) * time.Second) + } + }) + t.Run("ReopenJournalFile", func(t *testing.T) { + // re-open + j.Close() + j, err = OpenJournalFile(JournalTestFile, passwd) + if err != nil { + t.Error("Could not open test journal; ", err) + } + }) + t.Run("ReadEntries", func(t *testing.T) { + es := j.GetEntries() + len_es := len(es) + lenInput := len(entryTexts) + if len_es != lenInput { + t.Errorf("Invalid number of entries! Expected %v, but got %v", lenInput, len_es) + } + slices.Sort(es) // this is important to get the right order! + for i, ts := range es { + e := j.GetEntry(ts) + if e == nil { + t.Errorf("Could not get entry %v!", ts) + } + txt, err := e.Decrypt(passwd) + if err != nil { + t.Errorf("Could not decrypt entry %v! %v", ts, err) + } + if txt != entryTexts[i] { + t.Errorf("Decrypted text of entry %v does not match input text!", ts) + } + } + }) + var removed_ts uint64 + t.Run("RemoveEntries", func(t *testing.T) { + removed_ts = j.GetEntries()[0] + err := j.DeleteEntry(removed_ts) + if err != nil { + t.Errorf("Could not delete entry %v from journal!", removed_ts) + } + }) + t.Run("ReopenJournalFile2", func(t *testing.T) { + // re-open + j.Close() + j, err = OpenJournalFile(JournalTestFile, passwd) + if err != nil { + t.Error("Could not open test journal; ", err) + } + }) + t.Run("CheckRemovedEntries", func(t *testing.T) { + for _, ts := range j.GetEntries() { + if ts == removed_ts { + t.Error("Found deleted entry!") + } + } + }) +} From f9ca09c25bf5fc4d68314d8750aed2b603334691 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Thu, 22 Jan 2026 20:03:41 +0100 Subject: [PATCH 15/15] Optimize workflow test by using go container image --- .forgejo/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml index d1cdd07..bc50629 100644 --- a/.forgejo/workflows/test.yml +++ b/.forgejo/workflows/test.yml @@ -8,10 +8,10 @@ on: jobs: run-tests: runs-on: docker - container: "node:24-bookworm" + container: "golang:1.25" steps: - - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: "1.25" + - run: apt install git + # always runs on code from main + - run: git clone -b ${{ env.FORGEJO_REF_NAME }} https://remotebranch.eu/ChaoticByte/journal.git - run: go test -v + working-directory: ./journal