2019-08-20 15:38:40 +00:00
package multiaccounts
import (
2020-11-24 13:13:46 +00:00
"context"
2019-08-20 15:38:40 +00:00
"database/sql"
2022-04-08 17:54:29 +00:00
"encoding/json"
2019-08-20 15:38:40 +00:00
2020-12-21 08:41:50 +00:00
"github.com/ethereum/go-ethereum/log"
2023-10-02 09:28:42 +00:00
"github.com/status-im/status-go/common/dbsetup"
2020-11-24 13:13:46 +00:00
"github.com/status-im/status-go/images"
2023-06-02 15:06:51 +00:00
"github.com/status-im/status-go/multiaccounts/common"
2019-08-20 15:38:40 +00:00
"github.com/status-im/status-go/multiaccounts/migrations"
2022-08-19 14:07:57 +00:00
"github.com/status-im/status-go/protocol/protobuf"
2019-08-20 15:38:40 +00:00
"github.com/status-im/status-go/sqlite"
)
2023-03-22 17:48:42 +00:00
type ColorHash [ ] [ 2 ] int
2019-08-20 15:38:40 +00:00
// Account stores public information about account.
type Account struct {
2023-07-18 13:35:06 +00:00
Name string ` json:"name" `
Timestamp int64 ` json:"timestamp" `
Identicon string ` json:"identicon" `
ColorHash ColorHash ` json:"colorHash" `
ColorID int64 ` json:"colorId" `
CustomizationColor common . CustomizationColor ` json:"customizationColor,omitempty" `
KeycardPairing string ` json:"keycard-pairing" `
KeyUID string ` json:"key-uid" `
Images [ ] images . IdentityImage ` json:"images" `
KDFIterations int ` json:"kdfIterations,omitempty" `
CustomizationColorClock uint64 ` json:"-" `
2020-11-24 13:13:46 +00:00
}
2024-02-29 12:44:35 +00:00
func ( a * Account ) RefersToKeycard ( ) bool {
return a . KeycardPairing != ""
}
2022-08-19 14:07:57 +00:00
func ( a * Account ) ToProtobuf ( ) * protobuf . MultiAccount {
2023-03-22 17:48:42 +00:00
var colorHashes [ ] * protobuf . MultiAccount_ColorHash
2022-08-19 14:07:57 +00:00
for _ , index := range a . ColorHash {
var i [ ] int64
for _ , is := range index {
i = append ( i , int64 ( is ) )
}
2023-03-22 17:48:42 +00:00
colorHashes = append ( colorHashes , & protobuf . MultiAccount_ColorHash { Index : i } )
2022-08-19 14:07:57 +00:00
}
var identityImages [ ] * protobuf . MultiAccount_IdentityImage
for _ , ii := range a . Images {
identityImages = append ( identityImages , ii . ToProtobuf ( ) )
}
return & protobuf . MultiAccount {
2023-07-18 13:35:06 +00:00
Name : a . Name ,
Timestamp : a . Timestamp ,
Identicon : a . Identicon ,
ColorHash : colorHashes ,
ColorId : a . ColorID ,
CustomizationColor : string ( a . CustomizationColor ) ,
KeycardPairing : a . KeycardPairing ,
KeyUid : a . KeyUID ,
Images : identityImages ,
CustomizationColorClock : a . CustomizationColorClock ,
2022-08-19 14:07:57 +00:00
}
}
func ( a * Account ) FromProtobuf ( ma * protobuf . MultiAccount ) {
2023-03-22 17:48:42 +00:00
var colorHash ColorHash
2022-08-19 14:07:57 +00:00
for _ , index := range ma . ColorHash {
2022-10-28 15:30:54 +00:00
var i [ 2 ] int
for n , is := range index . Index {
i [ n ] = int ( is )
2022-08-19 14:07:57 +00:00
}
2023-03-22 17:48:42 +00:00
colorHash = append ( colorHash , i )
2022-08-19 14:07:57 +00:00
}
var identityImages [ ] images . IdentityImage
for _ , ii := range ma . Images {
iii := images . IdentityImage { }
iii . FromProtobuf ( ii )
identityImages = append ( identityImages , iii )
}
a . Name = ma . Name
a . Timestamp = ma . Timestamp
a . Identicon = ma . Identicon
2023-03-22 17:48:42 +00:00
a . ColorHash = colorHash
2023-06-05 07:12:15 +00:00
a . ColorID = ma . ColorId
2022-08-19 14:07:57 +00:00
a . KeycardPairing = ma . KeycardPairing
2023-06-02 15:06:51 +00:00
a . CustomizationColor = common . CustomizationColor ( ma . CustomizationColor )
2022-08-19 14:07:57 +00:00
a . KeyUID = ma . KeyUid
a . Images = identityImages
2023-07-18 13:35:06 +00:00
a . CustomizationColorClock = ma . CustomizationColorClock
2022-08-19 14:07:57 +00:00
}
2024-04-03 14:49:57 +00:00
func ( a * Account ) GetCustomizationColor ( ) common . CustomizationColor {
if len ( a . CustomizationColor ) == 0 {
return common . CustomizationColorBlue
}
return a . CustomizationColor
}
func ( a * Account ) GetCustomizationColorID ( ) uint32 {
return common . ColorToIDFallbackToBlue ( a . GetCustomizationColor ( ) )
}
2020-12-15 15:06:59 +00:00
type MultiAccountMarshaller interface {
ToMultiAccount ( ) * Account
}
2023-10-24 10:15:32 +00:00
type IdentityImageSubscriptionChange struct {
PublishExpected bool
}
2020-11-24 13:13:46 +00:00
type Database struct {
2020-12-21 08:41:50 +00:00
db * sql . DB
2023-10-24 10:15:32 +00:00
identityImageSubscriptions [ ] chan * IdentityImageSubscriptionChange
2019-08-20 15:38:40 +00:00
}
// InitializeDB creates db file at a given path and applies migrations.
func InitializeDB ( path string ) ( * Database , error ) {
db , err := sqlite . OpenUnecryptedDB ( path )
if err != nil {
return nil , err
}
2023-05-19 15:31:45 +00:00
err = migrations . Migrate ( db , nil )
2019-08-20 15:38:40 +00:00
if err != nil {
return nil , err
}
return & Database { db : db } , nil
}
func ( db * Database ) Close ( ) error {
return db . db . Close ( )
}
2022-09-27 20:27:20 +00:00
func ( db * Database ) GetAccountKDFIterationsNumber ( keyUID string ) ( kdfIterationsNumber int , err error ) {
err = db . db . QueryRow ( "SELECT kdfIterations FROM accounts WHERE keyUid = ?" , keyUID ) . Scan ( & kdfIterationsNumber )
if err != nil {
return - 1 , err
}
return
}
2020-12-16 18:17:38 +00:00
func ( db * Database ) GetAccounts ( ) ( rst [ ] Account , err error ) {
2023-07-18 13:35:06 +00:00
rows , err := db . db . Query ( "SELECT a.name, a.loginTimestamp, a.identicon, a.colorHash, a.colorId, a.customizationColor, a.customizationColorClock, a.keycardPairing, a.keyUid, a.kdfIterations, ii.name, ii.image_payload, ii.width, ii.height, ii.file_size, ii.resize_target, ii.clock FROM accounts AS a LEFT JOIN identity_images AS ii ON ii.key_uid = a.keyUid ORDER BY loginTimestamp DESC" )
2019-08-20 15:38:40 +00:00
if err != nil {
return nil , err
}
2020-12-16 18:17:38 +00:00
defer func ( ) {
2022-04-12 16:21:31 +00:00
errClose := rows . Close ( )
err = valueOr ( err , errClose )
2020-12-16 18:17:38 +00:00
} ( )
2020-11-30 19:45:10 +00:00
2019-08-20 15:38:40 +00:00
for rows . Next ( ) {
acc := Account { }
2020-12-01 18:15:53 +00:00
accLoginTimestamp := sql . NullInt64 { }
2020-12-01 13:00:51 +00:00
accIdenticon := sql . NullString { }
2022-04-08 17:54:29 +00:00
accColorHash := sql . NullString { }
accColorID := sql . NullInt64 { }
2020-11-30 19:45:10 +00:00
ii := & images . IdentityImage { }
iiName := sql . NullString { }
iiWidth := sql . NullInt64 { }
iiHeight := sql . NullInt64 { }
iiFileSize := sql . NullInt64 { }
iiResizeTarget := sql . NullInt64 { }
2022-03-24 09:35:56 +00:00
iiClock := sql . NullInt64 { }
2020-11-30 19:45:10 +00:00
err = rows . Scan (
& acc . Name ,
& accLoginTimestamp ,
2020-12-01 13:00:51 +00:00
& accIdenticon ,
2022-04-08 17:54:29 +00:00
& accColorHash ,
& accColorID ,
2023-03-22 17:48:42 +00:00
& acc . CustomizationColor ,
2023-07-18 13:35:06 +00:00
& acc . CustomizationColorClock ,
2020-11-30 19:45:10 +00:00
& acc . KeycardPairing ,
& acc . KeyUID ,
2022-09-27 20:27:20 +00:00
& acc . KDFIterations ,
2020-11-30 19:45:10 +00:00
& iiName ,
& ii . Payload ,
& iiWidth ,
& iiHeight ,
& iiFileSize ,
& iiResizeTarget ,
2022-03-24 09:35:56 +00:00
& iiClock ,
2020-11-30 19:45:10 +00:00
)
2019-08-20 15:38:40 +00:00
if err != nil {
return nil , err
}
2020-11-30 19:45:10 +00:00
acc . Timestamp = accLoginTimestamp . Int64
2020-12-01 13:00:51 +00:00
acc . Identicon = accIdenticon . String
2022-04-08 17:54:29 +00:00
acc . ColorID = accColorID . Int64
2022-04-12 14:09:42 +00:00
if len ( accColorHash . String ) != 0 {
err = json . Unmarshal ( [ ] byte ( accColorHash . String ) , & acc . ColorHash )
if err != nil {
return nil , err
}
2022-04-08 17:54:29 +00:00
}
2020-11-30 19:45:10 +00:00
ii . KeyUID = acc . KeyUID
ii . Name = iiName . String
ii . Width = int ( iiWidth . Int64 )
ii . Height = int ( iiHeight . Int64 )
ii . FileSize = int ( iiFileSize . Int64 )
ii . ResizeTarget = int ( iiResizeTarget . Int64 )
2022-03-24 09:35:56 +00:00
ii . Clock = uint64 ( iiClock . Int64 )
2020-11-30 19:45:10 +00:00
if ii . Name == "" && len ( ii . Payload ) == 0 && ii . Width == 0 && ii . Height == 0 && ii . FileSize == 0 && ii . ResizeTarget == 0 {
ii = nil
}
2020-12-01 18:15:53 +00:00
// Last index
li := len ( rst ) - 1
// Don't process nil identity images
2020-11-30 19:45:10 +00:00
if ii != nil {
2020-12-01 18:15:53 +00:00
// attach the identity image to a previously created account if present, check keyUID matches
if len ( rst ) > 0 && rst [ li ] . KeyUID == acc . KeyUID {
rst [ li ] . Images = append ( rst [ li ] . Images , * ii )
// else attach the identity image to the newly created account
2020-11-30 19:45:10 +00:00
} else {
acc . Images = append ( acc . Images , * ii )
}
}
2020-12-01 18:15:53 +00:00
// Append newly created account only if this is the first loop or the keyUID doesn't match
if len ( rst ) == 0 || rst [ li ] . KeyUID != acc . KeyUID {
rst = append ( rst , acc )
}
2020-11-30 19:45:10 +00:00
}
2019-08-20 15:38:40 +00:00
return rst , nil
}
2022-06-27 13:48:00 +00:00
func ( db * Database ) GetAccount ( keyUID string ) ( * Account , error ) {
2023-07-18 13:35:06 +00:00
rows , err := db . db . Query ( "SELECT a.name, a.loginTimestamp, a.identicon, a.colorHash, a.colorId, a.customizationColor, a.customizationColorClock, a.keycardPairing, a.keyUid, a.kdfIterations, ii.key_uid, ii.name, ii.image_payload, ii.width, ii.height, ii.file_size, ii.resize_target, ii.clock FROM accounts AS a LEFT JOIN identity_images AS ii ON ii.key_uid = a.keyUid WHERE a.keyUid = ? ORDER BY loginTimestamp DESC" , keyUID )
2022-06-24 14:06:13 +00:00
if err != nil {
return nil , err
}
defer func ( ) {
errClose := rows . Close ( )
err = valueOr ( err , errClose )
} ( )
2022-06-27 13:48:00 +00:00
acc := new ( Account )
2022-06-24 14:06:13 +00:00
for rows . Next ( ) {
accLoginTimestamp := sql . NullInt64 { }
accIdenticon := sql . NullString { }
accColorHash := sql . NullString { }
accColorID := sql . NullInt64 { }
ii := & images . IdentityImage { }
2022-08-09 14:11:55 +00:00
iiKeyUID := sql . NullString { }
2022-06-24 14:06:13 +00:00
iiName := sql . NullString { }
iiWidth := sql . NullInt64 { }
iiHeight := sql . NullInt64 { }
iiFileSize := sql . NullInt64 { }
iiResizeTarget := sql . NullInt64 { }
iiClock := sql . NullInt64 { }
err = rows . Scan (
& acc . Name ,
& accLoginTimestamp ,
& accIdenticon ,
& accColorHash ,
& accColorID ,
2023-03-22 17:48:42 +00:00
& acc . CustomizationColor ,
2023-07-18 13:35:06 +00:00
& acc . CustomizationColorClock ,
2022-06-24 14:06:13 +00:00
& acc . KeycardPairing ,
& acc . KeyUID ,
2023-02-28 12:32:45 +00:00
& acc . KDFIterations ,
2022-08-09 14:11:55 +00:00
& iiKeyUID ,
2022-06-24 14:06:13 +00:00
& iiName ,
& ii . Payload ,
& iiWidth ,
& iiHeight ,
& iiFileSize ,
& iiResizeTarget ,
& iiClock ,
)
if err != nil {
return nil , err
}
acc . Timestamp = accLoginTimestamp . Int64
acc . Identicon = accIdenticon . String
acc . ColorID = accColorID . Int64
if len ( accColorHash . String ) != 0 {
err = json . Unmarshal ( [ ] byte ( accColorHash . String ) , & acc . ColorHash )
if err != nil {
return nil , err
}
}
2022-08-09 14:11:55 +00:00
ii . KeyUID = iiKeyUID . String
2022-06-24 14:06:13 +00:00
ii . Name = iiName . String
ii . Width = int ( iiWidth . Int64 )
ii . Height = int ( iiHeight . Int64 )
ii . FileSize = int ( iiFileSize . Int64 )
ii . ResizeTarget = int ( iiResizeTarget . Int64 )
ii . Clock = uint64 ( iiClock . Int64 )
2022-06-27 13:48:00 +00:00
// Don't process empty identity images
if ! ii . IsEmpty ( ) {
2022-06-24 14:06:13 +00:00
acc . Images = append ( acc . Images , * ii )
}
}
return acc , nil
}
2019-08-20 15:38:40 +00:00
func ( db * Database ) SaveAccount ( account Account ) error {
2022-04-08 17:54:29 +00:00
colorHash , err := json . Marshal ( account . ColorHash )
if err != nil {
return err
}
2022-06-24 23:09:01 +00:00
2022-09-27 20:27:20 +00:00
if account . KDFIterations <= 0 {
2023-10-02 09:28:42 +00:00
account . KDFIterations = dbsetup . ReducedKDFIterationsNumber
2022-09-27 20:27:20 +00:00
}
2023-07-18 13:35:06 +00:00
_ , err = db . db . Exec ( "INSERT OR REPLACE INTO accounts (name, identicon, colorHash, colorId, customizationColor, customizationColorClock, keycardPairing, keyUid, kdfIterations) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" , account . Name , account . Identicon , colorHash , account . ColorID , account . CustomizationColor , account . CustomizationColorClock , account . KeycardPairing , account . KeyUID , account . KDFIterations )
2022-06-24 23:09:01 +00:00
if err != nil {
return err
}
if account . Images == nil {
return nil
}
2022-06-27 13:48:00 +00:00
return db . StoreIdentityImages ( account . KeyUID , account . Images , false )
2019-08-20 15:38:40 +00:00
}
2024-02-01 15:43:41 +00:00
func ( db * Database ) UpdateDisplayName ( keyUID string , displayName string ) error {
_ , err := db . db . Exec ( "UPDATE accounts SET name = ? WHERE keyUid = ?" , displayName , keyUID )
return err
}
2019-08-20 15:38:40 +00:00
func ( db * Database ) UpdateAccount ( account Account ) error {
2022-04-08 17:54:29 +00:00
colorHash , err := json . Marshal ( account . ColorHash )
if err != nil {
return err
}
2022-09-27 20:27:20 +00:00
if account . KDFIterations <= 0 {
2023-10-02 09:28:42 +00:00
account . KDFIterations = dbsetup . ReducedKDFIterationsNumber
2022-09-27 20:27:20 +00:00
}
2023-07-18 13:35:06 +00:00
_ , err = db . db . Exec ( "UPDATE accounts SET name = ?, identicon = ?, colorHash = ?, colorId = ?, customizationColor = ?, customizationColorClock = ?, keycardPairing = ?, kdfIterations = ? WHERE keyUid = ?" , account . Name , account . Identicon , colorHash , account . ColorID , account . CustomizationColor , account . CustomizationColorClock , account . KeycardPairing , account . KDFIterations , account . KeyUID )
2019-08-20 15:38:40 +00:00
return err
}
2023-01-25 10:25:07 +00:00
func ( db * Database ) UpdateAccountKeycardPairing ( keyUID string , keycardPairing string ) error {
_ , err := db . db . Exec ( "UPDATE accounts SET keycardPairing = ? WHERE keyUid = ?" , keycardPairing , keyUID )
2021-07-20 11:48:10 +00:00
return err
}
2019-12-05 08:00:57 +00:00
func ( db * Database ) UpdateAccountTimestamp ( keyUID string , loginTimestamp int64 ) error {
_ , err := db . db . Exec ( "UPDATE accounts SET loginTimestamp = ? WHERE keyUid = ?" , loginTimestamp , keyUID )
2019-08-20 15:38:40 +00:00
return err
}
2024-02-07 15:20:54 +00:00
func ( db * Database ) UpdateAccountCustomizationColor ( keyUID string , color string , clock uint64 ) ( int64 , error ) {
result , err := db . db . Exec ( "UPDATE accounts SET customizationColor = ?, customizationColorClock = ? WHERE keyUid = ? AND customizationColorClock < ?" , color , clock , keyUID , clock )
if err != nil {
return 0 , err
}
return result . RowsAffected ( )
2023-07-18 13:35:06 +00:00
}
2019-12-05 08:00:57 +00:00
func ( db * Database ) DeleteAccount ( keyUID string ) error {
_ , err := db . db . Exec ( "DELETE FROM accounts WHERE keyUid = ?" , keyUID )
2019-08-20 15:38:40 +00:00
return err
}
2020-11-24 13:13:46 +00:00
// Account images
2020-12-16 18:17:38 +00:00
func ( db * Database ) GetIdentityImages ( keyUID string ) ( iis [ ] * images . IdentityImage , err error ) {
2022-03-24 09:35:56 +00:00
rows , err := db . db . Query ( ` SELECT key_uid, name, image_payload, width, height, file_size, resize_target, clock FROM identity_images WHERE key_uid = ? ` , keyUID )
2020-11-24 13:13:46 +00:00
if err != nil {
return nil , err
}
2020-12-16 18:17:38 +00:00
defer func ( ) {
2022-04-12 16:21:31 +00:00
errClose := rows . Close ( )
err = valueOr ( err , errClose )
2020-12-16 18:17:38 +00:00
} ( )
2020-11-24 13:13:46 +00:00
for rows . Next ( ) {
ii := & images . IdentityImage { }
2022-03-24 09:35:56 +00:00
err = rows . Scan ( & ii . KeyUID , & ii . Name , & ii . Payload , & ii . Width , & ii . Height , & ii . FileSize , & ii . ResizeTarget , & ii . Clock )
2020-11-24 13:13:46 +00:00
if err != nil {
return nil , err
}
iis = append ( iis , ii )
}
return iis , nil
}
2020-11-24 23:16:19 +00:00
func ( db * Database ) GetIdentityImage ( keyUID , it string ) ( * images . IdentityImage , error ) {
2020-11-24 13:13:46 +00:00
var ii images . IdentityImage
2022-03-24 09:35:56 +00:00
err := db . db . QueryRow ( "SELECT key_uid, name, image_payload, width, height, file_size, resize_target, clock FROM identity_images WHERE key_uid = ? AND name = ?" , keyUID , it ) . Scan ( & ii . KeyUID , & ii . Name , & ii . Payload , & ii . Width , & ii . Height , & ii . FileSize , & ii . ResizeTarget , & ii . Clock )
2020-12-15 15:28:05 +00:00
if err == sql . ErrNoRows {
return nil , nil
} else if err != nil {
return nil , err
2020-11-24 13:13:46 +00:00
}
return & ii , nil
}
2022-06-27 13:48:00 +00:00
func ( db * Database ) StoreIdentityImages ( keyUID string , iis [ ] images . IdentityImage , publish bool ) ( err error ) {
2020-11-24 13:13:46 +00:00
// Because SQL INSERTs are triggered in a loop use a tx to ensure a single call to the DB.
tx , err := db . db . BeginTx ( context . Background ( ) , & sql . TxOptions { } )
if err != nil {
return err
}
defer func ( ) {
if err == nil {
err = tx . Commit ( )
return
}
2022-04-12 16:21:31 +00:00
errRollback := tx . Rollback ( )
err = valueOr ( err , errRollback )
2020-11-24 13:13:46 +00:00
} ( )
2022-08-07 07:35:54 +00:00
for i , ii := range iis {
2022-06-27 13:48:00 +00:00
if ii . IsEmpty ( ) {
2020-11-24 13:13:46 +00:00
continue
}
2022-08-07 07:35:54 +00:00
iis [ i ] . KeyUID = keyUID
2020-11-24 13:13:46 +00:00
_ , err := tx . Exec (
2022-03-24 09:35:56 +00:00
"INSERT INTO identity_images (key_uid, name, image_payload, width, height, file_size, resize_target, clock) VALUES (?, ?, ?, ?, ?, ?, ?, ?)" ,
2022-08-07 07:35:54 +00:00
keyUID ,
2020-11-24 13:13:46 +00:00
ii . Name ,
ii . Payload ,
ii . Width ,
ii . Height ,
ii . FileSize ,
ii . ResizeTarget ,
2022-03-24 09:35:56 +00:00
ii . Clock ,
2020-11-24 13:13:46 +00:00
)
if err != nil {
return err
}
}
2023-10-24 10:15:32 +00:00
db . publishOnIdentityImageSubscriptions ( & IdentityImageSubscriptionChange {
PublishExpected : publish ,
} )
2020-12-21 08:41:50 +00:00
2020-11-24 13:13:46 +00:00
return nil
}
2023-10-24 10:15:32 +00:00
func ( db * Database ) SubscribeToIdentityImageChanges ( ) chan * IdentityImageSubscriptionChange {
s := make ( chan * IdentityImageSubscriptionChange , 100 )
2020-12-21 08:41:50 +00:00
db . identityImageSubscriptions = append ( db . identityImageSubscriptions , s )
return s
}
2023-10-24 10:15:32 +00:00
func ( db * Database ) publishOnIdentityImageSubscriptions ( change * IdentityImageSubscriptionChange ) {
2020-12-21 08:41:50 +00:00
// Publish on channels, drop if buffer is full
for _ , s := range db . identityImageSubscriptions {
select {
2023-10-24 10:15:32 +00:00
case s <- change :
2020-12-21 08:41:50 +00:00
default :
log . Warn ( "subscription channel full, dropping message" )
}
}
}
2020-11-24 23:16:19 +00:00
func ( db * Database ) DeleteIdentityImage ( keyUID string ) error {
_ , err := db . db . Exec ( ` DELETE FROM identity_images WHERE key_uid = ? ` , keyUID )
2024-01-24 20:09:28 +00:00
if err != nil {
return err
}
db . publishOnIdentityImageSubscriptions ( & IdentityImageSubscriptionChange {
PublishExpected : true ,
} )
2020-11-24 13:13:46 +00:00
return err
}
2022-04-12 16:21:31 +00:00
func valueOr ( value error , or error ) error {
if value != nil {
return value
}
return or
}