add keyfileio to manage private key storage on disk

This commit is contained in:
Tadge Dryja 2016-01-20 21:08:05 -08:00
parent 1f40c7214e
commit 73bbb29026
4 changed files with 239 additions and 23 deletions

@ -8,7 +8,6 @@ import (
"net" "net"
"os" "os"
"github.com/boltdb/bolt"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil/bloom" "github.com/btcsuite/btcutil/bloom"
@ -46,7 +45,7 @@ type SPVCon struct {
mBlockQueue chan RootAndHeight mBlockQueue chan RootAndHeight
} }
func OpenSPV(remoteNode string, hfn string, func OpenSPV(remoteNode string, hfn, tsfn string,
inTs *TxStore, p *chaincfg.Params) (SPVCon, error) { inTs *TxStore, p *chaincfg.Params) (SPVCon, error) {
// create new SPVCon // create new SPVCon
var s SPVCon var s SPVCon
@ -70,7 +69,7 @@ func OpenSPV(remoteNode string, hfn string,
s.localVersion = VERSION s.localVersion = VERSION
// transaction store for this SPV connection // transaction store for this SPV connection
err = inTs.OpenDB("utxo.db") err = inTs.OpenDB(tsfn)
if err != nil { if err != nil {
return s, err return s, err
} }
@ -130,26 +129,6 @@ func OpenSPV(remoteNode string, hfn string,
return s, nil 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 { func (s *SPVCon) openHeaderFile(hfn string) error {
_, err := os.Stat(hfn) _, err := os.Stat(hfn)
if err != nil { if err != nil {

191
uspv/keyfileio.go Normal file

@ -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
}

@ -5,10 +5,13 @@ import (
"fmt" "fmt"
"log" "log"
"github.com/btcsuite/btcd/chaincfg"
"github.com/boltdb/bolt" "github.com/boltdb/bolt"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/bloom" "github.com/btcsuite/btcutil/bloom"
"github.com/btcsuite/btcutil/hdkeychain"
) )
type TxStore struct { type TxStore struct {
@ -17,7 +20,13 @@ type TxStore struct {
Utxos []Utxo // stacks on stacks Utxos []Utxo // stacks on stacks
Sum int64 // racks on racks Sum int64 // racks on racks
Adrs []MyAdr // endeavouring to acquire capital Adrs []MyAdr // endeavouring to acquire capital
LastIdx uint32 // should equal len(Adrs)
StateDB *bolt.DB // place to write all this down 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. type Utxo struct { // cash money.

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"log"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
@ -16,6 +17,42 @@ var (
KEYState = []byte("LastUpdate") // last state of DB 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 { func (u *Utxo) SaveToDB(dbx *bolt.DB) error {
return dbx.Update(func(tx *bolt.Tx) error { return dbx.Update(func(tx *bolt.Tx) error {
duf := tx.Bucket(BKTUtxos) duf := tx.Bucket(BKTUtxos)