Add KeyUID to IdentifiedAccountInfo

This field will be used as a deterministic id of multiaccount on
status-react side instead of master key address. The reason why
it is calculated as sha256 of public key is that this way is
already used on keycard side and it will simplify integration.

Rename keycardKeyUid to keyUid

As long as this field will be present in all multiaccounts from now on it
shouldn't be named as keycard specific.
This commit is contained in:
Roman Volosovskyi 2019-11-21 12:04:52 +02:00
parent 10635d555f
commit 5f6c7008e1
No known key found for this signature in database
GPG Key ID: 0238A4B5ECEE70DE
5 changed files with 251 additions and 49 deletions

View File

@ -163,7 +163,7 @@ setup-build: lint-install release-install gomobile-install ##@other Prepare proj
setup: setup-build setup-dev tidy ##@other Prepare project for development and building setup: setup-build setup-dev tidy ##@other Prepare project for development and building
generate: ##@other Regenerate assets and other auto-generated stuff generate: ##@other Regenerate assets and other auto-generated stuff
go generate ./static ./static/mailserver_db_migrations ./t go generate ./static ./static/mailserver_db_migrations ./t ./multiaccounts/...
prepare-release: clean-release prepare-release: clean-release
mkdir -p $(RELEASE_DIR) mkdir -p $(RELEASE_DIR)

View File

