Remove encrypted fields from notes and books

This commit is contained in:
Sung 2025-10-19 15:47:34 -07:00
commit bb84369611
9 changed files with 47 additions and 325 deletions

View file

@ -97,8 +97,7 @@ func (l syncList) getLength() int {
return len(l.Notes) + len(l.Books) + len(l.ExpungedNotes) + len(l.ExpungedBooks)
}
// processFragments categorizes items in sync fragments into a sync list. It also decrypts any
// encrypted data in sync fragments.
// processFragments categorizes items in sync fragments into a sync list.
func processFragments(fragments []client.SyncFragment) (syncList, error) {
notes := map[string]client.SyncFragNote{}
books := map[string]client.SyncFragBook{}

View file

@ -1,123 +0,0 @@
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
// Package crypt provides cryptographic funcitonalities
package crypt
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"io"
"github.com/pkg/errors"
"golang.org/x/crypto/hkdf"
"golang.org/x/crypto/pbkdf2"
)
var aesGcmNonceSize = 12
func runHkdf(secret, salt, info []byte) ([]byte, error) {
r := hkdf.New(sha256.New, secret, salt, info)
ret := make([]byte, 32)
_, err := io.ReadFull(r, ret)
if err != nil {
return []byte{}, errors.Wrap(err, "reading key bytes")
}
return ret, nil
}
// MakeKeys derives, from the given credential, a key set comprising of an encryption key
// and an authentication key
func MakeKeys(password, email []byte, iteration int) ([]byte, []byte, error) {
masterKey := pbkdf2.Key([]byte(password), []byte(email), iteration, 32, sha256.New)
authKey, err := runHkdf(masterKey, email, []byte("auth"))
if err != nil {
return nil, nil, errors.Wrap(err, "deriving auth key")
}
return masterKey, authKey, nil
}
// AesGcmEncrypt encrypts the plaintext using AES in a GCM mode. It returns
// a ciphertext prepended by a 12 byte pseudo-random nonce, encoded in base64.
func AesGcmEncrypt(key, plaintext []byte) (string, error) {
if key == nil {
return "", errors.New("no key provided")
}
block, err := aes.NewCipher(key)
if err != nil {
return "", errors.Wrap(err, "initializing aes")
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return "", errors.Wrap(err, "initializing gcm")
}
nonce := make([]byte, aesGcmNonceSize)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", errors.Wrap(err, "generating nonce")
}
ciphertext := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil)
cipherKeyB64 := base64.StdEncoding.EncodeToString(ciphertext)
return cipherKeyB64, nil
}
// AesGcmDecrypt decrypts the encrypted data using AES in a GCM mode. The data should be
// a base64 encoded string in the format of 12 byte nonce followed by a ciphertext.
func AesGcmDecrypt(key []byte, dataB64 string) ([]byte, error) {
if key == nil {
return nil, errors.New("no key provided")
}
data, err := base64.StdEncoding.DecodeString(dataB64)
if err != nil {
return nil, errors.Wrap(err, "decoding base64 data")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, errors.Wrap(err, "initializing aes")
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, errors.Wrap(err, "initializing gcm")
}
if len(data) < aesGcmNonceSize {
return nil, errors.Wrap(err, "malformed data")
}
nonce, ciphertext := data[:aesGcmNonceSize], data[aesGcmNonceSize:]
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, errors.Wrap(err, "decrypting")
}
return plaintext, nil
}

View file

