Skip to content

Commit c68416d

Browse files
committed
refactor: update mnemonic to be saved as bytes
1 parent 8a2dff1 commit c68416d

9 files changed

Lines changed: 92 additions & 46 deletions

File tree

backend/app/admin_handler.go

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -107,26 +107,13 @@ func (h *Handler) ListUsersHandler(c *gin.Context) {
107107
go func(user models.User) {
108108
defer wg.Done()
109109
defer func() { <-balanceConcurrencyLimiter }()
110-
if len(user.Mnemonic) == 0 {
110+
if len(user.Mnemonic) == 0 || len(user.AccountAddress) == 0 {
111111
mu.Lock()
112112
usersWithBalance = append(usersWithBalance, UserResponse{User: user, Balance: 0})
113113
mu.Unlock()
114114
return
115115
}
116116

117-
if len(user.AccountAddress) == 0 {
118-
addr, err := internal.AccountFromMnemonic(user.Mnemonic)
119-
if err != nil {
120-
logger.GetLogger().Error().Err(err).Int("user_id", user.ID).Msg("failed to derive account address from mnemonic")
121-
mu.Lock()
122-
multiErr = multierror.Append(multiErr, fmt.Errorf("failed to derive account address from mnemonic for user %d: %w", user.ID, err))
123-
mu.Unlock()
124-
return
125-
}
126-
127-
user.AccountAddress = addr
128-
}
129-
130117
decryptedMnemonic, err := h.cryptoManager.DecryptMnemonic(user.Mnemonic, user.AccountAddress)
131118
if err != nil {
132119
logger.GetLogger().Error().Err(err).Int("user_id", user.ID).Msg("failed to decrypt user mnemonic")

backend/app/app.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@ func NewApp(ctx context.Context, config internal.Configuration) (*App, error) {
7676
return nil, fmt.Errorf("failed to create user storage: %w", err)
7777
}
7878

79+
// Initialize: ensure any plaintext mnemonics get encrypted (idempotent)
80+
if err := cryptoMgr.EnsureMnemonicsEncrypted(ctx, db); err != nil {
81+
logger.GetLogger().Error().Err(err).Msg("mnemonic encryption initializer failed")
82+
}
83+
7984
mailService := internal.NewMailService(config.MailSender.SendGridKey)
8085

8186
gridProxy := proxy.NewRetryingClient(proxy.NewClient(config.GridProxyURL))

backend/app/setup.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,16 @@ func GetAuthToken(t *testing.T, app *App, id int, email, username string, isAdmi
173173
func CreateTestUser(t *testing.T, app *App, email, username string, hashedPassword []byte, verified, admin bool, mnemonicRequired bool, code int, updatedAt time.Time) *models.User {
174174
mnemonic := ""
175175
sponseeAddress := ""
176+
var encryptedBytes []byte
176177
if !mnemonicRequired {
177-
mnemonic = ""
178+
// no mnemonic needed in tests; leave encryptedBytes nil
178179
} else {
179180
var err error
180181
mnemonic, sponseeAddress, _, err = internal.SetupUserOnTFChain(app.handlers.substrateClient, app.config)
181182
require.NoError(t, err)
182183

183-
encryptedMnemonic, err := app.handlers.cryptoManager.EncryptMnemonic(mnemonic, sponseeAddress)
184+
encryptedBytes, err = app.handlers.cryptoManager.EncryptMnemonic(mnemonic, sponseeAddress)
184185
require.NoError(t, err)
185-
mnemonic = encryptedMnemonic
186186
}
187187
user := &models.User{
188188
Username: username,
@@ -192,7 +192,7 @@ func CreateTestUser(t *testing.T, app *App, email, username string, hashedPasswo
192192
Admin: admin,
193193
Code: code,
194194
UpdatedAt: updatedAt,
195-
Mnemonic: mnemonic,
195+
Mnemonic: encryptedBytes,
196196
AccountAddress: sponseeAddress,
197197
}
198198
err := app.handlers.db.RegisterUser(user)

backend/app/user_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1132,5 +1132,5 @@ func isUserRegistered(user models.User) bool {
11321132
user.Verified &&
11331133
len(strings.TrimSpace(user.AccountAddress)) > 0 &&
11341134
len(strings.TrimSpace(user.StripeCustomerID)) > 0 &&
1135-
len(strings.TrimSpace(user.Mnemonic)) > 0
1135+
len(user.Mnemonic) > 0
11361136
}

backend/app/user_handler_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func TestRegisterHandler(t *testing.T) {
5151

5252
t.Run("Register Existing Verified User", func(t *testing.T) {
5353
user := CreateTestUser(t, app, "dupe@example.com", "Test User", []byte("securepassword"), true, false, false, 0, time.Now())
54-
user.Mnemonic = "mnemonic"
54+
user.Mnemonic = []byte("mnemonic")
5555
user.AccountAddress = "sponseeAddress"
5656
user.Sponsored = true
5757
user.StripeCustomerID = "stripeCustomerID"
@@ -132,7 +132,7 @@ func TestVerifyRegisterCode(t *testing.T) {
132132
})
133133
t.Run("Test Verify Register Code with registered user", func(t *testing.T) {
134134
registeredUser := CreateTestUser(t, app, "registered@example.com", "Registered User", []byte("securepassword"), true, false, false, 123, time.Now())
135-
registeredUser.Mnemonic = "mnemonic"
135+
registeredUser.Mnemonic = []byte("mnemonic")
136136
registeredUser.AccountAddress = "sponseeAddress"
137137
registeredUser.Sponsored = true
138138
registeredUser.StripeCustomerID = "stripeCustomerID"
@@ -518,7 +518,7 @@ func TestChargeBalanceHandler(t *testing.T) {
518518
t.Run("Test ChargeBalance with Invalid Request format", func(t *testing.T) {
519519

520520
user := CreateTestUser(t, app, "chargeuser@example.com", "Charge User", []byte("securepassword"), true, false, true, 0, time.Now())
521-
user.Mnemonic = "test-menmonic"
521+
user.Mnemonic = []byte("test-menmonic")
522522
err = app.handlers.db.UpdateUserByID(user)
523523
assert.NoError(t, err)
524524
token := GetAuthToken(t, app, user.ID, user.Email, user.Username, false)

backend/cmd/cleanup/moneycollector/money_collector.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func (m *MoneyCollector) CollectMoney() {
5151
balanceConcurrencyLimiter <- struct{}{}
5252
defer wg.Done()
5353
defer func() { <-balanceConcurrencyLimiter }()
54-
if user.Mnemonic == "" {
54+
if len(user.Mnemonic) == 0 || len(user.AccountAddress) == 0 {
5555
return
5656
}
5757

@@ -75,11 +75,10 @@ func (m *MoneyCollector) CollectMoney() {
7575
log.Debug().Int("user_id", user.ID).Uint64("balance", balance).Msg("MoneyCollector: transferring balance to system account")
7676
if err := m.substrateClient.Transfer(userIdentity, balance-MinBalanceThreshold, substrate.AccountID(system.PublicKey())); err != nil {
7777
log.Error().Err(err).Int("user_id", user.ID).Msg("MoneyCollector: failed to transfer balance")
78+
return
7879
}
79-
return
8080
}
8181
}(user)
82-
8382
}
8483
wg.Wait()
8584
log.Info().Msg("MoneyCollector: finished")

backend/internal/activities/user_activities.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ func SetupTFChainStep(client *substrate.Substrate, config internal.Configuration
158158
return fmt.Errorf("failed to check existing user: %w", err)
159159
}
160160

161-
if len(strings.TrimSpace(existingUser.Mnemonic)) > 0 {
161+
if len(existingUser.Mnemonic) > 0 {
162162

163163
decryptedMnemonic, err := crypto.DecryptMnemonic(existingUser.Mnemonic, existingUser.AccountAddress)
164164
if err != nil {

backend/internal/crypto.go

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package internal
22

33
import (
4+
"context"
45
"crypto/aes"
56
"crypto/cipher"
67
"crypto/rand"
78
"crypto/sha256"
8-
"encoding/base64"
99
"errors"
1010
"fmt"
1111
"io"
1212

13+
"kubecloud/internal/logger"
14+
"kubecloud/models"
15+
16+
"sync"
17+
1318
"golang.org/x/crypto/argon2"
1419
)
1520

@@ -56,41 +61,36 @@ func (cm *CryptoManager) deriveKey(passphrase string, userIdentifier string) ([]
5661
return key, nil
5762
}
5863

59-
func (cm *CryptoManager) encrypt(plainText string, key []byte) (string, error) {
64+
func (cm *CryptoManager) encrypt(plainText string, key []byte) ([]byte, error) {
6065
if len(key) != 32 {
61-
return "", ErrInvalidKeyLength
66+
return nil, ErrInvalidKeyLength
6267
}
6368

6469
block, err := aes.NewCipher(key)
6570
if err != nil {
66-
return "", fmt.Errorf("failed to create cipher: %w", err)
71+
return nil, fmt.Errorf("failed to create cipher: %w", err)
6772
}
6873

6974
aesGCM, err := cipher.NewGCM(block)
7075
if err != nil {
71-
return "", fmt.Errorf("failed to create GCM: %w", err)
76+
return nil, fmt.Errorf("failed to create GCM: %w", err)
7277
}
7378

7479
nonce := make([]byte, aesGCM.NonceSize())
7580
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
76-
return "", fmt.Errorf("failed to generate nonce: %w", err)
81+
return nil, fmt.Errorf("failed to generate nonce: %w", err)
7782
}
7883

7984
ciphertext := aesGCM.Seal(nonce, nonce, []byte(plainText), nil)
8085

81-
return base64.StdEncoding.EncodeToString(ciphertext), nil
86+
return ciphertext, nil
8287
}
8388

84-
func (cm *CryptoManager) decrypt(encryptedText string, key []byte) (string, error) {
89+
func (cm *CryptoManager) decrypt(encryptedBytes []byte, key []byte) (string, error) {
8590
if len(key) != 32 {
8691
return "", ErrInvalidKeyLength
8792
}
8893

89-
ciphertext, err := base64.StdEncoding.DecodeString(encryptedText)
90-
if err != nil {
91-
return "", fmt.Errorf("failed to decode base64: %w", err)
92-
}
93-
9494
block, err := aes.NewCipher(key)
9595
if err != nil {
9696
return "", fmt.Errorf("failed to create cipher: %w", err)
@@ -102,11 +102,11 @@ func (cm *CryptoManager) decrypt(encryptedText string, key []byte) (string, erro
102102
}
103103

104104
nonceSize := aesGCM.NonceSize()
105-
if len(ciphertext) < nonceSize {
105+
if len(encryptedBytes) < nonceSize {
106106
return "", ErrInvalidData
107107
}
108108

109-
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
109+
nonce, ciphertext := encryptedBytes[:nonceSize], encryptedBytes[nonceSize:]
110110

111111
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
112112
if err != nil {
@@ -124,18 +124,73 @@ func (cm *CryptoManager) getMnemonicKey(userAddress string) ([]byte, error) {
124124
return cm.deriveKey(cm.config.MnemonicEncryptionPassphrase, userAddress)
125125
}
126126

127-
func (cm *CryptoManager) EncryptMnemonic(plainText string, userAddress string) (string, error) {
127+
func (cm *CryptoManager) EncryptMnemonic(plainText string, userAddress string) ([]byte, error) {
128128
key, err := cm.getMnemonicKey(userAddress)
129129
if err != nil {
130-
return "", err
130+
return nil, err
131131
}
132132
return cm.encrypt(plainText, key)
133133
}
134134

135-
func (cm *CryptoManager) DecryptMnemonic(encryptedText string, userAddress string) (string, error) {
135+
func (cm *CryptoManager) DecryptMnemonic(encryptedBytes []byte, userAddress string) (string, error) {
136136
key, err := cm.getMnemonicKey(userAddress)
137137
if err != nil {
138138
return "", err
139139
}
140-
return cm.decrypt(encryptedText, key)
140+
return cm.decrypt(encryptedBytes, key)
141+
}
142+
143+
func (cm *CryptoManager) EnsureMnemonicsEncrypted(ctx context.Context, db models.DB) error {
144+
users, err := db.ListAllUsers()
145+
if err != nil {
146+
return fmt.Errorf("ensure encryption: list users failed: %w", err)
147+
}
148+
149+
const maxWorkers = 16
150+
sem := make(chan struct{}, maxWorkers)
151+
var wg sync.WaitGroup
152+
153+
for i := range users {
154+
u := users[i]
155+
wg.Add(1)
156+
sem <- struct{}{}
157+
go func(u models.User) {
158+
defer wg.Done()
159+
defer func() { <-sem }()
160+
161+
select {
162+
case <-ctx.Done():
163+
return
164+
default:
165+
}
166+
167+
if len(u.Mnemonic) == 0 {
168+
return
169+
}
170+
171+
// Derive account address if missing and mnemonic appears to be plaintext
172+
if len(u.AccountAddress) == 0 {
173+
addr, err := AccountFromMnemonic(string(u.Mnemonic))
174+
if err != nil {
175+
logger.GetLogger().Error().Err(err).Int("user_id", u.ID).Msg("failed to derive account address from mnemonic")
176+
return
177+
}
178+
u.AccountAddress = addr
179+
}
180+
181+
if _, err := cm.DecryptMnemonic(u.Mnemonic, u.AccountAddress); err == nil {
182+
return
183+
}
184+
185+
encryptedMnemonic, err := cm.EncryptMnemonic(string(u.Mnemonic), u.AccountAddress)
186+
if err != nil {
187+
return
188+
}
189+
u.Mnemonic = encryptedMnemonic
190+
_ = db.UpdateUserByID(&u)
191+
}(u)
192+
}
193+
194+
wg.Wait()
195+
return nil
141196
}

backend/models/user.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ type User struct {
1515
Admin bool `json:"admin"`
1616
CreditCardBalance uint64 `json:"credit_card_balance" gorm:"default:0"` // millicent, money from credit card
1717
CreditedBalance uint64 `json:"credited_balance" gorm:"default:0"` // millicent, manually added by admin or from vouchers
18-
Mnemonic string `json:"-" gorm:"column:mnemonic"`
18+
Mnemonic []byte `json:"-" gorm:"column:mnemonic"`
1919
SSHKey string `json:"ssh_key"`
2020
Debt uint64 `json:"debt"` // millicent
2121
Sponsored bool `json:"sponsored"`

0 commit comments

Comments
 (0)