diff --git a/uspv/eight333.go b/uspv/eight333.go index 41a83f69..7096bef3 100644 --- a/uspv/eight333.go +++ b/uspv/eight333.go @@ -8,7 +8,6 @@ import ( "net" "os" - "github.com/boltdb/bolt" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil/bloom" @@ -46,7 +45,7 @@ type SPVCon struct { mBlockQueue chan RootAndHeight } -func OpenSPV(remoteNode string, hfn string, +func OpenSPV(remoteNode string, hfn, tsfn string, inTs *TxStore, p *chaincfg.Params) (SPVCon, error) { // create new SPVCon var s SPVCon @@ -70,7 +69,7 @@ func OpenSPV(remoteNode string, hfn string, s.localVersion = VERSION // transaction store for this SPV connection - err = inTs.OpenDB("utxo.db") + err = inTs.OpenDB(tsfn) if err != nil { return s, err } @@ -130,26 +129,6 @@ func OpenSPV(remoteNode string, hfn string, return s, nil } -func (ts *TxStore) OpenDB(filename string) error { - var err error - ts.StateDB, err = bolt.Open(filename, 0644, nil) - if err != nil { - return err - } - // create buckets if they're not already there - return ts.StateDB.Update(func(tx *bolt.Tx) error { - _, err = tx.CreateBucketIfNotExists(BKTUtxos) - if err != nil { - return err - } - _, err = tx.CreateBucketIfNotExists(BKTOld) - if err != nil { - return err - } - return nil - }) -} - func (s *SPVCon) openHeaderFile(hfn string) error { _, err := os.Stat(hfn) if err != nil { diff --git a/uspv/keyfileio.go b/uspv/keyfileio.go new file mode 100644 index 00000000..0bebe4de --- /dev/null +++ b/uspv/keyfileio.go @@ -0,0 +1,191 @@ +package uspv + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcutil/hdkeychain" + "github.com/howeyc/gopass" + "golang.org/x/crypto/nacl/secretbox" + "golang.org/x/crypto/scrypt" +) + +// warning! look at those imports! crypto! hopefully this works! + +/* on-disk stored keys are 32bytes. This is good for ed25519 private keys, +for seeds for bip32, for individual secp256k1 priv keys, and so on. +32 bytes is enough for anyone. +If you want fewer bytes, put some zeroes at the end */ + +// LoadKeyFromFileInteractive opens the file 'filename' and presents a +// keyboard prompt for the passphrase to decrypt it. It returns the +// key if decryption works, or errors out. +func LoadKeyFromFileInteractive(filename string) (*[32]byte, error) { + a, err := os.Stat(filename) + if err != nil { + return new([32]byte), err + } + if a.Size() < 80 { // there can't be a password... + return LoadKeyFromFileArg(filename, nil) + } + fmt.Printf("passphrase: ") + pass := gopass.GetPasswd() + fmt.Printf("\n") + return LoadKeyFromFileArg(filename, pass) +} + +// LoadKeyFromFileArg opens the file and returns the key. If the key is +// unencrypted it will ignore the password argument. +func LoadKeyFromFileArg(filename string, pass []byte) (*[32]byte, error) { + priv32 := new([32]byte) + keyhex, err := ioutil.ReadFile(filename) + if err != nil { + return priv32, err + } + keyhex = []byte(strings.TrimSpace(string(keyhex))) + enckey, err := hex.DecodeString(string(keyhex)) + if err != nil { + return priv32, err + } + + if len(enckey) == 32 { // UNencrypted key, length 32 + fmt.Printf("WARNING!! Key file not encrypted!!\n") + fmt.Printf("Anyone who can read the key file can take everything!\n") + fmt.Printf("You should start over and use a good passphrase!\n") + copy(priv32[:], enckey[:]) + return priv32, nil + } + // enckey should be 72 bytes. 24 for scrypt salt/box nonce, + // 16 for box auth + if len(enckey) != 72 { + return priv32, fmt.Errorf("Key length error for %s ", filename) + } + // enckey is actually encrypted, get derived key from pass and salt + // first extract salt + salt := new([24]byte) // salt (also nonce for secretbox) + dk32 := new([32]byte) // derived key array + copy(salt[:], enckey[:24]) // first 24 bytes are scrypt salt/box nonce + + dk, err := scrypt.Key(pass, salt[:], 16384, 8, 1, 32) // derive key + if err != nil { + return priv32, err + } + copy(dk32[:], dk[:]) // copy into fixed size array + + // nonce for secretbox is the same as scrypt salt. Seems fine. Really. + priv, worked := secretbox.Open(nil, enckey[24:], salt, dk32) + if worked != true { + return priv32, fmt.Errorf("Decryption failed for %s ", filename) + } + copy(priv32[:], priv[:]) //copy decrypted private key into array + + priv = nil // this probably doesn't do anything but... eh why not + return priv32, nil +} + +// saves a 32 byte key to file, prompting for passphrase. +// if user enters empty passphrase (hits enter twice), will be saved +// in the clear. +func SaveKeyToFileInteractive(filename string, priv32 *[32]byte) error { + var match bool + var pass1, pass2 []byte + for match != true { + fmt.Printf("passphrase: ") + pass1 = gopass.GetPasswd() + fmt.Printf("repeat passphrase: ") + pass2 = gopass.GetPasswd() + if string(pass1) == string(pass2) { + match = true + } else { + fmt.Printf("user input error. Try again gl hf dd.\n") + } + } + fmt.Printf("\n") + return SaveKeyToFileArg(filename, priv32, pass1) +} + +// saves a 32 byte key to a file, encrypting with pass. +// if pass is nil or zero length, doesn't encrypt and just saves in hex. +func SaveKeyToFileArg(filename string, priv32 *[32]byte, pass []byte) error { + if len(pass) == 0 { // zero-length pass, save unencrypted + keyhex := fmt.Sprintf("%x\n", priv32[:]) + err := ioutil.WriteFile(filename, []byte(keyhex), 0600) + if err != nil { + return err + } + fmt.Printf("WARNING!! Key file not encrypted!!\n") + fmt.Printf("Anyone who can read the key file can take everything!\n") + fmt.Printf("You should start over and use a good passphrase!\n") + fmt.Printf("Saved unencrypted key at %s\n", filename) + return nil + } + + salt := new([24]byte) // salt for scrypt / nonce for secretbox + dk32 := new([32]byte) // derived key from scrypt + + //get 24 random bytes for scrypt salt (and secretbox nonce) + _, err := rand.Read(salt[:]) + if err != nil { + return err + } + // next use the pass and salt to make a 32-byte derived key + dk, err := scrypt.Key(pass, salt[:], 16384, 8, 1, 32) + if err != nil { + return err + } + copy(dk32[:], dk[:]) + + enckey := append(salt[:], secretbox.Seal(nil, priv32[:], salt, dk32)...) + // enckey = append(salt, enckey...) + keyhex := fmt.Sprintf("%x\n", enckey) + + err = ioutil.WriteFile(filename, []byte(keyhex), 0600) + if err != nil { + return err + } + fmt.Printf("Wrote encrypted key to %s\n", filename) + return nil +} + +// ReadKeyFileToECPriv returns an extendedkey from a file. +// If there's no file there, it'll make one. If there's a password needed, +// it'll prompt for one. One stop function. +func ReadKeyFileToECPriv(filename string) (*hdkeychain.ExtendedKey, error) { + key32 := new([32]byte) + _, err := os.Stat(filename) + if err != nil { + if os.IsNotExist(err) { + // no key found, generate and save one + fmt.Printf("No file %s, generating.\n", filename) + rn, err := hdkeychain.GenerateSeed(32) + if err != nil { + return nil, err + } + copy(key32[:], rn[:]) + err = SaveKeyToFileInteractive(filename, key32) + if err != nil { + return nil, err + } + } else { + // unknown error, crash + fmt.Printf("unknown\n") + return nil, err + } + } + + key, err := LoadKeyFromFileInteractive(filename) + if err != nil { + return nil, err + } + + rootpriv, err := hdkeychain.NewMaster(key[:], &chaincfg.TestNet3Params) + if err != nil { + return nil, err + } + return rootpriv, nil +} diff --git a/uspv/txstore.go b/uspv/txstore.go index 7c1abee3..e666ed6c 100644 --- a/uspv/txstore.go +++ b/uspv/txstore.go @@ -5,10 +5,13 @@ import ( "fmt" "log" + "github.com/btcsuite/btcd/chaincfg" + "github.com/boltdb/bolt" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil/bloom" + "github.com/btcsuite/btcutil/hdkeychain" ) type TxStore struct { @@ -17,7 +20,13 @@ type TxStore struct { Utxos []Utxo // stacks on stacks Sum int64 // racks on racks Adrs []MyAdr // endeavouring to acquire capital + LastIdx uint32 // should equal len(Adrs) StateDB *bolt.DB // place to write all this down + // this is redundant with the SPVCon param... ugly and should be taken out + param *chaincfg.Params // network parameters (testnet3, testnetL) + + // From here, comes everything. It's a secret to everybody. + rootPrivKey *hdkeychain.ExtendedKey } type Utxo struct { // cash money. diff --git a/uspv/utxodb.go b/uspv/utxodb.go index 32fcf06b..e27e9140 100644 --- a/uspv/utxodb.go +++ b/uspv/utxodb.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/binary" "fmt" + "log" "github.com/btcsuite/btcd/wire" @@ -16,6 +17,42 @@ var ( KEYState = []byte("LastUpdate") // last state of DB ) +func (ts *TxStore) OpenDB(filename string) error { + var err error + ts.StateDB, err = bolt.Open(filename, 0644, nil) + if err != nil { + return err + } + // create buckets if they're not already there + return ts.StateDB.Update(func(tx *bolt.Tx) error { + _, err = tx.CreateBucketIfNotExists(BKTUtxos) + if err != nil { + return err + } + _, err = tx.CreateBucketIfNotExists(BKTOld) + if err != nil { + return err + } + return nil + }) +} + +func (ts *TxStore) PopulateAdrs(lastKey uint32) error { + for k := uint32(0); k < lastKey; k++ { + + priv, err := ts.rootPrivKey.Child(k) + if err != nil { + log.Fatal(err) + } + myadr, err := priv.Address(ts.param) + if err != nil { + log.Fatal(err) + } + fmt.Printf("made adr %s\n", myadr.String()) + ts.AddAdr(myadr, k) + } + return nil +} func (u *Utxo) SaveToDB(dbx *bolt.DB) error { return dbx.Update(func(tx *bolt.Tx) error { duf := tx.Bucket(BKTUtxos)