2022-06-10 15:32:15 +00:00
|
|
|
package server
|
|
|
|
|
2022-06-10 23:03:16 +00:00
|
|
|
import (
|
|
|
|
"crypto/ecdsa"
|
|
|
|
"crypto/rand"
|
2022-06-24 14:06:13 +00:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2022-06-10 23:03:16 +00:00
|
|
|
|
2022-06-24 14:06:13 +00:00
|
|
|
"github.com/golang/protobuf/proto"
|
|
|
|
|
2022-06-24 23:09:01 +00:00
|
|
|
"github.com/status-im/status-go/account/generator"
|
2022-06-24 14:06:13 +00:00
|
|
|
"github.com/status-im/status-go/eth-node/keystore"
|
|
|
|
"github.com/status-im/status-go/images"
|
|
|
|
"github.com/status-im/status-go/multiaccounts"
|
2022-06-10 23:03:16 +00:00
|
|
|
"github.com/status-im/status-go/protocol/common"
|
2022-06-24 14:06:13 +00:00
|
|
|
"github.com/status-im/status-go/protocol/protobuf"
|
2022-06-10 23:03:16 +00:00
|
|
|
)
|
|
|
|
|
2022-07-04 22:36:15 +00:00
|
|
|
// PayloadManager is the interface for PayloadManagers and wraps the basic functions for fulfilling payload management
|
2022-07-01 15:37:53 +00:00
|
|
|
type PayloadManager interface {
|
|
|
|
Mount() error
|
|
|
|
Receive(data []byte) error
|
|
|
|
ToSend() []byte
|
|
|
|
Received() []byte
|
|
|
|
}
|
|
|
|
|
2022-07-04 22:36:15 +00:00
|
|
|
// PairingPayloadManagerConfig represents the initialisation parameters required for a PairingPayloadManager
|
2022-07-01 15:37:53 +00:00
|
|
|
type PairingPayloadManagerConfig struct {
|
|
|
|
DB *multiaccounts.Database
|
|
|
|
KeystorePath, KeyUID, Password string
|
|
|
|
}
|
|
|
|
|
|
|
|
// PairingPayloadManager is responsible for the whole lifecycle of a PairingPayload
|
2022-06-29 15:21:22 +00:00
|
|
|
type PairingPayloadManager struct {
|
|
|
|
pem *PayloadEncryptionManager
|
|
|
|
ppm *PairingPayloadMarshaller
|
2022-07-01 15:37:53 +00:00
|
|
|
ppr PayloadRepository
|
2022-06-29 15:21:22 +00:00
|
|
|
}
|
|
|
|
|
2022-07-04 22:36:15 +00:00
|
|
|
// NewPairingPayloadManager generates a new and initialised PairingPayloadManager
|
2022-07-01 15:37:53 +00:00
|
|
|
func NewPairingPayloadManager(pk *ecdsa.PrivateKey, config *PairingPayloadManagerConfig) (*PairingPayloadManager, error) {
|
2022-06-29 15:21:22 +00:00
|
|
|
pem, err := NewPayloadEncryptionManager(pk)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-07-04 22:36:15 +00:00
|
|
|
// A new SHARED PairingPayload
|
|
|
|
p := new(PairingPayload)
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
return &PairingPayloadManager{
|
|
|
|
pem: pem,
|
2022-07-04 22:36:15 +00:00
|
|
|
ppm: NewPairingPayloadMarshaller(p),
|
|
|
|
ppr: NewPairingPayloadRepository(p, config),
|
2022-06-29 15:21:22 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2022-07-04 22:36:15 +00:00
|
|
|
// Mount loads and prepares the payload to be stored in the PairingPayloadManager's state ready for later access
|
2022-07-01 15:37:53 +00:00
|
|
|
func (ppm *PairingPayloadManager) Mount() error {
|
|
|
|
err := ppm.ppr.LoadFromSource()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
pb, err := ppm.ppm.MarshalToProtobuf()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ppm.pem.Encrypt(pb)
|
|
|
|
}
|
|
|
|
|
2022-07-04 22:36:15 +00:00
|
|
|
// Receive takes a []byte representing raw data, parses and stores the data
|
2022-07-01 15:37:53 +00:00
|
|
|
func (ppm *PairingPayloadManager) Receive(data []byte) error {
|
|
|
|
err := ppm.pem.Decrypt(data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = ppm.ppm.UnmarshalProtobuf(ppm.pem.Received())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return ppm.ppr.StoreToSource()
|
|
|
|
}
|
|
|
|
|
2022-07-04 22:36:15 +00:00
|
|
|
// ToSend returns the result of Mount
|
2022-07-01 15:37:53 +00:00
|
|
|
func (ppm *PairingPayloadManager) ToSend() []byte {
|
|
|
|
return ppm.pem.ToSend()
|
|
|
|
}
|
|
|
|
|
2022-07-04 22:36:15 +00:00
|
|
|
// Received returns the decrypted input of Receive
|
2022-07-01 15:37:53 +00:00
|
|
|
func (ppm *PairingPayloadManager) Received() []byte {
|
|
|
|
return ppm.pem.Received()
|
|
|
|
}
|
|
|
|
|
|
|
|
// EncryptionPayload represents the plain text and encrypted text of payload data
|
2022-06-29 15:21:22 +00:00
|
|
|
type EncryptionPayload struct {
|
2022-06-10 23:03:16 +00:00
|
|
|
plain []byte
|
|
|
|
encrypted []byte
|
|
|
|
}
|
|
|
|
|
2022-07-01 15:37:53 +00:00
|
|
|
// PayloadEncryptionManager is responsible for encrypting and decrypting payload data
|
2022-06-29 15:21:22 +00:00
|
|
|
type PayloadEncryptionManager struct {
|
2022-06-10 23:03:16 +00:00
|
|
|
aesKey []byte
|
2022-06-29 15:21:22 +00:00
|
|
|
toSend *EncryptionPayload
|
|
|
|
received *EncryptionPayload
|
2022-06-10 15:32:15 +00:00
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
func NewPayloadEncryptionManager(pk *ecdsa.PrivateKey) (*PayloadEncryptionManager, error) {
|
2022-06-10 23:03:16 +00:00
|
|
|
ek, err := makeEncryptionKey(pk)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
return &PayloadEncryptionManager{ek, new(EncryptionPayload), new(EncryptionPayload)}, nil
|
2022-06-10 15:32:15 +00:00
|
|
|
}
|
|
|
|
|
2022-07-01 15:37:53 +00:00
|
|
|
func (pem *PayloadEncryptionManager) Encrypt(data []byte) error {
|
2022-06-29 15:21:22 +00:00
|
|
|
ep, err := common.Encrypt(data, pem.aesKey, rand.Reader)
|
2022-06-10 23:03:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
pem.toSend.plain = data
|
|
|
|
pem.toSend.encrypted = ep
|
2022-06-10 23:03:16 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-07-01 15:37:53 +00:00
|
|
|
func (pem *PayloadEncryptionManager) Decrypt(data []byte) error {
|
2022-06-29 15:21:22 +00:00
|
|
|
pd, err := common.Decrypt(data, pem.aesKey)
|
2022-06-10 23:03:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
pem.received.encrypted = data
|
|
|
|
pem.received.plain = pd
|
2022-06-10 23:03:16 +00:00
|
|
|
return nil
|
2022-06-10 15:32:15 +00:00
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
func (pem *PayloadEncryptionManager) ToSend() []byte {
|
|
|
|
return pem.toSend.encrypted
|
2022-06-10 15:32:15 +00:00
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
func (pem *PayloadEncryptionManager) Received() []byte {
|
|
|
|
return pem.received.plain
|
2022-06-10 23:03:16 +00:00
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
func (pem *PayloadEncryptionManager) ResetPayload() {
|
|
|
|
pem.toSend = new(EncryptionPayload)
|
|
|
|
pem.received = new(EncryptionPayload)
|
2022-06-10 15:32:15 +00:00
|
|
|
}
|
2022-06-24 14:06:13 +00:00
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
// PairingPayload represents the payload structure a PairingServer handles
|
|
|
|
type PairingPayload struct {
|
2022-06-24 14:06:13 +00:00
|
|
|
keys map[string][]byte
|
|
|
|
multiaccount *multiaccounts.Account
|
|
|
|
password string
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
// PairingPayloadMarshaller is responsible for marshalling and unmarshalling PairingServer payload data
|
|
|
|
type PairingPayloadMarshaller struct {
|
|
|
|
*PairingPayload
|
|
|
|
}
|
|
|
|
|
2022-07-04 22:36:15 +00:00
|
|
|
func NewPairingPayloadMarshaller(p *PairingPayload) *PairingPayloadMarshaller {
|
|
|
|
return &PairingPayloadMarshaller{PairingPayload: p}
|
2022-07-01 15:37:53 +00:00
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
func (ppm *PairingPayloadMarshaller) MarshalToProtobuf() ([]byte, error) {
|
|
|
|
return proto.Marshal(&protobuf.LocalPairingPayload{
|
|
|
|
Keys: ppm.accountKeysToProtobuf(),
|
|
|
|
Multiaccount: ppm.multiaccountToProtobuf(),
|
|
|
|
Password: ppm.password,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ppm *PairingPayloadMarshaller) accountKeysToProtobuf() []*protobuf.LocalPairingPayload_Key {
|
|
|
|
var keys []*protobuf.LocalPairingPayload_Key
|
|
|
|
for name, data := range ppm.keys {
|
|
|
|
keys = append(keys, &protobuf.LocalPairingPayload_Key{Name: name, Data: data})
|
|
|
|
}
|
|
|
|
return keys
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ppm *PairingPayloadMarshaller) multiaccountToProtobuf() *protobuf.MultiAccount {
|
|
|
|
var colourHashes []*protobuf.MultiAccount_ColourHash
|
|
|
|
for _, index := range ppm.multiaccount.ColorHash {
|
|
|
|
var i []int64
|
|
|
|
for _, is := range index {
|
|
|
|
i = append(i, int64(is))
|
|
|
|
}
|
|
|
|
|
|
|
|
colourHashes = append(colourHashes, &protobuf.MultiAccount_ColourHash{Index: i})
|
|
|
|
}
|
|
|
|
|
|
|
|
var identityImages []*protobuf.MultiAccount_IdentityImage
|
|
|
|
for _, ii := range ppm.multiaccount.Images {
|
|
|
|
identityImages = append(identityImages, &protobuf.MultiAccount_IdentityImage{
|
|
|
|
KeyUid: ii.KeyUID,
|
|
|
|
Name: ii.Name,
|
|
|
|
Payload: ii.Payload,
|
|
|
|
Width: int64(ii.Width),
|
|
|
|
Height: int64(ii.Height),
|
|
|
|
Filesize: int64(ii.FileSize),
|
|
|
|
ResizeTarget: int64(ii.ResizeTarget),
|
|
|
|
Clock: ii.Clock,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return &protobuf.MultiAccount{
|
|
|
|
Name: ppm.multiaccount.Name,
|
|
|
|
Timestamp: ppm.multiaccount.Timestamp,
|
|
|
|
Identicon: ppm.multiaccount.Identicon,
|
|
|
|
ColorHash: colourHashes,
|
|
|
|
ColorId: ppm.multiaccount.ColorID,
|
|
|
|
KeycardPairing: ppm.multiaccount.KeycardPairing,
|
|
|
|
KeyUid: ppm.multiaccount.KeyUID,
|
|
|
|
Images: identityImages,
|
|
|
|
}
|
2022-06-28 23:08:57 +00:00
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
func (ppm *PairingPayloadMarshaller) UnmarshalProtobuf(data []byte) error {
|
|
|
|
pb := new(protobuf.LocalPairingPayload)
|
|
|
|
err := proto.Unmarshal(data, pb)
|
2022-06-24 14:06:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
ppm.accountKeysFromProtobuf(pb.Keys)
|
|
|
|
ppm.multiaccountFromProtobuf(pb.Multiaccount)
|
|
|
|
ppm.password = pb.Password
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ppm *PairingPayloadMarshaller) accountKeysFromProtobuf(pbKeys []*protobuf.LocalPairingPayload_Key) {
|
|
|
|
if ppm.keys == nil {
|
|
|
|
ppm.keys = make(map[string][]byte)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, key := range pbKeys {
|
|
|
|
ppm.keys[key.Name] = key.Data
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (ppm *PairingPayloadMarshaller) multiaccountFromProtobuf(pbMultiAccount *protobuf.MultiAccount) {
|
|
|
|
var colourHash [][]int
|
|
|
|
for _, index := range pbMultiAccount.ColorHash {
|
|
|
|
var i []int
|
|
|
|
for _, is := range index.Index {
|
|
|
|
i = append(i, int(is))
|
|
|
|
}
|
|
|
|
|
|
|
|
colourHash = append(colourHash, i)
|
|
|
|
}
|
|
|
|
|
|
|
|
var identityImages []images.IdentityImage
|
|
|
|
for _, ii := range pbMultiAccount.Images {
|
|
|
|
identityImages = append(identityImages, images.IdentityImage{
|
|
|
|
KeyUID: ii.KeyUid,
|
|
|
|
Name: ii.Name,
|
|
|
|
Payload: ii.Payload,
|
|
|
|
Width: int(ii.Width),
|
|
|
|
Height: int(ii.Height),
|
|
|
|
FileSize: int(ii.Filesize),
|
|
|
|
ResizeTarget: int(ii.ResizeTarget),
|
|
|
|
Clock: ii.Clock,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
ppm.multiaccount = &multiaccounts.Account{
|
|
|
|
Name: pbMultiAccount.Name,
|
|
|
|
Timestamp: pbMultiAccount.Timestamp,
|
|
|
|
Identicon: pbMultiAccount.Identicon,
|
|
|
|
ColorHash: colourHash,
|
|
|
|
ColorID: pbMultiAccount.ColorId,
|
|
|
|
KeycardPairing: pbMultiAccount.KeycardPairing,
|
|
|
|
KeyUID: pbMultiAccount.KeyUid,
|
|
|
|
Images: identityImages,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-01 15:37:53 +00:00
|
|
|
type PayloadRepository interface {
|
|
|
|
LoadFromSource() error
|
|
|
|
StoreToSource() error
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
// PairingPayloadRepository is responsible for loading, parsing, validating and storing PairingServer payload data
|
|
|
|
type PairingPayloadRepository struct {
|
|
|
|
*PairingPayload
|
|
|
|
|
|
|
|
multiaccountsDB *multiaccounts.Database
|
2022-07-01 15:37:53 +00:00
|
|
|
|
|
|
|
keystorePath, keyUID string
|
2022-06-29 15:21:22 +00:00
|
|
|
}
|
|
|
|
|
2022-07-04 22:36:15 +00:00
|
|
|
func NewPairingPayloadRepository(p *PairingPayload, config *PairingPayloadManagerConfig) *PairingPayloadRepository {
|
2022-07-01 15:37:53 +00:00
|
|
|
ppr := &PairingPayloadRepository{
|
2022-07-04 22:36:15 +00:00
|
|
|
PairingPayload: p,
|
2022-06-29 15:21:22 +00:00
|
|
|
}
|
2022-07-01 15:37:53 +00:00
|
|
|
|
|
|
|
if config == nil {
|
|
|
|
return ppr
|
|
|
|
}
|
|
|
|
|
|
|
|
ppr.multiaccountsDB = config.DB
|
|
|
|
ppr.keystorePath = config.KeystorePath
|
|
|
|
ppr.keyUID = config.KeyUID
|
|
|
|
ppr.password = config.Password
|
|
|
|
return ppr
|
2022-06-29 15:21:22 +00:00
|
|
|
}
|
|
|
|
|
2022-07-01 15:37:53 +00:00
|
|
|
func (ppr *PairingPayloadRepository) LoadFromSource() error {
|
|
|
|
err := ppr.loadKeys(ppr.keystorePath)
|
2022-06-24 14:06:13 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-06-29 15:21:22 +00:00
|
|
|
|
2022-07-01 15:37:53 +00:00
|
|
|
err = ppr.validateKeys(ppr.password)
|
2022-06-29 15:21:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-07-01 15:37:53 +00:00
|
|
|
ppr.multiaccount, err = ppr.multiaccountsDB.GetAccount(ppr.keyUID)
|
2022-06-29 15:21:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-06-24 14:06:13 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
func (ppr *PairingPayloadRepository) loadKeys(keyStorePath string) error {
|
|
|
|
ppr.keys = make(map[string][]byte)
|
2022-06-24 14:06:13 +00:00
|
|
|
|
|
|
|
fileWalker := func(path string, fileInfo os.FileInfo, err error) error {
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if fileInfo.IsDir() || filepath.Dir(path) != keyStorePath {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
rawKeyFile, err := ioutil.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid account key file: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
accountKey := new(keystore.EncryptedKeyJSONV3)
|
|
|
|
if err := json.Unmarshal(rawKeyFile, &accountKey); err != nil {
|
|
|
|
return fmt.Errorf("failed to read key file: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(accountKey.Address) != 40 {
|
|
|
|
return fmt.Errorf("account key address has invalid length '%s'", accountKey.Address)
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
ppr.keys[fileInfo.Name()] = rawKeyFile
|
2022-06-24 14:06:13 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err := filepath.Walk(keyStorePath, fileWalker)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("cannot traverse key store folder: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-07-01 15:37:53 +00:00
|
|
|
func (ppr *PairingPayloadRepository) StoreToSource() error {
|
|
|
|
err := ppr.validateKeys(ppr.password)
|
2022-06-24 23:09:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-07-01 15:37:53 +00:00
|
|
|
err = ppr.storeKeys(ppr.keystorePath)
|
2022-06-24 23:09:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
err = ppr.storeMultiAccount()
|
2022-06-24 23:09:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
// TODO install PublicKey into settings, probably do this outside of StoreToSource
|
2022-06-24 23:09:01 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
func (ppr *PairingPayloadRepository) validateKeys(password string) error {
|
|
|
|
for _, key := range ppr.keys {
|
2022-06-24 23:09:01 +00:00
|
|
|
k, err := keystore.DecryptKey(key, password)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = generator.ValidateKeystoreExtendedKey(k)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
func (ppr *PairingPayloadRepository) storeKeys(keyStorePath string) error {
|
|
|
|
for name, data := range ppr.keys {
|
2022-06-24 23:09:01 +00:00
|
|
|
accountKey := new(keystore.EncryptedKeyJSONV3)
|
|
|
|
if err := json.Unmarshal(data, &accountKey); err != nil {
|
|
|
|
return fmt.Errorf("failed to read key file: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(accountKey.Address) != 40 {
|
|
|
|
return fmt.Errorf("account key address has invalid length '%s'", accountKey.Address)
|
|
|
|
}
|
|
|
|
|
|
|
|
err := ioutil.WriteFile(filepath.Join(keyStorePath, name), data, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-06-29 15:21:22 +00:00
|
|
|
func (ppr *PairingPayloadRepository) storeMultiAccount() error {
|
|
|
|
return ppr.multiaccountsDB.SaveAccount(*ppr.multiaccount)
|
2022-06-24 14:06:13 +00:00
|
|
|
}
|