@ -1,118 +0,0 @@
/* Copyright (C) 2019, 2020, 2021, 2022, 2023, 2024, 2025 Dnote contributors
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
package crypt
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"fmt"
"testing"
"github.com/dnote/dnote/pkg/assert"
"github.com/pkg/errors"
)
func TestAesGcmEncrypt(t *testing.T) {
testCases := []struct {
key []byte
plaintext []byte
}{
{
key: []byte("AES256Key-32Characters1234567890"),
plaintext: []byte("foo bar baz quz"),
},
{
key: []byte("AES256Key-32Charactersabcdefghij"),
plaintext: []byte("1234 foo 5678 bar 7890 baz"),
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("key %s plaintext %s", tc.key, tc.plaintext), func(t *testing.T) {
// encrypt
dataB64, err := AesGcmEncrypt(tc.key, tc.plaintext)
if err != nil {
t.Fatal(errors.Wrap(err, "performing encryption"))
}
// test that data can be decrypted
data, err := base64.StdEncoding.DecodeString(dataB64)
if err != nil {
t.Fatal(errors.Wrap(err, "decoding data from base64"))
}
nonce, ciphertext := data[:12], data[12:]
fmt.Println(string(data))
block, err := aes.NewCipher([]byte(tc.key))
if err != nil {
t.Fatal(errors.Wrap(err, "initializing aes"))
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
t.Fatal(errors.Wrap(err, "initializing gcm"))
}
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
t.Fatal(errors.Wrap(err, "decode"))
}
assert.DeepEqual(t, plaintext, tc.plaintext, "plaintext mismatch")
})
}
}
func TestAesGcmDecrypt(t *testing.T) {
testCases := []struct {
key []byte
ciphertextB64 string
expectedPlaintext string
}{
{
key: []byte("AES256Key-32Characters1234567890"),
ciphertextB64: "M2ov9hWMQ52v1S/zigwX3bJt4cVCV02uiRm/grKqN/rZxNkJrD7vK4Ii0g==",
expectedPlaintext: "foo bar baz quz",
},
{
key: []byte("AES256Key-32Characters1234567890"),
ciphertextB64: "M4csFKUIUbD1FBEzLgHjscoKgN0lhMGJ0n2nKWiCkE/qSKlRP7kS",
expectedPlaintext: "foo\n1\nbar\n2",
},
{
key: []byte("AES256Key-32Characters1234567890"),
ciphertextB64: "pe/fnw73MR1clmVIlRSJ5gDwBdnPly/DF7DsR5dJVz4dHZlv0b10WzvJEGOCHZEr+Q==",
expectedPlaintext: "föo\nbār\nbåz & qūz",
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("key %s ciphertext %s", tc.key, tc.ciphertextB64), func(t *testing.T) {
plaintext, err := AesGcmDecrypt(tc.key, tc.ciphertextB64)
if err != nil {
t.Fatal(errors.Wrap(err, "performing decryption"))
}
assert.DeepEqual(t, plaintext, []byte(tc.expectedPlaintext), "plaintext mismatch")
})
}
}

View file

@ -41,12 +41,11 @@ func (a *App) CreateBook(user database.User, name string) (database.Book, error)
}
book := database.Book{
UUID: uuid,
UserID: user.ID,
Label: name,
AddedOn: a.Clock.Now().UnixNano(),
USN: nextUSN,
Encrypted: false,
UUID: uuid,
UserID: user.ID,
Label: name,
AddedOn: a.Clock.Now().UnixNano(),
USN: nextUSN,
}
if err := tx.Create(&book).Error; err != nil {
tx.Rollback()
@ -99,8 +98,6 @@ func (a *App) UpdateBook(tx *gorm.DB, user database.User, book database.Book, la
book.USN = nextUSN
book.EditedOn = a.Clock.Now().UnixNano()
book.Deleted = false
// TODO: remove after all users have been migrated
book.Encrypted = false
if err := tx.Save(&book).Error; err != nil {
return book, errors.Wrap(err, "updating the book")

View file

@ -59,16 +59,15 @@ func (a *App) CreateNote(user database.User, bookUUID, content string, addedOn *
}
note := database.Note{
UUID: uuid,
BookUUID: bookUUID,
UserID: user.ID,
AddedOn: noteAddedOn,
EditedOn: noteEditedOn,
USN: nextUSN,
Body: content,
Public: public,
Encrypted: false,
Client: client,
UUID: uuid,
BookUUID: bookUUID,
UserID: user.ID,
AddedOn: noteAddedOn,
EditedOn: noteEditedOn,
USN: nextUSN,
Body: content,
Public: public,
Client: client,
}
if err := tx.Create(&note).Error; err != nil {
tx.Rollback()
@ -134,8 +133,6 @@ func (a *App) UpdateNote(tx *gorm.DB, user database.User, note database.Note, p
note.USN = nextUSN
note.EditedOn = a.Clock.Now().UnixNano()
note.Deleted = false
// TODO: remove after all users are migrated
note.Encrypted = false
if err := tx.Save(&note).Error; err != nil {
return note, pkgErrors.Wrap(err, "editing note")
@ -180,13 +177,12 @@ func (a *App) GetUserNoteByUUID(userID int, uuid string) (*database.Note, error)
// GetNotesParams is params for finding notes
type GetNotesParams struct {
Year int
Month int
Page int
Books []string
Search string
Encrypted bool
PerPage int
Year int
Month int
Page int
Books []string
Search string
PerPage int
}
type ftsParams struct {
@ -215,14 +211,13 @@ notes.added_on,
notes.edited_on,
notes.usn,
notes.deleted,
notes.encrypted,
` + bodyExpr)
}
func getNotesBaseQuery(db *gorm.DB, userID int, q GetNotesParams) *gorm.DB {
conn := db.Where(
"notes.user_id = ? AND notes.deleted = ? AND notes.encrypted = ?",
userID, false, q.Encrypted,
"notes.user_id = ? AND notes.deleted = ?",
userID, false,
)
if q.Search != "" {

View file

@ -374,10 +374,9 @@ func TestGetNotes_FTSSearch(t *testing.T) {
// Search "baz"
result, err := a.GetNotes(user.ID, GetNotesParams{
Search: "baz",
Encrypted: false,
Page: 1,
PerPage: 30,
Search: "baz",
Page: 1,
PerPage: 30,
})
if err != nil {
t.Fatal(errors.Wrap(err, "getting notes with FTS search"))
@ -390,10 +389,9 @@ func TestGetNotes_FTSSearch(t *testing.T) {
// Search for "running" - should return 1 note
result, err = a.GetNotes(user.ID, GetNotesParams{
Search: "running",
Encrypted: false,
Page: 1,
PerPage: 30,
Search: "running",
Page: 1,
PerPage: 30,
})
if err != nil {
t.Fatal(errors.Wrap(err, "getting notes with FTS search for review"))
@ -405,10 +403,9 @@ func TestGetNotes_FTSSearch(t *testing.T) {
// Search for non-existent term - should return 0 notes
result, err = a.GetNotes(user.ID, GetNotesParams{
Search: "nonexistent",
Encrypted: false,
Page: 1,
PerPage: 30,
Search: "nonexistent",
Page: 1,
PerPage: 30,
})
if err != nil {
t.Fatal(errors.Wrap(err, "getting notes with FTS search for nonexistent"))
@ -437,10 +434,9 @@ func TestGetNotes_FTSSearch_Snippet(t *testing.T) {
// Search for "keyword" in long note - should return snippet with "..."
result, err := a.GetNotes(user.ID, GetNotesParams{
Search: "keyword",
Encrypted: false,
Page: 1,
PerPage: 30,
Search: "keyword",
Page: 1,
PerPage: 30,
})
if err != nil {
t.Fatal(errors.Wrap(err, "getting notes with FTS search for keyword"))
@ -472,10 +468,9 @@ func TestGetNotes_FTSSearch_ShortWord(t *testing.T) {
a.Clock = clock.NewMock()
result, err := a.GetNotes(user.ID, GetNotesParams{
Search: "a",
Encrypted: false,
Page: 1,
PerPage: 30,
Search: "a",
Page: 1,
PerPage: 30,
})
if err != nil {
t.Fatal(errors.Wrap(err, "getting notes with FTS search for 'a'"))
@ -504,10 +499,9 @@ func TestGetNotes_All(t *testing.T) {
a.Clock = clock.NewMock()
result, err := a.GetNotes(user.ID, GetNotesParams{
Search: "",
Encrypted: false,
Page: 1,
PerPage: 30,
Search: "",
Page: 1,
PerPage: 30,
})
if err != nil {
t.Fatal(errors.Wrap(err, "getting notes with FTS search for 'a'"))

View file

@ -56,22 +56,11 @@ func (b *Books) getBooks(r *http.Request) ([]database.Book, error) {
query := r.URL.Query()
name := query.Get("name")
encryptedStr := query.Get("encrypted")
if name != "" {
part := fmt.Sprintf("%%%s%%", name)
conn = conn.Where("LOWER(label) LIKE ?", part)
}
if encryptedStr != "" {
var encrypted bool
if encryptedStr == "true" {
encrypted = true
} else {
encrypted = false
}
conn = conn.Where("encrypted = ?", encrypted)
}
var books []database.Book
if err := conn.Find(&books).Error; err != nil {

View file

@ -73,7 +73,6 @@ func parseGetNotesQuery(q url.Values) (app.GetNotesParams, error) {
yearStr := q.Get("year")
monthStr := q.Get("month")
books := q["book"]
encryptedStr := q.Get("encrypted")
pageStr := q.Get("page")
page, err := parsePageQuery(q)
@ -107,21 +106,13 @@ func parseGetNotesQuery(q url.Values) (app.GetNotesParams, error) {
month = m
}
var encrypted bool
if strings.ToLower(encryptedStr) == "true" {
encrypted = true
} else {
encrypted = false
}
ret := app.GetNotesParams{
Year: year,
Month: month,
Page: page,
Search: parseSearchQuery(q),
Books: books,
Encrypted: encrypted,
PerPage: notesPerPage,
Year: year,
Month: month,
Page: page,
Search: parseSearchQuery(q),
Books: books,
PerPage: notesPerPage,
}
return ret, nil

View file

@ -40,7 +40,6 @@ type Book struct {
EditedOn int64 `json:"edited_on"`
USN int `json:"-" gorm:"index"`
Deleted bool `json:"-" gorm:"default:false"`
Encrypted bool `json:"-" gorm:"default:false"`
}
// Note is a model for a note
@ -57,7 +56,6 @@ type Note struct {
Public bool `json:"public" gorm:"default:false"`
USN int `json:"-" gorm:"index"`
Deleted bool `json:"-" gorm:"default:false"`
Encrypted bool `json:"-" gorm:"default:false"`
Client string `gorm:"index"`
}