2020-12-27 13:44:23 +00:00
|
|
|
// Copyright 2017-2021 DERO Project. All rights reserved.
|
|
|
|
// Use of this source code in any form is governed by RESEARCH license.
|
|
|
|
// license can be found in the LICENSE file.
|
|
|
|
// GPG: 0F39 E425 8C65 3947 702A 8234 08B2 0360 A03A 9DE8
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
|
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
|
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
|
|
|
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
|
|
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
|
|
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
|
|
|
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
|
|
|
|
package walletapi
|
|
|
|
|
|
|
|
import "fmt"
|
|
|
|
import "time"
|
|
|
|
import "crypto/rand"
|
|
|
|
import "crypto/sha1"
|
|
|
|
import "sync"
|
|
|
|
import "runtime"
|
|
|
|
|
|
|
|
//import "strings"
|
|
|
|
//import "math/big"
|
|
|
|
//import "encoding/hex"
|
|
|
|
import "encoding/json"
|
|
|
|
|
|
|
|
//import "encoding/binary"
|
|
|
|
|
|
|
|
import "github.com/romana/rlog"
|
|
|
|
|
|
|
|
//import "github.com/vmihailenco/msgpack"
|
|
|
|
|
|
|
|
import "github.com/blang/semver"
|
|
|
|
import "golang.org/x/crypto/pbkdf2" // // used to encrypt master password ( so user can change his password anytime)
|
|
|
|
|
2021-02-22 17:48:14 +00:00
|
|
|
import "github.com/deroproject/derohe/cryptography/crypto"
|
2020-12-27 13:44:23 +00:00
|
|
|
import "github.com/deroproject/derohe/config"
|
|
|
|
import "github.com/deroproject/derohe/walletapi/mnemonics"
|
|
|
|
|
|
|
|
// address book will have random number based entries
|
|
|
|
|
|
|
|
// see this https://godoc.org/golang.org/x/crypto/pbkdf2
|
|
|
|
type KDF struct {
|
|
|
|
Hashfunction string `json:"hash"` //"SHA1" currently only sha1 is supported
|
|
|
|
Keylen int `json:"keylen"`
|
|
|
|
Iterations int `json:"iterations"`
|
|
|
|
Salt []byte `json:"salt"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// this is stored in disk in encrypted form
|
|
|
|
type Wallet_Memory struct {
|
|
|
|
Version semver.Version `json:"version"` // database version
|
|
|
|
Secret []byte `json:"secret"` // actual unlocker to the DB, depends on password from user, stored encrypted
|
|
|
|
// secret key used to encrypt all DB data ( both keys and values )
|
|
|
|
// this is always in encrypted form
|
|
|
|
|
|
|
|
KDF KDF `json:"kdf"`
|
|
|
|
|
|
|
|
account *Account //`json:"-"` // not serialized, we store an encrypted version // keys, seed language etc settings
|
|
|
|
Account_Encrypted []byte `json:"account_encrypted"`
|
|
|
|
|
|
|
|
pbkdf2_password []byte // used to encrypt metadata on updates
|
|
|
|
master_password []byte // single password which never changes
|
|
|
|
|
|
|
|
Daemon_Endpoint string `json:"-"` // endpoint used to communicate with daemon
|
|
|
|
Daemon_Height uint64 `json:"-"` // used to track daemon height ony if wallet in online
|
|
|
|
Daemon_TopoHeight int64 `json:"-"` // used to track daemon topo height ony if wallet in online
|
|
|
|
Merkle_Balance_TreeHash string `json:"-"` // current balance tree state
|
|
|
|
|
|
|
|
wallet_online_mode bool // set whether the mode is online or offline
|
|
|
|
// an offline wallet can be converted to online mode, calling.
|
|
|
|
// SetOffline() and vice versa using SetOnline
|
|
|
|
// used to create transaction with this fee rate,
|
|
|
|
//if this is lower than network, then created transaction will be rejected by network
|
|
|
|
dynamic_fees_per_kb uint64
|
2021-02-22 17:48:14 +00:00
|
|
|
Quit chan bool `json:"-"` // channel to quit any processing go routines
|
2020-12-27 13:44:23 +00:00
|
|
|
|
2021-02-22 17:48:14 +00:00
|
|
|
db_memory []byte // all data is stored here
|
|
|
|
wallet_disk *Wallet_Disk // a loopback pointer for some operations
|
2020-12-27 13:44:23 +00:00
|
|
|
|
|
|
|
id string // first 8 bytes of wallet address , to put into logs to identify different wallets in case many are active
|
|
|
|
|
|
|
|
Error error `json:"-"`
|
|
|
|
|
|
|
|
transfer_mutex sync.Mutex // to avoid races within the transfer
|
|
|
|
//sync.Mutex // used to syncronise access
|
|
|
|
sync.RWMutex
|
|
|
|
}
|
|
|
|
|
|
|
|
// when smart contracts are implemented, each will have it's own universe to track and maintain transactions
|
|
|
|
|
|
|
|
// this file implements the encrypted data store at rest
|
|
|
|
func Create_Encrypted_Wallet_Memory(password string, seed *crypto.BNRed) (w *Wallet_Memory, err error) {
|
|
|
|
rlog.Debugf("Creating Wallet from recovery seed")
|
|
|
|
w = &Wallet_Memory{}
|
|
|
|
w.Version = config.Version
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// generate account keys
|
|
|
|
w.account, err = Generate_Account_From_Seed(seed)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// generate a 64 byte key to be used as master Key
|
|
|
|
w.master_password = make([]byte, 32, 32)
|
|
|
|
_, err = rand.Read(w.master_password)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
err = w.Set_Encrypted_Wallet_Password(password) // lock the db with the password
|
|
|
|
|
2021-02-22 17:48:14 +00:00
|
|
|
w.Quit = make(chan bool)
|
2020-12-27 13:44:23 +00:00
|
|
|
|
|
|
|
w.id = string((w.account.GetAddress().String())[:8]) // set unique id for logs
|
|
|
|
w.account.Balance_Result.Registration = -1
|
|
|
|
|
|
|
|
rlog.Debugf("Successfully created wallet %s", w.id)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// create an encrypted wallet using electrum recovery words
|
|
|
|
func Create_Encrypted_Wallet_From_Recovery_Words_Memory(password string, electrum_seed string) (w *Wallet_Memory, err error) {
|
|
|
|
rlog.Debugf("Creating Wallet from recovery words")
|
|
|
|
|
|
|
|
language, seed, err := mnemonics.Words_To_Key(electrum_seed)
|
|
|
|
if err != nil {
|
|
|
|
rlog.Errorf("err parsing recovery words %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w, err = Create_Encrypted_Wallet_Memory(password, crypto.GetBNRed(seed))
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
rlog.Errorf("err creating wallet %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.account.SeedLanguage = language
|
|
|
|
rlog.Infof("Successfully created wallet %s", w.id)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// create an encrypted wallet using using random data
|
|
|
|
func Create_Encrypted_Wallet_Random_Memory(password string) (w *Wallet_Memory, err error) {
|
|
|
|
rlog.Infof("Creating Wallet Randomly")
|
|
|
|
w, err = Create_Encrypted_Wallet_Memory(password, crypto.RandomScalarBNRed())
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
rlog.Errorf("err %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// TODO setup seed language, default is already english
|
|
|
|
rlog.Infof("Successfully created wallet %s", w.id)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// wallet must already be open
|
|
|
|
func (w *Wallet_Memory) Set_Encrypted_Wallet_Password(password string) (err error) {
|
|
|
|
|
|
|
|
if w == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.Lock()
|
|
|
|
|
|
|
|
// set up KDF structure
|
|
|
|
w.KDF.Salt = make([]byte, 32, 32)
|
|
|
|
_, err = rand.Read(w.KDF.Salt)
|
|
|
|
if err != nil {
|
|
|
|
w.Unlock()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.KDF.Keylen = 32
|
|
|
|
w.KDF.Iterations = 262144
|
|
|
|
w.KDF.Hashfunction = "SHA1"
|
|
|
|
|
|
|
|
if runtime.GOOS == "js" {
|
|
|
|
w.KDF.Iterations = 32768
|
|
|
|
}
|
|
|
|
|
|
|
|
// lets generate the bcrypted password
|
|
|
|
|
|
|
|
w.pbkdf2_password = Generate_Key(w.KDF, password)
|
|
|
|
|
|
|
|
w.Unlock()
|
|
|
|
w.Save_Wallet() // save wallet data
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func Open_Encrypted_Wallet_Memory(password string, filedata []byte) (w *Wallet_Memory, err error) {
|
|
|
|
w = &Wallet_Memory{}
|
|
|
|
|
|
|
|
//fmt.Printf("v %+v\n",string(v)) // DO NOT dump account keys
|
|
|
|
|
|
|
|
// deserialize json data
|
|
|
|
err = json.Unmarshal(filedata, &w)
|
|
|
|
if err != nil {
|
|
|
|
rlog.Errorf("err parsing metabucket %s", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-02-22 17:48:14 +00:00
|
|
|
w.Quit = make(chan bool)
|
2020-12-27 13:44:23 +00:00
|
|
|
// todo make any routines necessary, such as sync etc
|
|
|
|
|
|
|
|
// try to deseal password and store it
|
|
|
|
w.pbkdf2_password = Generate_Key(w.KDF, password)
|
|
|
|
|
|
|
|
// try to decrypt the master password with the pbkdf2
|
|
|
|
w.master_password, err = DecryptWithKey(w.pbkdf2_password, w.Secret) // decrypt the master key
|
|
|
|
if err != nil {
|
|
|
|
//rlog.Errorf("err opening secret err: %s ", err)
|
|
|
|
err = fmt.Errorf("Invalid Password")
|
|
|
|
w = nil
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// password has been found, open the account
|
|
|
|
|
|
|
|
account_bytes, err := w.Decrypt(w.Account_Encrypted)
|
|
|
|
if err != nil {
|
|
|
|
rlog.Errorf("err opening account err: %s ", err)
|
|
|
|
err = fmt.Errorf("Invalid Password")
|
|
|
|
w = nil
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.account = &Account{} // allocate a new instance
|
|
|
|
err = json.Unmarshal(account_bytes, w.account)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.account.Balance_Result.Registration = -1
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// check whether the already opened wallet can use this password
|
|
|
|
func (w *Wallet_Memory) Check_Password(password string) bool {
|
|
|
|
w.Lock()
|
|
|
|
defer w.Unlock()
|
|
|
|
if w == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
pbkdf2_password := Generate_Key(w.KDF, password)
|
|
|
|
|
|
|
|
// TODO we can compare pbkdf2_password & w.pbkdf2_password, if they are equal password is vaid
|
|
|
|
|
|
|
|
// try to decrypt the master password with the pbkdf2
|
|
|
|
_, err := DecryptWithKey(pbkdf2_password, w.Secret) // decrypt the master key
|
|
|
|
|
|
|
|
if err == nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
rlog.Warnf("%s Invalid Password", w.id)
|
|
|
|
return false
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// save updated copy of wallet
|
|
|
|
func (w *Wallet_Memory) Save_Wallet() (err error) {
|
|
|
|
w.Lock()
|
|
|
|
defer w.Unlock()
|
|
|
|
if w == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// encrypted the master password with the pbkdf2
|
|
|
|
w.Secret, err = EncryptWithKey(w.pbkdf2_password[:], w.master_password) // encrypt the master key
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// encrypt the account
|
|
|
|
|
|
|
|
account_serialized, err := json.Marshal(w.account)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// fmt.Printf("account serialized %s\n", string(account_serialized))
|
|
|
|
// fmt.Printf("account serialized full %+v %s\n", w.account , w.account.Keys.Secret.Text(16))
|
|
|
|
|
|
|
|
w.Account_Encrypted, err = w.Encrypt(account_serialized)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// json marshal wallet data struct, serialize it, encrypt it and store it
|
|
|
|
serialized, err := json.Marshal(&w)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
//fmt.Printf("Serialized %+v\n",serialized)
|
|
|
|
|
|
|
|
w.db_memory = serialized
|
|
|
|
rlog.Infof("Saving wallet %s", w.id)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// get encrypted wallet
|
|
|
|
func (w *Wallet_Memory) Get_Encrypted_Wallet() []byte {
|
|
|
|
if err := w.Save_Wallet(); err == nil {
|
|
|
|
return w.db_memory
|
|
|
|
}
|
|
|
|
return []byte{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// close the wallet
|
|
|
|
// note that w is still valid and can be used to obtaine encrypted copy of data
|
|
|
|
func (w *Wallet_Memory) Close_Encrypted_Wallet() {
|
|
|
|
time.Sleep(time.Second) // give goroutines some time to quit
|
|
|
|
rlog.Infof("Saving and Closing Wallet %s\n", w.id)
|
|
|
|
w.Save_Wallet()
|
|
|
|
}
|
|
|
|
|
|
|
|
// generate key from password
|
|
|
|
func Generate_Key(k KDF, password string) (key []byte) {
|
|
|
|
switch k.Hashfunction {
|
|
|
|
case "SHA1":
|
|
|
|
return pbkdf2.Key([]byte(password), k.Salt, k.Iterations, k.Keylen, sha1.New)
|
|
|
|
|
|
|
|
default:
|
|
|
|
return pbkdf2.Key([]byte(password), k.Salt, k.Iterations, k.Keylen, sha1.New)
|
|
|
|
}
|
|
|
|
}
|