@ -2,10 +2,11 @@ package generator
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/sha256"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/extkeys" "github.com/status-im/status-go/extkeys"
protocol "github.com/status-im/status-go/protocol/types" statusproto "github.com/status-im/status-go/protocol/types"
) )
type account struct { type account struct {
@ -14,7 +15,7 @@ type account struct {
} }
func (a *account) toAccountInfo() AccountInfo { func (a *account) toAccountInfo() AccountInfo {
publicKeyHex := protocol.EncodeHex(crypto.FromECDSAPub(&a.privateKey.PublicKey)) publicKeyHex := statusproto.EncodeHex(crypto.FromECDSAPub(&a.privateKey.PublicKey))
addressHex := crypto.PubkeyToAddress(a.privateKey.PublicKey).Hex() addressHex := crypto.PubkeyToAddress(a.privateKey.PublicKey).Hex()
return AccountInfo{ return AccountInfo{
@ -25,9 +26,12 @@ func (a *account) toAccountInfo() AccountInfo {
func (a *account) toIdentifiedAccountInfo(id string) IdentifiedAccountInfo { func (a *account) toIdentifiedAccountInfo(id string) IdentifiedAccountInfo {
info := a.toAccountInfo() info := a.toAccountInfo()
keyUID := sha256.Sum256(crypto.FromECDSAPub(&a.privateKey.PublicKey))
keyUIDHex := statusproto.EncodeHex(keyUID[:])
return IdentifiedAccountInfo{ return IdentifiedAccountInfo{
AccountInfo: info, AccountInfo: info,
ID: id, ID: id,
KeyUID: keyUIDHex,
} }
} }
@ -49,6 +53,12 @@ type AccountInfo struct {
type IdentifiedAccountInfo struct { type IdentifiedAccountInfo struct {
AccountInfo AccountInfo
ID string `json:"id"` ID string `json:"id"`
// KeyUID is calculated as sha256 of the master public key and used for key
// identification. This is the only information available about the master
// key stored on a keycard before the card is paired.
// KeyUID name is chosen over KeyID in order to make it consistent with
// the name already used in Status and Keycard codebases.
KeyUID string `json:"keyUid"`
} }
// GeneratedAccountInfo contains IdentifiedAccountInfo and the mnemonic of an account. // GeneratedAccountInfo contains IdentifiedAccountInfo and the mnemonic of an account.

View File

@ -15,7 +15,7 @@ type Account struct {
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
PhotoPath string `json:"photo-path"` PhotoPath string `json:"photo-path"`
KeycardPairing string `json:"keycard-pairing"` KeycardPairing string `json:"keycard-pairing"`
KeycardKeyUID string `json:"keycard-key-uid"` KeyUID string `json:"key-uid"`
} }
// InitializeDB creates db file at a given path and applies migrations. // InitializeDB creates db file at a given path and applies migrations.
@ -40,7 +40,7 @@ func (db *Database) Close() error {
} }
func (db *Database) GetAccounts() ([]Account, error) { func (db *Database) GetAccounts() ([]Account, error) {
rows, err := db.db.Query("SELECT address, name, loginTimestamp, photoPath, keycardPairing, keycardKeyUid from accounts ORDER BY loginTimestamp DESC") rows, err := db.db.Query("SELECT address, name, loginTimestamp, photoPath, keycardPairing, keyUid from accounts ORDER BY loginTimestamp DESC")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -48,7 +48,7 @@ func (db *Database) GetAccounts() ([]Account, error) {
inthelper := sql.NullInt64{} inthelper := sql.NullInt64{}
for rows.Next() { for rows.Next() {
acc := Account{} acc := Account{}
err = rows.Scan(&acc.Address, &acc.Name, &inthelper, &acc.PhotoPath, &acc.KeycardPairing, &acc.KeycardKeyUID) err = rows.Scan(&acc.Address, &acc.Name, &inthelper, &acc.PhotoPath, &acc.KeycardPairing, &acc.KeyUID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -59,12 +59,12 @@ func (db *Database) GetAccounts() ([]Account, error) {
} }
func (db *Database) SaveAccount(account Account) error { func (db *Database) SaveAccount(account Account) error {
_, err := db.db.Exec("INSERT OR REPLACE INTO accounts (address, name, photoPath, keycardPairing, keycardKeyUid) VALUES (?, ?, ?, ?, ?)", account.Address, account.Name, account.PhotoPath, account.KeycardPairing, account.KeycardKeyUID) _, err := db.db.Exec("INSERT OR REPLACE INTO accounts (address, name, photoPath, keycardPairing, keyUid) VALUES (?, ?, ?, ?, ?)", account.Address, account.Name, account.PhotoPath, account.KeycardPairing, account.KeyUID)
return err return err
} }
func (db *Database) UpdateAccount(account Account) error { func (db *Database) UpdateAccount(account Account) error {
_, err := db.db.Exec("UPDATE accounts SET name = ?, photoPath = ?, keycardPairing = ?, keycardKeyUid = ? WHERE address = ?", account.Name, account.PhotoPath, account.KeycardPairing, account.KeycardKeyUID, account.Address) _, err := db.db.Exec("UPDATE accounts SET name = ?, photoPath = ?, keycardPairing = ?, keyUid = ? WHERE address = ?", account.Name, account.PhotoPath, account.KeycardPairing, account.KeyUID, account.Address)
return err return err
} }

View File

@ -1,68 +1,216 @@
// Code generated by go-bindata. DO NOT EDIT.
// sources:
// 0001_accounts.down.sql (21B)
// 0001_accounts.up.sql (177B)
// doc.go (74B)
package migrations package migrations
import ( import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/sha256"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os"
"path/filepath"
"strings" "strings"
"time"
) )
func bindata_read(data []byte, name string) ([]byte, error) { func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data)) gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil { if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err) return nil, fmt.Errorf("read %q: %v", name, err)
} }
var buf bytes.Buffer var buf bytes.Buffer
_, err = io.Copy(&buf, gz) _, err = io.Copy(&buf, gz)
gz.Close() clErr := gz.Close()
if err != nil { if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err) return nil, fmt.Errorf("read %q: %v", name, err)
}
if clErr != nil {
return nil, err
} }
return buf.Bytes(), nil return buf.Bytes(), nil
} }
var __0001_accounts_down_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x48\x4c\x4e\xce\x2f\xcd\x2b\x29\xb6\xe6\x02\x04\x00\x00\xff\xff\x96\x1e\x13\xa1\x15\x00\x00\x00") type asset struct {
bytes []byte
info os.FileInfo
digest [sha256.Size]byte
}
func _0001_accounts_down_sql() ([]byte, error) { type bindataFileInfo struct {
return bindata_read( name string
__0001_accounts_down_sql, size int64
mode os.FileMode
modTime time.Time
}
func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var __0001_accountsDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x48\x4c\x4e\xce\x2f\xcd\x2b\x29\xb6\xe6\x02\x04\x00\x00\xff\xff\x96\x1e\x13\xa1\x15\x00\x00\x00")
func _0001_accountsDownSqlBytes() ([]byte, error) {
return bindataRead(
__0001_accountsDownSql,
"0001_accounts.down.sql", "0001_accounts.down.sql",
) )
} }
var __0001_accounts_up_sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\xcb\x31\x8e\x84\x20\x14\x00\xd0\x9e\x53\xfc\x72\x37\xe1\x06\x5b\xa1\xcb\xae\x44\x47\x0d\x7e\x47\x2d\x7f\x84\x28\x99\x11\x8c\x30\x85\xb7\x9f\xc4\x6e\xda\x97\xbc\x5c\x4b\x81\x12\x50\x64\x95\x04\xf5\x07\x75\x83\x20\x47\xd5\x61\x07\x34\xcf\xe1\xe5\x53\x84\x2f\x46\xc6\x1c\x36\x46\xb8\x0b\x9d\x17\x42\x43\xab\xd5\x4d\xe8\x09\x4a\x39\x71\xe6\x69\xb3\x80\x72\xc4\x2b\xd7\x7d\x55\x71\xf6\x0c\x8b\xf3\xe8\x36\x1b\x13\x6d\x3b\x64\xea\x1f\x54\x8d\x9c\xed\x6b\x48\xa1\xa5\xb4\x5e\x81\xb3\x87\x3d\x67\x3a\x4c\x4b\xee\x70\x7e\xf9\xc4\xd2\x9e\xbd\x33\x97\xb1\x6f\x18\x14\x16\x4d\x8f\xa0\x9b\x41\xfd\xfe\xb0\x77\x00\x00\x00\xff\xff\x96\x21\x96\xe1\xb8\x00\x00\x00") func _0001_accountsDownSql() (*asset, error) {
bytes, err := _0001_accountsDownSqlBytes()
if err != nil {
return nil, err
}
func _0001_accounts_up_sql() ([]byte, error) { info := bindataFileInfo{name: "0001_accounts.down.sql", size: 21, mode: os.FileMode(0644), modTime: time.Unix(1573216280, 0)}
return bindata_read( a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xd2, 0x61, 0x4c, 0x18, 0xfc, 0xc, 0xdf, 0x5c, 0x1f, 0x5e, 0xd3, 0xbd, 0xfa, 0x12, 0x5e, 0x8d, 0x8d, 0x8b, 0xb9, 0x5f, 0x99, 0x46, 0x63, 0xa5, 0xe3, 0xa6, 0x8a, 0x4, 0xf1, 0x73, 0x8a, 0xe9}}
__0001_accounts_up_sql, return a, nil
}
var __0001_accountsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x44\xcb\xb1\xae\x82\x30\x14\x80\xe1\xbd\x4f\x71\xc6\x7b\x93\xbe\x81\x53\xc1\x2a\x8d\x08\xa4\x1c\x04\xc6\x13\xda\x40\xa3\xb4\x84\xd6\xc1\xb7\x37\x61\x71\xfc\xff\xe4\xcb\xb5\x14\x28\x01\x45\x56\x4a\x50\x17\xa8\x6a\x04\x39\xa8\x16\x5b\xa0\x69\x0a\x6f\x9f\x22\xfc\x31\x32\x66\xb7\x31\xc2\x43\xe8\xbc\x10\x1a\x1a\xad\xee\x42\x8f\x70\x93\x23\x67\x9e\x56\x0b\x28\x07\x3c\x70\xd5\x95\x25\x67\xaf\x30\x3b\x8f\x6e\xb5\x31\xd1\xba\x41\xa6\xae\xa0\x2a\xe4\x6c\x5b\x42\x0a\x0d\xa5\xe5\x00\x9c\x3d\xed\x67\xa2\xdd\x34\xe4\x76\xe7\xe7\xdf\xec\x9c\x39\x82\xfd\x43\xaf\xb0\xa8\x3b\x04\x5d\xf7\xea\x7c\x62\xdf\x00\x00\x00\xff\xff\xf9\xc7\x34\x46\xb1\x00\x00\x00")
func _0001_accountsUpSqlBytes() ([]byte, error) {
return bindataRead(
__0001_accountsUpSql,
"0001_accounts.up.sql", "0001_accounts.up.sql",
) )
} }
var _doc_go = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xc9\xb1\x0d\xc4\x20\x0c\x05\xd0\x9e\x29\xfe\x02\xd8\xfd\x6d\xe3\x4b\xac\x2f\x44\x82\x09\x78\x7f\xa5\x49\xfd\xa6\x1d\xdd\xe8\xd8\xcf\x55\x8a\x2a\xe3\x47\x1f\xbe\x2c\x1d\x8c\xfa\x6f\xe3\xb4\x34\xd4\xd9\x89\xbb\x71\x59\xb6\x18\x1b\x35\x20\xa2\x9f\x0a\x03\xa2\xe5\x0d\x00\x00\xff\xff\x60\xcd\x06\xbe\x4a\x00\x00\x00") func _0001_accountsUpSql() (*asset, error) {
bytes, err := _0001_accountsUpSqlBytes()
if err != nil {
return nil, err
}
func doc_go() ([]byte, error) { info := bindataFileInfo{name: "0001_accounts.up.sql", size: 177, mode: os.FileMode(0644), modTime: time.Unix(1574408465, 0)}
return bindata_read( a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x3e, 0x24, 0xcf, 0x7a, 0x7b, 0xb0, 0xf0, 0x95, 0x3f, 0x45, 0x63, 0x9c, 0xe1, 0xca, 0x8f, 0xe4, 0x1a, 0x83, 0x8b, 0x19, 0x1f, 0x11, 0x98, 0xc4, 0x2b, 0xa5, 0x2f, 0x79, 0x96, 0xc2, 0x8f, 0x75}}
_doc_go, return a, nil
}
var _docGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xc9\xb1\x0d\xc4\x20\x0c\x05\xd0\x9e\x29\xfe\x02\xd8\xfd\x6d\xe3\x4b\xac\x2f\x44\x82\x09\x78\x7f\xa5\x49\xfd\xa6\x1d\xdd\xe8\xd8\xcf\x55\x8a\x2a\xe3\x47\x1f\xbe\x2c\x1d\x8c\xfa\x6f\xe3\xb4\x34\xd4\xd9\x89\xbb\x71\x59\xb6\x18\x1b\x35\x20\xa2\x9f\x0a\x03\xa2\xe5\x0d\x00\x00\xff\xff\x60\xcd\x06\xbe\x4a\x00\x00\x00")
func docGoBytes() ([]byte, error) {
return bindataRead(
_docGo,
"doc.go", "doc.go",
) )
} }
func docGo() (*asset, error) {
bytes, err := docGoBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "doc.go", size: 74, mode: os.FileMode(0644), modTime: time.Unix(1573216280, 0)}
a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xde, 0x7c, 0x28, 0xcd, 0x47, 0xf2, 0xfa, 0x7c, 0x51, 0x2d, 0xd8, 0x38, 0xb, 0xb0, 0x34, 0x9d, 0x4c, 0x62, 0xa, 0x9e, 0x28, 0xc3, 0x31, 0x23, 0xd9, 0xbb, 0x89, 0x9f, 0xa0, 0x89, 0x1f, 0xe8}}
return a, nil
}
// Asset loads and returns the asset for the given name. // Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or // It returns an error if the asset could not be found or
// could not be loaded. // could not be loaded.
func Asset(name string) ([]byte, error) { func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1) canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok { if f, ok := _bindata[canonicalName]; ok {
return f() a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
} }
return nil, fmt.Errorf("Asset %s not found", name) return nil, fmt.Errorf("Asset %s not found", name)
} }
// AssetString returns the asset contents as a string (instead of a []byte).
func AssetString(name string) (string, error) {
data, err := Asset(name)
return string(data), err
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// MustAssetString is like AssetString but panics when Asset would return an
// error. It simplifies safe initialization of global variables.
func MustAssetString(name string) string {
return string(MustAsset(name))
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetDigest returns the digest of the file with the given name. It returns an
// error if the asset could not be found or the digest could not be loaded.
func AssetDigest(name string) ([sha256.Size]byte, error) {
canonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[canonicalName]; ok {
a, err := f()
if err != nil {
return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err)
}
return a.digest, nil
}
return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name)
}
// Digests returns a map of all known files and their checksums.
func Digests() (map[string][sha256.Size]byte, error) {
mp := make(map[string][sha256.Size]byte, len(_bindata))
for name := range _bindata {
a, err := _bindata[name]()
if err != nil {
return nil, err
}
mp[name] = a.digest
}
return mp, nil
}
// AssetNames returns the names of the assets. // AssetNames returns the names of the assets.
func AssetNames() []string { func AssetNames() []string {
names := make([]string, 0, len(_bindata)) names := make([]string, 0, len(_bindata))
@ -73,11 +221,14 @@ func AssetNames() []string {
} }
// _bindata is a table, holding each asset generator, mapped to its name. // _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() ([]byte, error){ var _bindata = map[string]func() (*asset, error){
"0001_accounts.down.sql": _0001_accounts_down_sql, "0001_accounts.down.sql": _0001_accountsDownSql,
"0001_accounts.up.sql": _0001_accounts_up_sql,
"doc.go": doc_go, "0001_accounts.up.sql": _0001_accountsUpSql,
"doc.go": docGo,
} }
// AssetDir returns the file names below a certain // AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata. // directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the // For example if you run go-bindata on data/... and data contains the
@ -87,15 +238,15 @@ var _bindata = map[string]func() ([]byte, error){
// img/ // img/
// a.png // a.png
// b.png // b.png
// then AssetDir("data") would return []string{"foo.txt", "img"} // then AssetDir("data") would return []string{"foo.txt", "img"},
// AssetDir("data/img") would return []string{"a.png", "b.png"} // AssetDir("data/img") would return []string{"a.png", "b.png"},
// AssetDir("foo.txt") and AssetDir("notexist") would return an error // AssetDir("foo.txt") and AssetDir("notexist") would return an error, and
// AssetDir("") will return []string{"data"}. // AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) { func AssetDir(name string) ([]string, error) {
node := _bintree node := _bintree
if len(name) != 0 { if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1) canonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/") pathList := strings.Split(canonicalName, "/")
for _, p := range pathList { for _, p := range pathList {
node = node.Children[p] node = node.Children[p]
if node == nil { if node == nil {
@ -107,21 +258,62 @@ func AssetDir(name string) ([]string, error) {
return nil, fmt.Errorf("Asset %s not found", name) return nil, fmt.Errorf("Asset %s not found", name)
} }
rv := make([]string, 0, len(node.Children)) rv := make([]string, 0, len(node.Children))
for name := range node.Children { for childName := range node.Children {
rv = append(rv, name) rv = append(rv, childName)
} }
return rv, nil return rv, nil
} }
type _bintree_t struct { type bintree struct {
Func func() ([]byte, error) Func func() (*asset, error)
Children map[string]*_bintree_t Children map[string]*bintree
} }
var _bintree = &_bintree_t{nil, map[string]*_bintree_t{
"0001_accounts.down.sql": &_bintree_t{_0001_accounts_down_sql, map[string]*_bintree_t{ var _bintree = &bintree{nil, map[string]*bintree{
}}, "0001_accounts.down.sql": &bintree{_0001_accountsDownSql, map[string]*bintree{}},
"0001_accounts.up.sql": &_bintree_t{_0001_accounts_up_sql, map[string]*_bintree_t{ "0001_accounts.up.sql": &bintree{_0001_accountsUpSql, map[string]*bintree{}},
}}, "doc.go": &bintree{docGo, map[string]*bintree{}},
"doc.go": &_bintree_t{doc_go, map[string]*_bintree_t{
}},
}} }}
// RestoreAsset restores an asset under the given directory.
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
}
// RestoreAssets restores an asset under the given directory recursively.
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
canonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...)
}

View File

@ -4,5 +4,5 @@ name TEXT NOT NULL,
loginTimestamp BIG INT, loginTimestamp BIG INT,
photoPath TEXT, photoPath TEXT,
keycardPairing TEXT, keycardPairing TEXT,
keycardKeyUid TEXT keyUid TEXT
) WITHOUT ROWID; ) WITHOUT ROWID;