From 9eae969230bb8ff4e5a59a7e6f30304ec55ab338 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Tue, 3 Sep 2013 00:10:32 -0400 Subject: [PATCH] Implement new wallet and chained address creation. --- cmd.go | 131 +++++++- cmdmgr.go | 27 +- config.go | 13 +- sockets.go | 38 +++ wallet/wallet.go | 712 +++++++++++++++++++++++++++++++++--------- wallet/wallet_test.go | 83 ++++- 6 files changed, 823 insertions(+), 181 deletions(-) diff --git a/cmd.go b/cmd.go index 4c50511..9442163 100644 --- a/cmd.go +++ b/cmd.go @@ -18,16 +18,24 @@ package main import ( "fmt" + "github.com/conformal/btcwallet/tx" "github.com/conformal/btcwallet/wallet" "github.com/conformal/seelog" "os" + "path/filepath" + "sync" "time" ) var ( log seelog.LoggerInterface = seelog.Default cfg *config - wallets = make(map[string]*wallet.Wallet) + wallets = struct { + sync.RWMutex + m map[string]*BtcWallet + }{ + m: make(map[string]*BtcWallet), + } ) func main() { @@ -38,18 +46,24 @@ func main() { } cfg = tcfg - // Open wallet - file, err := os.Open(cfg.WalletFile) - if err != nil { - log.Error("Error opening wallet:", err) - } - w := new(wallet.Wallet) - if _, err = w.ReadFrom(file); err != nil { - log.Error(err) - } + /* + // Open wallet + file, err := os.Open(cfg.WalletFile) + if err != nil { + log.Error("Error opening wallet:", err) + } + w := new(wallet.Wallet) + if _, err = w.ReadFrom(file); err != nil { + log.Error(err) + } + */ - // Associate this wallet with default account. - wallets[""] = w + // Open wallet + btcw, err := OpenOrCreateWallet(cfg, "") + if err != nil { + panic(err) + } + _ = btcw // Start HTTP server to listen and send messages to frontend and btcd // backend. Try reconnection if connection failed. @@ -64,3 +78,96 @@ func main() { } } } + +type BtcWallet struct { + *wallet.Wallet + tx.UtxoStore + tx.TxStore +} + +func OpenOrCreateWallet(cfg *config, account string) (*BtcWallet, error) { + // Open wallet file specified by account. + var wname string + if account == "" { + wname = "btcwallet" + } else { + wname = fmt.Sprintf("btcwallet-%s", account) + } + + wdir := filepath.Join(cfg.DataDir, wname) + fi, err := os.Stat(wdir) + if err != nil { + if os.IsNotExist(err) { + // Attempt data directory creation + if err = os.MkdirAll(wdir, 0700); err != nil { + return nil, err + } + } else { + return nil, err + } + } else { + if !fi.IsDir() { + return nil, fmt.Errorf("Data directory '%s' is not a directory.", cfg.DataDir) + } + } + + wfilepath := filepath.Join(wdir, "wallet.bin") + txfilepath := filepath.Join(wdir, "tx.bin") + utxofilepath := filepath.Join(wdir, "utxo.bin") + var wfile, txfile, utxofile *os.File + if wfile, err = os.Open(wfilepath); err != nil { + if os.IsNotExist(err) { + if wfile, err = os.Create(wfilepath); err != nil { + return nil, err + } + } else { + return nil, err + } + } + if txfile, err = os.Open(txfilepath); err != nil { + if os.IsNotExist(err) { + if txfile, err = os.Create(txfilepath); err != nil { + return nil, err + } + } else { + return nil, err + } + } + if utxofile, err = os.Open(utxofilepath); err != nil { + if os.IsNotExist(err) { + if utxofile, err = os.Create(utxofilepath); err != nil { + return nil, err + } + } else { + return nil, err + } + } + + wlt := new(wallet.Wallet) + if _, err = wlt.ReadFrom(wfile); err != nil { + return nil, err + } + + var txs tx.TxStore + if _, err = txs.ReadFrom(txfile); err != nil { + return nil, err + } + + var utxos tx.UtxoStore + if _, err = utxos.ReadFrom(utxofile); err != nil { + + } + + w := &BtcWallet{ + Wallet: wlt, + UtxoStore: utxos, + TxStore: txs, + } + + // Associate this wallet with default account. + wallets.Lock() + wallets.m[account] = w + wallets.Unlock() + + return w, nil +} diff --git a/cmdmgr.go b/cmdmgr.go index 3a3be65..9162f3f 100644 --- a/cmdmgr.go +++ b/cmdmgr.go @@ -219,8 +219,11 @@ func GetAddressesByAccount(reply chan []byte, msg []byte) { params := v["params"].([]interface{}) var result interface{} - if w := wallets[params[0].(string)]; w != nil { - result = w.GetActiveAddresses() + wallets.RLock() + w := wallets.m[params[0].(string)] + wallets.RUnlock() + if w != nil { + result = w.Wallet.GetActiveAddresses() } else { result = []interface{}{} } @@ -235,7 +238,10 @@ func GetNewAddress(reply chan []byte, msg []byte) { json.Unmarshal(msg, &v) params := v["params"].([]interface{}) if len(params) == 0 || params[0].(string) == "" { - if w := wallets[""]; w != nil { + wallets.RLock() + w := wallets.m[""] + wallets.RUnlock() + if w != nil { addr := w.NextUnusedAddress() ReplySuccess(reply, v["id"], addr) } @@ -257,7 +263,10 @@ func WalletIsLocked(reply chan []byte, msg []byte) { return } } - if w := wallets[account]; w != nil { + wallets.RLock() + w := wallets.m[account] + wallets.RUnlock() + if w != nil { result := w.IsLocked() ReplySuccess(reply, v["id"], result) } else { @@ -272,7 +281,10 @@ func WalletIsLocked(reply chan []byte, msg []byte) { func WalletLock(reply chan []byte, msg []byte) { var v map[string]interface{} json.Unmarshal(msg, &v) - if w := wallets[""]; w != nil { + wallets.RLock() + w := wallets.m[""] + wallets.RUnlock() + if w != nil { if err := w.Lock(); err != nil { ReplyError(reply, v["id"], &WalletWrongEncState) } else { @@ -302,7 +314,10 @@ func WalletPassphrase(reply chan []byte, msg []byte) { return } - if w := wallets[""]; w != nil { + wallets.RLock() + w := wallets.m[""] + wallets.RUnlock() + if w != nil { if err := w.Unlock([]byte(passphrase)); err != nil { ReplyError(reply, v["id"], &WalletPassphraseIncorrect) return diff --git a/config.go b/config.go index f8c3405..e780366 100644 --- a/config.go +++ b/config.go @@ -17,7 +17,6 @@ package main import ( - "errors" "fmt" "github.com/conformal/go-flags" "os" @@ -34,6 +33,7 @@ const ( var ( defaultConfigFile = filepath.Join(btcwalletHomeDir(), defaultConfigFilename) + defaultDataDir = btcwalletHomeDir() ) type config struct { @@ -42,7 +42,7 @@ type config struct { DebugLevel string `short:"d" long:"debuglevel" description:"Logging level {trace, debug, info, warn, error, critical}"` ConfigFile string `short:"C" long:"configfile" description:"Path to configuration file"` SvrPort int `short:"p" long:"serverport" description:"Port to serve frontend websocket connections on"` - WalletFile string `short:"f" long:"walletfile" description:"Path to wallet file"` + DataDir string `short:"D" long:"datadir" description:"Directory to store wallets and transactions"` } // btcwalletHomeDir returns an OS appropriate home directory for btcwallet. @@ -92,6 +92,7 @@ func loadConfig() (*config, []string, error) { ConfigFile: defaultConfigFile, BtcdPort: defaultBtcdPort, SvrPort: defaultServerPort, + DataDir: defaultDataDir, } // A config file in the current directory takes precedence. @@ -141,9 +142,11 @@ func loadConfig() (*config, []string, error) { } // wallet file must be valid - if !fileExists(cfg.WalletFile) { - return &cfg, nil, errors.New("Wallet file does not exist.") - } + /* + if !fileExists(cfg.WalletFile) { + return &cfg, nil, errors.New("Wallet file does not exist.") + } + */ return &cfg, remainingArgs, nil } diff --git a/sockets.go b/sockets.go index 2072445..2444640 100644 --- a/sockets.go +++ b/sockets.go @@ -23,6 +23,7 @@ import ( "fmt" "github.com/conformal/btcjson" "github.com/conformal/btcwire" + "github.com/davecgh/go-spew/spew" "net/http" "sync" ) @@ -183,6 +184,8 @@ func BtcdHandler(ws *websocket.Conn) { // TODO(jrick): hook this up with addresses in wallet. // reqTxsForAddress("addr") + reqUtxoForAddress("1PZ67BehXWbzoqkovph4Cyfiz9LiFfTUot") + for { select { case rply, ok := <-replies: @@ -290,6 +293,16 @@ func ProcessBtcdNotificationReply(b []byte) { case "btcd:blockdisconnected": // TODO(jrick): rollback txs and utxos from removed block. + case "btcd:recvtx": + log.Info("got recvtx (ignoring)") + + case "btcd:sendtx": + log.Info("got sendtx (ignoring)") + + case "btcd:utxo": + result := m["result"].(map[string]interface{}) + spew.Dump(result) + default: frontendNotificationMaster <- b } @@ -381,3 +394,28 @@ func reqTxsForAddress(addr string) { btcdMsgs <- msg } + +func reqUtxoForAddress(addr string) { + seq.Lock() + n := seq.n + seq.n++ + seq.Unlock() + + m := &btcjson.Message{ + Jsonrpc: "", + Id: fmt.Sprintf("btcwallet(%d)", n), + Method: "requestutxos", + Params: []interface{}{ + addr, + }, + } + msg, _ := json.Marshal(m) + + replyHandlers.Lock() + replyHandlers.m[n] = func(result interface{}) bool { + return true + } + replyHandlers.Unlock() + + btcdMsgs <- msg +} diff --git a/wallet/wallet.go b/wallet/wallet.go index 7b80c15..e9606f6 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -21,6 +21,7 @@ import ( "code.google.com/p/go.crypto/ripemd160" "crypto/aes" "crypto/cipher" + "crypto/rand" "crypto/sha256" "crypto/sha512" "encoding/binary" @@ -29,10 +30,17 @@ import ( "github.com/conformal/btcec" "github.com/conformal/btcutil" "github.com/conformal/btcwire" + "github.com/davecgh/go-spew/spew" + "hash" "io" + "math" + "math/big" "sync" + "time" ) +var _ = spew.Dump + const ( // Length in bytes of KDF output. kdfOutputBytes = 32 @@ -42,6 +50,11 @@ const ( maxCommentLen = (1 << 16) - 1 ) +const ( + defaultKdfComputeTime = 0.25 + defaultKdfMaxMem = 32 * 1024 * 1024 +) + // Possible errors when dealing with wallets. var ( ChecksumErr = errors.New("Checksum mismatch") @@ -49,6 +62,14 @@ var ( WalletDoesNotExist = errors.New("Non-existant wallet") ) +var ( + // '\xbaWALLET\x00' + fileID = [8]byte{0xba, 0x57, 0x41, 0x4c, 0x4c, 0x45, 0x54, 0x00} + + mainnetMagicBytes = [4]byte{0xf9, 0xbe, 0xb4, 0xd9} + testnetMagicBytes = [4]byte{0x0b, 0x11, 0x09, 0x07} +) + type entryHeader byte const ( @@ -83,6 +104,38 @@ func binaryWrite(w io.Writer, order binary.ByteOrder, data interface{}) (n int64 return int64(written), err } +// Calculate the hash of hasher over buf. +func calcHash(buf []byte, hasher hash.Hash) []byte { + hasher.Write(buf) + return hasher.Sum(nil) +} + +// calculate hash160 which is ripemd160(sha256(data)) +func calcHash160(buf []byte) []byte { + return calcHash(calcHash(buf, sha256.New()), ripemd160.New()) +} + +// calculate hash256 which is sha256(sha256(data)) +func calcHash256(buf []byte) []byte { + return calcHash(calcHash(buf, sha256.New()), sha256.New()) +} + +// First byte in uncompressed pubKey field. +const pubkeyUncompressed = 0x4 + +// pubkeyFromPrivkey creates a 65-byte encoded pubkey based on a +// 32-byte privkey. +func pubkeyFromPrivkey(privkey []byte) (pubkey []byte) { + x, y := btcec.S256().ScalarBaseMult(privkey) + + pubkey = make([]byte, 65) + pubkey[0] = pubkeyUncompressed + copy(pubkey[1:33], x.Bytes()) + copy(pubkey[33:], y.Bytes()) + + return pubkey +} + func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte { saltedpass := append(passphrase, salt...) lutbl := make([]byte, memReqts) @@ -125,14 +178,65 @@ func keyOneIter(passphrase, salt []byte, memReqts uint64) []byte { // based on the ROMix algorithm described in Colin Percival's paper // "Stronger Key Derivation via Sequential Memory-Hard Functions" // (http://www.tarsnap.com/scrypt/scrypt.pdf). -func Key(passphrase, salt []byte, memReqts uint64, nIters uint32) []byte { +func Key(passphrase []byte, params *kdfParameters) []byte { masterKey := passphrase - for i := uint32(0); i < nIters; i++ { - masterKey = keyOneIter(masterKey, salt, memReqts) + for i := uint32(0); i < params.nIter; i++ { + masterKey = keyOneIter(masterKey, params.salt[:], params.mem) } return masterKey } +// leftPad returns a new slice of length size. The contents of input are right +// aligned in the new slice. +func leftPad(input []byte, size int) (out []byte) { + n := len(input) + if n > size { + n = size + } + out = make([]byte, size) + copy(out[len(out)-n:], input) + return +} + +// ChainedPrivKey deterministically generates new private key using a +// previous address and chaincode. privkey and chaincode must be 32 +// bytes long, and pubkey may either be 65 bytes or nil (in which case it +// is generated by the privkey). +func ChainedPrivKey(privkey, pubkey, chaincode []byte) ([]byte, error) { + if len(privkey) != 32 { + return nil, fmt.Errorf("Invalid privkey length %d (must be 32)", + len(privkey)) + } + if len(chaincode) != 32 { + return nil, fmt.Errorf("Invalid chaincode length %d (must be 32)", + len(chaincode)) + } + if pubkey == nil { + pubkey = pubkeyFromPrivkey(privkey) + } else if len(pubkey) != 65 { + return nil, fmt.Errorf("Invalid pubkey length %d.", len(pubkey)) + } + + // This is a perfect example of YOLO crypto. Armory claims this XORing + // with the SHA256 hash of the pubkey is done to add extra entropy (why + // you'd want to add entropy to a deterministic function, I don't know), + // even though the pubkey is generated directly from the privkey. In + // terms of security or privacy, this is a complete waste of CPU cycles, + // but we do the same because we want to keep compatibility with + // Armory's chained address generation. + xorbytes := make([]byte, 32) + chainMod := calcHash256(pubkey) + for i, _ := range xorbytes { + xorbytes[i] = chainMod[i] ^ chaincode[i] + } + chainXor := new(big.Int).SetBytes(xorbytes) + privint := new(big.Int).SetBytes(privkey) + + t := new(big.Int).Mul(chainXor, privint) + b := t.Mod(t, btcec.S256().N).Bytes() + return leftPad(b, 32), nil +} + type varEntries []io.WriterTo func (v *varEntries) WriteTo(w io.Writer) (n int64, err error) { @@ -214,98 +318,140 @@ func (v *varEntries) ReadFrom(r io.Reader) (n int64, err error) { // from and write to any type of byte streams, including files. // TODO(jrick) remove as many more magic numbers as possible. type Wallet struct { - fileID [8]byte - version uint32 - netMagicBytes [4]byte - walletFlags [8]byte - uniqID [6]byte - createDate [8]byte - name [32]byte - description [256]byte - highestUsed int64 - kdfParams kdfParameters - encryptionParams [256]byte - keyGenerator btcAddress - appendedEntries varEntries + version uint32 + net btcwire.BitcoinNet + flags walletFlags + uniqID [6]byte + createDate int64 + name [32]byte + desc [256]byte + highestUsed int64 + kdfParams kdfParameters + keyGenerator btcAddress + addrMap map[[ripemd160.Size]byte]*btcAddress + addrCommentMap map[[ripemd160.Size]byte]*[]byte + txCommentMap map[[sha256.Size]byte]*[]byte // These are not serialized key struct { sync.Mutex secret []byte } - addrMap map[[ripemd160.Size]byte]*btcAddress - addrCommentMap map[[ripemd160.Size]byte]*[]byte - chainIdxMap map[int64]*[ripemd160.Size]byte - txCommentMap map[[sha256.Size]byte]*[]byte - lastChainIdx int64 + chainIdxMap map[int64]*[ripemd160.Size]byte + lastChainIdx int64 } -// WriteTo serializes a Wallet and writes it to a io.Writer, -// returning the number of bytes written and any errors encountered. -func (wallet *Wallet) WriteTo(w io.Writer) (n int64, err error) { - // Iterate through each entry needing to be written. If data - // implements io.WriterTo, use its WriteTo func. Otherwise, - // data is a pointer to a fixed size value. - datas := []interface{}{ - &wallet.fileID, - &wallet.version, - &wallet.netMagicBytes, - &wallet.walletFlags, - &wallet.uniqID, - &wallet.createDate, - &wallet.name, - &wallet.description, - &wallet.highestUsed, - &wallet.kdfParams, - &wallet.encryptionParams, - &wallet.keyGenerator, - make([]byte, 1024), - &wallet.appendedEntries, +// NewWallet() creates and initializes a new Wallet. name's and +// desc's binary representation must not exceed 32 and 256 bytes, +// respectively. All address private keys are encrypted with passphrase. +// The wallet is returned unlocked. +func NewWallet(name, desc string, passphrase []byte) (*Wallet, error) { + if binary.Size(name) > 32 { + return nil, errors.New("name exceeds 32 byte maximum size") } - var read int64 - for _, data := range datas { - if s, ok := data.(io.WriterTo); ok { - read, err = s.WriteTo(w) - } else { - read, err = binaryWrite(w, binary.LittleEndian, data) - } - n += read - if err != nil { - return n, err - } + if binary.Size(desc) > 256 { + return nil, errors.New("desc exceeds 256 byte maximum size") } - return n, nil + kdfp := computeKdfParameters(defaultKdfComputeTime, defaultKdfMaxMem) + + rootkey, chaincode := make([]byte, 32), make([]byte, 32) + rand.Read(rootkey) + rand.Read(chaincode) + root, err := newRootBtcAddress(rootkey, nil, chaincode) + if err != nil { + return nil, err + } + aeskey := Key([]byte(passphrase), kdfp) + if err := root.encrypt(aeskey); err != nil { + return nil, err + } + + // Number of pregenerated addresses. + const pregenerated = 100 + + // TODO(jrick): not sure we will need uniqID, but would be good for + // compat with armory. + w := &Wallet{ + version: 0, // TODO(jrick): implement versioning + net: btcwire.MainNet, + flags: walletFlags{ + useEncryption: true, + watchingOnly: false, + }, + createDate: time.Now().Unix(), + //highestUsed: + kdfParams: *kdfp, + keyGenerator: *root, + addrMap: make(map[[ripemd160.Size]byte]*btcAddress), + addrCommentMap: make(map[[ripemd160.Size]byte]*[]byte), + txCommentMap: make(map[[sha256.Size]byte]*[]byte), + chainIdxMap: make(map[int64]*[ripemd160.Size]byte), + lastChainIdx: pregenerated - 1, + } + + // Add root address to maps. + w.addrMap[w.keyGenerator.pubKeyHash] = &w.keyGenerator + w.chainIdxMap[w.keyGenerator.chainIndex] = &w.keyGenerator.pubKeyHash + + // Pre-generate 100 encrypted addresses and add to maps. + addr := &w.keyGenerator + cc := addr.chaincode[:] + for i := 0; i < pregenerated; i++ { + privkey, err := ChainedPrivKey(addr.privKeyCT, addr.pubKey[:], cc) + if err != nil { + return nil, err + } + newaddr, err := newBtcAddress(privkey, nil) + if err != nil { + return nil, err + } + if err = newaddr.encrypt(aeskey); err != nil { + return nil, err + } + w.addrMap[newaddr.pubKeyHash] = newaddr + newaddr.chainIndex = addr.chainIndex + 1 + w.chainIdxMap[newaddr.chainIndex] = &newaddr.pubKeyHash + copy(newaddr.chaincode[:], cc) // armory does this.. but why? + addr = newaddr + } + + copy(w.name[:], []byte(name)) + copy(w.desc[:], []byte(desc)) + return w, nil } // ReadFrom reads data from a io.Reader and saves it to a Wallet, // returning the number of bytes read and any errors encountered. -func (wallet *Wallet) ReadFrom(r io.Reader) (n int64, err error) { +func (w *Wallet) ReadFrom(r io.Reader) (n int64, err error) { var read int64 - wallet.addrMap = make(map[[ripemd160.Size]byte]*btcAddress) - wallet.addrCommentMap = make(map[[ripemd160.Size]byte]*[]byte) - wallet.chainIdxMap = make(map[int64]*[ripemd160.Size]byte) - wallet.txCommentMap = make(map[[sha256.Size]byte]*[]byte) + w.addrMap = make(map[[ripemd160.Size]byte]*btcAddress) + w.addrCommentMap = make(map[[ripemd160.Size]byte]*[]byte) + w.chainIdxMap = make(map[int64]*[ripemd160.Size]byte) + w.txCommentMap = make(map[[sha256.Size]byte]*[]byte) + + var id [8]byte + var appendedEntries varEntries // Iterate through each entry needing to be read. If data // implements io.ReaderFrom, use its ReadFrom func. Otherwise, // data is a pointer to a fixed sized value. datas := []interface{}{ - &wallet.fileID, - &wallet.version, - &wallet.netMagicBytes, - &wallet.walletFlags, - &wallet.uniqID, - &wallet.createDate, - &wallet.name, - &wallet.description, - &wallet.highestUsed, - &wallet.kdfParams, - &wallet.encryptionParams, - &wallet.keyGenerator, + &id, + &w.version, + &w.net, + &w.flags, + &w.uniqID, + &w.createDate, + &w.name, + &w.desc, + &w.highestUsed, + &w.kdfParams, + make([]byte, 256), + &w.keyGenerator, make([]byte, 1024), - &wallet.appendedEntries, + &appendedEntries, } for _, data := range datas { var err error @@ -320,27 +466,31 @@ func (wallet *Wallet) ReadFrom(r io.Reader) (n int64, err error) { } } + if id != fileID { + return n, errors.New("Unknown File ID.") + } + // Add root address to address map - wallet.addrMap[wallet.keyGenerator.pubKeyHash] = &wallet.keyGenerator - wallet.chainIdxMap[wallet.keyGenerator.chainIndex] = &wallet.keyGenerator.pubKeyHash + w.addrMap[w.keyGenerator.pubKeyHash] = &w.keyGenerator + w.chainIdxMap[w.keyGenerator.chainIndex] = &w.keyGenerator.pubKeyHash // Fill unserializied fields. - wts := ([]io.WriterTo)(wallet.appendedEntries) + wts := ([]io.WriterTo)(appendedEntries) for _, wt := range wts { switch wt.(type) { case *addrEntry: e := wt.(*addrEntry) - wallet.addrMap[e.pubKeyHash160] = &e.addr - wallet.chainIdxMap[e.addr.chainIndex] = &e.pubKeyHash160 - if wallet.lastChainIdx < e.addr.chainIndex { - wallet.lastChainIdx = e.addr.chainIndex + w.addrMap[e.pubKeyHash160] = &e.addr + w.chainIdxMap[e.addr.chainIndex] = &e.pubKeyHash160 + if w.lastChainIdx < e.addr.chainIndex { + w.lastChainIdx = e.addr.chainIndex } case *addrCommentEntry: e := wt.(*addrCommentEntry) - wallet.addrCommentMap[e.pubKeyHash160] = &e.comment + w.addrCommentMap[e.pubKeyHash160] = &e.comment case *txCommentEntry: e := wt.(*txCommentEntry) - wallet.txCommentMap[e.txHash] = &e.comment + w.txCommentMap[e.txHash] = &e.comment default: return n, errors.New("Unknown appended entry") } @@ -349,19 +499,82 @@ func (wallet *Wallet) ReadFrom(r io.Reader) (n int64, err error) { return n, nil } +// WriteTo serializes a Wallet and writes it to a io.Writer, +// returning the number of bytes written and any errors encountered. +func (w *Wallet) WriteTo(wtr io.Writer) (n int64, err error) { + wts := make([]io.WriterTo, len(w.addrMap)-1) + for hash, addr := range w.addrMap { + if addr.chainIndex != -1 { // ignore root address + e := &addrEntry{ + pubKeyHash160: hash, + addr: *addr, + } + wts[addr.chainIndex] = e + } + } + for hash, comment := range w.addrCommentMap { + e := &addrCommentEntry{ + pubKeyHash160: hash, + comment: *comment, + } + wts = append(wts, e) + } + for hash, comment := range w.txCommentMap { + e := &txCommentEntry{ + txHash: hash, + comment: *comment, + } + wts = append(wts, e) + } + appendedEntries := varEntries(wts) + + // Iterate through each entry needing to be written. If data + // implements io.WriterTo, use its WriteTo func. Otherwise, + // data is a pointer to a fixed size value. + datas := []interface{}{ + &fileID, + &w.version, + &w.net, + &w.flags, + &w.uniqID, + &w.createDate, + &w.name, + &w.desc, + &w.highestUsed, + &w.kdfParams, + make([]byte, 256), + &w.keyGenerator, + make([]byte, 1024), + &appendedEntries, + } + var written int64 + for _, data := range datas { + if s, ok := data.(io.WriterTo); ok { + written, err = s.WriteTo(wtr) + } else { + written, err = binaryWrite(wtr, binary.LittleEndian, data) + } + n += written + if err != nil { + return n, err + } + } + + return n, nil +} + // Unlock derives an AES key from passphrase and wallet's KDF // parameters and unlocks the root key of the wallet. -func (wallet *Wallet) Unlock(passphrase []byte) error { - key := Key(passphrase, wallet.kdfParams.salt[:], - wallet.kdfParams.mem, wallet.kdfParams.nIter) +func (w *Wallet) Unlock(passphrase []byte) error { + key := Key(passphrase, &w.kdfParams) // Attempt unlocking root address - if err := wallet.keyGenerator.unlock(key); err != nil { + if err := w.keyGenerator.unlock(key); err != nil { return err } else { - wallet.key.Lock() - wallet.key.secret = key - wallet.key.Unlock() + w.key.Lock() + w.key.secret = key + w.key.Unlock() return nil } } @@ -369,42 +582,52 @@ func (wallet *Wallet) Unlock(passphrase []byte) error { // Lock does a best effort to zero the keys. // Being go this might not succeed but try anway. // TODO(jrick) -func (wallet *Wallet) Lock() (err error) { - wallet.key.Lock() - if wallet.key.secret != nil { - for i, _ := range wallet.key.secret { - wallet.key.secret[i] = 0 +func (w *Wallet) Lock() (err error) { + // Remove clear text private keys from all entries. + for _, addr := range w.addrMap { + addr.privKeyCT = nil + } + + w.key.Lock() + if w.key.secret != nil { + for i, _ := range w.key.secret { + w.key.secret[i] = 0 } - wallet.key.secret = nil + w.key.secret = nil } else { err = fmt.Errorf("Wallet already locked") } - wallet.key.Unlock() - return err + w.key.Unlock() + + return nil } -func (wallet *Wallet) IsLocked() (locked bool) { - wallet.key.Lock() - locked = wallet.key.secret == nil - wallet.key.Unlock() +// IsLocked returns whether a wallet is unlocked (in which case the +// key is saved in memory), or locked. +func (w *Wallet) IsLocked() (locked bool) { + w.key.Lock() + locked = w.key.secret == nil + w.key.Unlock() return locked } // Returns wallet version as string and int. // TODO(jrick) -func (wallet *Wallet) Version() (string, int) { +func (w *Wallet) Version() (string, int) { return "", 0 } -// TODO(jrick) -func (wallet *Wallet) NextUnusedAddress() string { - _ = wallet.lastChainIdx - wallet.highestUsed++ - new160, err := wallet.addr160ForIdx(wallet.highestUsed) +// NextUnusedAddress attempts to get the next chained address. It +// currently relies on pre-generated addresses and will return an empty +// string if the address pool has run out. TODO(jrick) +func (w *Wallet) NextUnusedAddress() string { + _ = w.lastChainIdx + w.highestUsed++ + new160, err := w.addr160ForIdx(w.highestUsed) if err != nil { return "" } - addr := wallet.addrMap[*new160] + addr := w.addrMap[*new160] if addr != nil { return btcutil.Base58Encode(addr.pubKeyHash[:]) } else { @@ -412,39 +635,105 @@ func (wallet *Wallet) NextUnusedAddress() string { } } -func (wallet *Wallet) addr160ForIdx(idx int64) (*[ripemd160.Size]byte, error) { - if idx > wallet.lastChainIdx { +func (w *Wallet) addr160ForIdx(idx int64) (*[ripemd160.Size]byte, error) { + if idx > w.lastChainIdx { return nil, errors.New("Chain index out of range") } - return wallet.chainIdxMap[idx], nil + return w.chainIdxMap[idx], nil } -func (wallet *Wallet) GetActiveAddresses() []string { +// GetActiveAddresses returns all wallet addresses that have been +// requested to be generated. These do not include pre-generated +// addresses. +func (w *Wallet) GetActiveAddresses() []string { addrs := []string{} - for i := int64(-1); i <= wallet.highestUsed; i++ { - addr160, err := wallet.addr160ForIdx(i) + for i := int64(-1); i <= w.highestUsed; i++ { + addr160, err := w.addr160ForIdx(i) if err != nil { return addrs } - addr := wallet.addrMap[*addr160] + addr := w.addrMap[*addr160] addrs = append(addrs, btcutil.Base58Encode(addr.pubKeyHash[:])) } return addrs } -/* -func OpenWallet(file string) (*Wallet, error) { - +type walletFlags struct { + useEncryption bool + watchingOnly bool +} + +func (wf *walletFlags) ReadFrom(r io.Reader) (n int64, err error) { + raw := make([]byte, 8) + n, err = binaryRead(r, binary.LittleEndian, raw) + wf.useEncryption = raw[0] != 0 + wf.watchingOnly = raw[1] != 0 + return n, err +} + +func (wf *walletFlags) WriteTo(w io.Writer) (n int64, err error) { + raw := make([]byte, 8) + if wf.useEncryption { + raw[0] = 1 + } + if wf.watchingOnly { + raw[1] = 1 + } + return binaryWrite(w, binary.LittleEndian, raw) +} + +type addrFlags struct { + hasPrivKey bool + hasPubKey bool + encrypted bool +} + +func (af *addrFlags) ReadFrom(r io.Reader) (n int64, err error) { + var read int64 + var b [8]byte + read, err = binaryRead(r, binary.LittleEndian, &b) + if err != nil { + return n + read, err + } + n += read + + if b[0]&(1<<0) != 0 { + af.hasPrivKey = true + } + if b[0]&(1<<1) != 0 { + af.hasPubKey = true + } + if b[0]&(1<<2) == 0 { + return n, errors.New("Address flag specifies unencrypted address.") + } + af.encrypted = true + + return n, nil +} + +func (af *addrFlags) WriteTo(w io.Writer) (n int64, err error) { + var b [8]byte + if af.hasPrivKey { + b[0] |= 1 << 0 + } + if af.hasPubKey { + b[0] |= 1 << 1 + } + if !af.encrypted { + // We only support encrypted privkeys. + return n, errors.New("Address must be encrypted.") + } + b[0] |= 1 << 2 + + return binaryWrite(w, binary.LittleEndian, b) } -*/ type btcAddress struct { pubKeyHash [ripemd160.Size]byte - version uint32 - flags uint64 + flags addrFlags chaincode [32]byte chainIndex int64 - chainDepth int64 + chainDepth int64 // currently unused (will use when extending a locked wallet) initVector [16]byte privKey [32]byte pubKey [65]byte @@ -452,7 +741,57 @@ type btcAddress struct { lastSeen uint64 firstBlock uint32 lastBlock uint32 - privKeyCT []byte // Points to clear text private key if unlocked. + privKeyCT []byte // non-nil if unlocked. +} + +// newBtcAddress initializes and returns a new address. privkey must +// be 32 bytes. iv must be 16 bytes, or nil (in which case it is +// randomly generated). +func newBtcAddress(privkey, iv []byte) (addr *btcAddress, err error) { + if len(privkey) != 32 { + return nil, errors.New("Private key is not 32 bytes.") + } + if iv == nil { + iv = make([]byte, 16) + rand.Read(iv) + } else if len(iv) != 16 { + return nil, errors.New("Init vector must be nil or 16 bytes large.") + } + + addr = &btcAddress{ + privKeyCT: privkey, + flags: addrFlags{ + hasPrivKey: true, + hasPubKey: true, + }, + firstSeen: math.MaxUint64, + firstBlock: math.MaxUint32, + } + copy(addr.initVector[:], iv) + pub := pubkeyFromPrivkey(privkey) + copy(addr.pubKey[:], pub) + copy(addr.pubKeyHash[:], calcHash160(pub)) + + return addr, nil +} + +// newRootBtcAddress generates a new address, also setting the +// chaincode and chain index to represent this address as a root +// address. +func newRootBtcAddress(privKey, iv, chaincode []byte) (addr *btcAddress, err error) { + if len(chaincode) != 32 { + return nil, errors.New("Chaincode is not 32 bytes.") + } + + addr, err = newBtcAddress(privKey, iv) + if err != nil { + return nil, err + } + + copy(addr.chaincode[:], chaincode) + addr.chainIndex = -1 + + return addr, err } // ReadFrom reads an encrypted address from an io.Reader. @@ -470,7 +809,7 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) { datas := []interface{}{ &addr.pubKeyHash, &chkPubKeyHash, - &addr.version, + make([]byte, 4), // version &addr.flags, &addr.chaincode, &chkChaincode, @@ -488,7 +827,12 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) { &addr.lastBlock, } for _, data := range datas { - if read, err = binaryRead(r, binary.LittleEndian, data); err != nil { + if rf, ok := data.(io.ReaderFrom); ok { + read, err = rf.ReadFrom(r) + } else { + read, err = binaryRead(r, binary.LittleEndian, data) + } + if err != nil { return n + read, err } n += read @@ -511,8 +855,6 @@ func (addr *btcAddress) ReadFrom(r io.Reader) (n int64, err error) { } } - // TODO(jrick) verify encryption - return n, nil } @@ -522,7 +864,7 @@ func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) { datas := []interface{}{ &addr.pubKeyHash, walletHash(addr.pubKeyHash[:]), - &addr.version, + make([]byte, 4), //version &addr.flags, &addr.chaincode, walletHash(addr.chaincode[:]), @@ -540,7 +882,11 @@ func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) { &addr.lastBlock, } for _, data := range datas { - written, err = binaryWrite(w, binary.LittleEndian, data) + if wt, ok := data.(io.WriterTo); ok { + written, err = wt.WriteTo(w) + } else { + written, err = binaryWrite(w, binary.LittleEndian, data) + } if err != nil { return n + written, err } @@ -549,25 +895,66 @@ func (addr *btcAddress) WriteTo(w io.Writer) (n int64, err error) { return n, nil } -func (addr *btcAddress) unlock(key []byte) error { - aesBlockDecrypter, err := aes.NewCipher([]byte(key)) +// encrypt attempts to encrypt an address's clear text private key, +// failing if the address is already encrypted or if the private key is +// not 32 bytes. If successful, the encryption flag is set. +func (a *btcAddress) encrypt(key []byte) error { + if a.flags.encrypted { + return errors.New("Address already encrypted.") + } + if len(a.privKeyCT) != 32 { + return errors.New("Invalid clear text private key.") + } + + aesBlockEncrypter, err := aes.NewCipher(key) if err != nil { return err } - aesDecrypter := cipher.NewCFBDecrypter(aesBlockDecrypter, addr.initVector[:]) - ct := make([]byte, 32) - aesDecrypter.XORKeyStream(ct, addr.privKey[:]) - addr.privKeyCT = ct + aesEncrypter := cipher.NewCFBEncrypter(aesBlockEncrypter, a.initVector[:]) - pubKey, err := btcec.ParsePubKey(addr.pubKey[:], btcec.S256()) + aesEncrypter.XORKeyStream(a.privKey[:], a.privKeyCT) + + a.flags.encrypted = true + return nil +} + +// lock removes the reference this address holds to its clear text +// private key. This function fails if the address is not encrypted. +func (a *btcAddress) lock() error { + if !a.flags.encrypted { + return errors.New("Unable to lock unencrypted address.") + } + + a.privKeyCT = nil + return nil +} + +// unlock decrypts and stores a pointer to this address's private key, +// failing if the address is not encrypted, or the provided key is +// incorrect. +func (a *btcAddress) unlock(key []byte) error { + if !a.flags.encrypted { + return errors.New("Unable to unlock unencrypted address.") + } + + aesBlockDecrypter, err := aes.NewCipher(key) + if err != nil { + return err + } + aesDecrypter := cipher.NewCFBDecrypter(aesBlockDecrypter, a.initVector[:]) + ct := make([]byte, 32) + aesDecrypter.XORKeyStream(ct, a.privKey[:]) + + pubKey, err := btcec.ParsePubKey(a.pubKey[:], btcec.S256()) if err != nil { return fmt.Errorf("ParsePubKey faild:", err) } - x, y := btcec.S256().ScalarBaseMult(addr.privKeyCT) + x, y := btcec.S256().ScalarBaseMult(ct) if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 { - return fmt.Errorf("decryption failed") + return errors.New("Decryption failed.") } + a.privKeyCT = ct return nil } @@ -576,16 +963,6 @@ func (addr *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error { return nil } -// TODO(jrick) -func (addr *btcAddress) verifyEncryptionKey() { -} - -// TODO(jrick) -func newRandomAddress(key []byte) *btcAddress { - addr := &btcAddress{} - return addr -} - func walletHash(b []byte) uint32 { sum := btcwire.DoubleSha256(b) return binary.LittleEndian.Uint32(sum) @@ -605,6 +982,42 @@ type kdfParameters struct { salt [32]byte } +// computeKdfParameters returns best guess parameters to the +// memory-hard key derivation function to make the computation last +// targetSec seconds, while using no more than maxMem bytes of memory. +func computeKdfParameters(targetSec float64, maxMem uint64) *kdfParameters { + params := &kdfParameters{} + rand.Read(params.salt[:]) + + testKey := []byte("This is an example key to test KDF iteration speed") + + memoryReqtBytes := uint64(1024) + approxSec := float64(0) + + for approxSec <= targetSec/4 && memoryReqtBytes < maxMem { + memoryReqtBytes *= 2 + before := time.Now() + _ = keyOneIter(testKey, params.salt[:], memoryReqtBytes) + approxSec = time.Since(before).Seconds() + } + + allItersSec := float64(0) + nIter := uint32(1) + for allItersSec < 0.02 { // This is a magic number straight from armory's source. + nIter *= 2 + before := time.Now() + for i := uint32(0); i < nIter; i++ { + _ = keyOneIter(testKey, params.salt[:], memoryReqtBytes) + } + allItersSec = time.Since(before).Seconds() + } + + params.mem = memoryReqtBytes + params.nIter = nIter + + return params +} + func (params *kdfParameters) WriteTo(w io.Writer) (n int64, err error) { var written int64 @@ -657,7 +1070,7 @@ func (params *kdfParameters) ReadFrom(r io.Reader) (n int64, err error) { return n, err } - // Write params + // Read params buf := bytes.NewBuffer(chkedBytes) datas = []interface{}{ ¶ms.mem, @@ -695,7 +1108,8 @@ func (e *addrEntry) WriteTo(w io.Writer) (n int64, err error) { // Write btcAddress written, err = e.addr.WriteTo(w) - return n + written, err + n += written + return n, err } func (e *addrEntry) ReadFrom(r io.Reader) (n int64, err error) { diff --git a/wallet/wallet_test.go b/wallet/wallet_test.go index 4f896c8..b8b862a 100644 --- a/wallet/wallet_test.go +++ b/wallet/wallet_test.go @@ -17,16 +17,33 @@ package wallet import ( - "bytes" - "encoding/binary" + "crypto/rand" "github.com/davecgh/go-spew/spew" "os" + "reflect" "testing" ) +var _ = spew.Dump + func TestBtcAddressSerializer(t *testing.T) { - var addr = btcAddress{ - pubKeyHash: [20]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19}, + kdfp := &kdfParameters{ + mem: 1024, + nIter: 5, + } + rand.Read(kdfp.salt[:]) + key := Key([]byte("banana"), kdfp) + privKey := make([]byte, 32) + rand.Read(privKey) + addr, err := newBtcAddress(privKey, nil) + if err != nil { + t.Error(err.Error()) + return + } + err = addr.encrypt(key) + if err != nil { + t.Error(err.Error()) + return } file, err := os.Create("btcaddress.bin") @@ -46,15 +63,63 @@ func TestBtcAddressSerializer(t *testing.T) { var readAddr btcAddress _, err = readAddr.ReadFrom(file) if err != nil { - spew.Dump(&readAddr) t.Error(err.Error()) return } - buf1, buf2 := new(bytes.Buffer), new(bytes.Buffer) - binary.Write(buf1, binary.LittleEndian, addr) - binary.Write(buf2, binary.LittleEndian, readAddr) - if !bytes.Equal(buf1.Bytes(), buf2.Bytes()) { + if err = readAddr.unlock(key); err != nil { + t.Error(err.Error()) + return + } + + if !reflect.DeepEqual(addr, &readAddr) { t.Error("Original and read btcAddress differ.") } } + +func TestWalletCreationSerialization(t *testing.T) { + w1, err := NewWallet("banana wallet", "A wallet for testing.", []byte("banana")) + if err != nil { + t.Error("Error creating new wallet: " + err.Error()) + } + + file, err := os.Create("newwallet.bin") + if err != nil { + t.Error(err.Error()) + return + } + defer file.Close() + + if _, err := w1.WriteTo(file); err != nil { + t.Error("Error writing new wallet: " + err.Error()) + return + } + + file.Seek(0, 0) + + w2 := new(Wallet) + _, err = w2.ReadFrom(file) + if err != nil { + t.Error("Error reading newly written wallet: " + err.Error()) + return + } + + w1.Lock() + w2.Lock() + + if err = w1.Unlock([]byte("banana")); err != nil { + t.Error("Decrypting original wallet failed: " + err.Error()) + return + } + + if err = w2.Unlock([]byte("banana")); err != nil { + t.Error("Decrypting newly read wallet failed: " + err.Error()) + return + } + + if !reflect.DeepEqual(w1, w2) { + t.Error("Created and read-in wallets do not match.") + spew.Dump(w1, w2) + return + } +}