mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
Remove encrypted fields from notes and books
This commit is contained in:
parent
505fc67966
commit
bb84369611
9 changed files with 47 additions and 325 deletions
|
|
@ -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{}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(¬e).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(¬e).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 != "" {
|
||||
|
|
|
|||
|
|
@ -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'"))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue