-
Notifications
You must be signed in to change notification settings - Fork 490
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[management] Add GCM encryption and migrate legacy encrypted events (#…
…2569) * Add AES-GCM encryption Signed-off-by: bcmmbaga <[email protected]> * migrate legacy encrypted data to AES-GCM encryption Signed-off-by: bcmmbaga <[email protected]> * Refactor and use transaction when migrating data Signed-off-by: bcmmbaga <[email protected]> * Add events migration tests Signed-off-by: bcmmbaga <[email protected]> * fix lint Signed-off-by: bcmmbaga <[email protected]> * skip migrating record on error Signed-off-by: bcmmbaga <[email protected]> * Preallocate capacity for nonce to avoid allocations in Seal Signed-off-by: bcmmbaga <[email protected]> --------- Signed-off-by: bcmmbaga <[email protected]>
- Loading branch information
Showing
5 changed files
with
396 additions
and
86 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,11 @@ func TestGenerateKey(t *testing.T) { | |
t.Fatalf("failed to init email encryption: %s", err) | ||
} | ||
|
||
encrypted := ee.Encrypt(testData) | ||
encrypted, err := ee.Encrypt(testData) | ||
if err != nil { | ||
t.Fatalf("failed to encrypt data: %s", err) | ||
} | ||
|
||
if encrypted == "" { | ||
t.Fatalf("invalid encrypted text") | ||
} | ||
|
@@ -30,6 +34,32 @@ func TestGenerateKey(t *testing.T) { | |
} | ||
} | ||
|
||
func TestGenerateKeyLegacy(t *testing.T) { | ||
testData := "[email protected]" | ||
key, err := GenerateKey() | ||
if err != nil { | ||
t.Fatalf("failed to generate key: %s", err) | ||
} | ||
ee, err := NewFieldEncrypt(key) | ||
if err != nil { | ||
t.Fatalf("failed to init email encryption: %s", err) | ||
} | ||
|
||
encrypted := ee.LegacyEncrypt(testData) | ||
if encrypted == "" { | ||
t.Fatalf("invalid encrypted text") | ||
} | ||
|
||
decrypted, err := ee.LegacyDecrypt(encrypted) | ||
if err != nil { | ||
t.Fatalf("failed to decrypt data: %s", err) | ||
} | ||
|
||
if decrypted != testData { | ||
t.Fatalf("decrypted data is not match with test data: %s, %s", testData, decrypted) | ||
} | ||
} | ||
|
||
func TestCorruptKey(t *testing.T) { | ||
testData := "[email protected]" | ||
key, err := GenerateKey() | ||
|
@@ -41,7 +71,11 @@ func TestCorruptKey(t *testing.T) { | |
t.Fatalf("failed to init email encryption: %s", err) | ||
} | ||
|
||
encrypted := ee.Encrypt(testData) | ||
encrypted, err := ee.Encrypt(testData) | ||
if err != nil { | ||
t.Fatalf("failed to encrypt data: %s", err) | ||
} | ||
|
||
if encrypted == "" { | ||
t.Fatalf("invalid encrypted text") | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
package sqlite | ||
|
||
import ( | ||
"context" | ||
"database/sql" | ||
"fmt" | ||
|
||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
func migrate(ctx context.Context, crypt *FieldEncrypt, db *sql.DB) error { | ||
if _, err := db.Exec(createTableQuery); err != nil { | ||
return err | ||
} | ||
|
||
if _, err := db.Exec(creatTableDeletedUsersQuery); err != nil { | ||
return err | ||
} | ||
|
||
if err := updateDeletedUsersTable(ctx, db); err != nil { | ||
return fmt.Errorf("failed to update deleted_users table: %v", err) | ||
} | ||
|
||
return migrateLegacyEncryptedUsersToGCM(ctx, crypt, db) | ||
} | ||
|
||
// updateDeletedUsersTable checks and updates the deleted_users table schema to ensure required columns exist. | ||
func updateDeletedUsersTable(ctx context.Context, db *sql.DB) error { | ||
exists, err := checkColumnExists(db, "deleted_users", "name") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if !exists { | ||
log.WithContext(ctx).Debug("Adding name column to the deleted_users table") | ||
|
||
_, err = db.Exec(`ALTER TABLE deleted_users ADD COLUMN name TEXT;`) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
log.WithContext(ctx).Debug("Successfully added name column to the deleted_users table") | ||
} | ||
|
||
exists, err = checkColumnExists(db, "deleted_users", "enc_algo") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if !exists { | ||
log.WithContext(ctx).Debug("Adding enc_algo column to the deleted_users table") | ||
|
||
_, err = db.Exec(`ALTER TABLE deleted_users ADD COLUMN enc_algo TEXT;`) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
log.WithContext(ctx).Debug("Successfully added enc_algo column to the deleted_users table") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// migrateLegacyEncryptedUsersToGCM migrates previously encrypted data using, | ||
// legacy CBC encryption with a static IV to the new GCM encryption method. | ||
func migrateLegacyEncryptedUsersToGCM(ctx context.Context, crypt *FieldEncrypt, db *sql.DB) error { | ||
log.WithContext(ctx).Debug("Migrating CBC encrypted deleted users to GCM") | ||
|
||
tx, err := db.Begin() | ||
if err != nil { | ||
return fmt.Errorf("failed to begin transaction: %v", err) | ||
} | ||
defer func() { | ||
_ = tx.Rollback() | ||
}() | ||
|
||
rows, err := tx.Query(fmt.Sprintf(`SELECT id, email, name FROM deleted_users where enc_algo IS NULL OR enc_algo != '%s'`, gcmEncAlgo)) | ||
if err != nil { | ||
return fmt.Errorf("failed to execute select query: %v", err) | ||
} | ||
defer rows.Close() | ||
|
||
updateStmt, err := tx.Prepare(`UPDATE deleted_users SET email = ?, name = ?, enc_algo = ? WHERE id = ?`) | ||
if err != nil { | ||
return fmt.Errorf("failed to prepare update statement: %v", err) | ||
} | ||
defer updateStmt.Close() | ||
|
||
if err = processUserRows(ctx, crypt, rows, updateStmt); err != nil { | ||
return err | ||
} | ||
|
||
if err = tx.Commit(); err != nil { | ||
return fmt.Errorf("failed to commit transaction: %v", err) | ||
} | ||
|
||
log.WithContext(ctx).Debug("Successfully migrated CBC encrypted deleted users to GCM") | ||
return nil | ||
} | ||
|
||
// processUserRows processes database rows of user data, decrypts legacy encryption fields, and re-encrypts them using GCM. | ||
func processUserRows(ctx context.Context, crypt *FieldEncrypt, rows *sql.Rows, updateStmt *sql.Stmt) error { | ||
for rows.Next() { | ||
var ( | ||
id, decryptedEmail, decryptedName string | ||
email, name *string | ||
) | ||
|
||
err := rows.Scan(&id, &email, &name) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if email != nil { | ||
decryptedEmail, err = crypt.LegacyDecrypt(*email) | ||
if err != nil { | ||
log.WithContext(ctx).Warnf("skipping migrating deleted user %s: %v", | ||
id, | ||
fmt.Errorf("failed to decrypt email: %w", err), | ||
) | ||
continue | ||
} | ||
} | ||
|
||
if name != nil { | ||
decryptedName, err = crypt.LegacyDecrypt(*name) | ||
if err != nil { | ||
log.WithContext(ctx).Warnf("skipping migrating deleted user %s: %v", | ||
id, | ||
fmt.Errorf("failed to decrypt name: %w", err), | ||
) | ||
continue | ||
} | ||
} | ||
|
||
encryptedEmail, err := crypt.Encrypt(decryptedEmail) | ||
if err != nil { | ||
return fmt.Errorf("failed to encrypt email: %w", err) | ||
} | ||
|
||
encryptedName, err := crypt.Encrypt(decryptedName) | ||
if err != nil { | ||
return fmt.Errorf("failed to encrypt name: %w", err) | ||
} | ||
|
||
_, err = updateStmt.Exec(encryptedEmail, encryptedName, gcmEncAlgo, id) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
if err := rows.Err(); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package sqlite | ||
|
||
import ( | ||
"context" | ||
"database/sql" | ||
"path/filepath" | ||
"testing" | ||
"time" | ||
|
||
_ "github.com/mattn/go-sqlite3" | ||
"github.com/netbirdio/netbird/management/server/activity" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func setupDatabase(t *testing.T) *sql.DB { | ||
t.Helper() | ||
|
||
dbFile := filepath.Join(t.TempDir(), eventSinkDB) | ||
db, err := sql.Open("sqlite3", dbFile) | ||
require.NoError(t, err, "Failed to open database") | ||
|
||
t.Cleanup(func() { | ||
_ = db.Close() | ||
}) | ||
|
||
_, err = db.Exec(createTableQuery) | ||
require.NoError(t, err, "Failed to create events table") | ||
|
||
_, err = db.Exec(`CREATE TABLE deleted_users (id TEXT NOT NULL, email TEXT NOT NULL, name TEXT);`) | ||
require.NoError(t, err, "Failed to create deleted_users table") | ||
|
||
return db | ||
} | ||
|
||
func TestMigrate(t *testing.T) { | ||
db := setupDatabase(t) | ||
|
||
key, err := GenerateKey() | ||
require.NoError(t, err, "Failed to generate key") | ||
|
||
crypt, err := NewFieldEncrypt(key) | ||
require.NoError(t, err, "Failed to initialize FieldEncrypt") | ||
|
||
legacyEmail := crypt.LegacyEncrypt("[email protected]") | ||
legacyName := crypt.LegacyEncrypt("Test Account") | ||
|
||
_, err = db.Exec(`INSERT INTO events(activity, timestamp, initiator_id, target_id, account_id, meta) VALUES(?, ?, ?, ?, ?, ?)`, | ||
activity.UserDeleted, time.Now(), "initiatorID", "targetID", "accountID", "") | ||
require.NoError(t, err, "Failed to insert event") | ||
|
||
_, err = db.Exec(`INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?)`, "targetID", legacyEmail, legacyName) | ||
require.NoError(t, err, "Failed to insert legacy encrypted data") | ||
|
||
colExists, err := checkColumnExists(db, "deleted_users", "enc_algo") | ||
require.NoError(t, err, "Failed to check if enc_algo column exists") | ||
require.False(t, colExists, "enc_algo column should not exist before migration") | ||
|
||
err = migrate(context.Background(), crypt, db) | ||
require.NoError(t, err, "Migration failed") | ||
|
||
colExists, err = checkColumnExists(db, "deleted_users", "enc_algo") | ||
require.NoError(t, err, "Failed to check if enc_algo column exists after migration") | ||
require.True(t, colExists, "enc_algo column should exist after migration") | ||
|
||
var encAlgo string | ||
err = db.QueryRow(`SELECT enc_algo FROM deleted_users LIMIT 1`, "").Scan(&encAlgo) | ||
require.NoError(t, err, "Failed to select updated data") | ||
require.Equal(t, gcmEncAlgo, encAlgo, "enc_algo should be set to 'GCM' after migration") | ||
|
||
store, err := createStore(crypt, db) | ||
require.NoError(t, err, "Failed to create store") | ||
|
||
events, err := store.Get(context.Background(), "accountID", 0, 1, false) | ||
require.NoError(t, err, "Failed to get events") | ||
|
||
require.Len(t, events, 1, "Should have one event") | ||
require.Equal(t, activity.UserDeleted, events[0].Activity, "activity should match") | ||
require.Equal(t, "initiatorID", events[0].InitiatorID, "initiator id should match") | ||
require.Equal(t, "targetID", events[0].TargetID, "target id should match") | ||
require.Equal(t, "accountID", events[0].AccountID, "account id should match") | ||
require.Equal(t, "[email protected]", events[0].Meta["email"], "email should match") | ||
require.Equal(t, "Test Account", events[0].Meta["username"], "username should match") | ||
} |
Oops, something went wrong.