Skip to content

Commit

Permalink
Unify local and external keys in keybase interface (cosmos#117)
Browse files Browse the repository at this point in the history
* Return errors on priv.Sign(), priv.PubKey()

* Add CreateLedger, CreateOffline

* Add switch on .Sign() for Ledger wallets

* Add offline signing switch on .Sign()

* Use MustUnmarshalBinaryBare()

* Add confirmation to delete offline/Ledger keys

* Lowercase error message

* Add human-readable .GetType() function to Info interface

* Rename CryptoAlgo => SignAlgo

* assert.Nil(t, err) => assert.NoError(t, err)
  • Loading branch information
cwgoes authored and liamsi committed Jun 10, 2018
1 parent f6c960c commit c21f67c
Show file tree
Hide file tree
Showing 14 changed files with 391 additions and 210 deletions.
23 changes: 16 additions & 7 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
# go-tests = true
# unused-packages = true


[[constraint]]
name = "github.com/btcsuite/btcutil"
branch = "master"
Expand Down Expand Up @@ -59,7 +58,7 @@

[[constraint]]
name = "github.com/zondax/ledger-goclient"
revision = "3e2146609cdb97894c064d59e9d00accd8c2b1dd"
revision = "065cbf938a16f20335c40cfe180f9cd4955c6a5a"

[prune]
go-tests = true
Expand Down
6 changes: 4 additions & 2 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,16 @@ func TestKeyEncodings(t *testing.T) {

// Check (de/en)codings of Signatures.
var sig1, sig2, sig3 Signature
sig1 = tc.privKey.Sign([]byte("something"))
sig1, err := tc.privKey.Sign([]byte("something"))
assert.NoError(t, err)
checkAminoBinary(t, sig1, &sig2, -1) // Siganture size changes for Secp anyways.
assert.EqualValues(t, sig1, sig2)
checkAminoJSON(t, sig1, &sig3, false) // TODO also check Prefix bytes.
assert.EqualValues(t, sig1, sig3)

// Check (de/en)codings of PubKeys.
pubKey := tc.privKey.PubKey()
pubKey, err := tc.privKey.PubKey()
assert.NoError(t, err)
var pub2, pub3 PubKey
checkAminoBinary(t, pubKey, &pub2, tc.pubSize)
assert.EqualValues(t, pubKey, pub2)
Expand Down
168 changes: 125 additions & 43 deletions keys/keybase.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package keys

import (
"bufio"
"fmt"
"os"
"strings"

"github.com/pkg/errors"
Expand All @@ -26,23 +28,23 @@ func New(db dbm.DB, codec words.Codec) dbKeybase {

var _ Keybase = dbKeybase{}

// Create generates a new key and persists it to storage, encrypted
// CreateMnemonic generates a new key and persists it to storage, encrypted
// using the passphrase. It returns the generated seedphrase
// (mnemonic) and the key Info. It returns an error if it fails to
// generate a key for the given algo type, or if another key is
// already stored under the same name.
func (kb dbKeybase) Create(name, passphrase string, algo CryptoAlgo) (Info, string, error) {
func (kb dbKeybase) CreateMnemonic(name, passphrase string, algo SignAlgo) (Info, string, error) {
// NOTE: secret is SHA256 hashed by secp256k1 and ed25519.
// 16 byte secret corresponds to 12 BIP39 words.
// XXX: Ledgers use 24 words now - should we ?
secret := crypto.CRandBytes(16)
priv, err := generate(algo, secret)
if err != nil {
return Info{}, "", err
return nil, "", err
}

// encrypt and persist the key
info := kb.writeKey(priv, name, passphrase)
info := kb.writeLocalKey(priv, name, passphrase)

// we append the type byte to the serialized secret to help with
// recovery
Expand All @@ -56,29 +58,52 @@ func (kb dbKeybase) Create(name, passphrase string, algo CryptoAlgo) (Info, stri
return info, seed, err
}

// CreateLedger creates a new locally-stored reference to a Ledger keypair
// It returns the created key info and an error if the Ledger could not be queried
func (kb dbKeybase) CreateLedger(name string, path crypto.DerivationPath, algo SignAlgo) (Info, error) {
if algo != AlgoSecp256k1 {
return nil, fmt.Errorf("Only secp256k1 is supported for Ledger devices")
}
priv, err := crypto.NewPrivKeyLedgerSecp256k1(path)
if err != nil {
return nil, err
}
pub, err := priv.PubKey()
if err != nil {
return nil, err
}
return kb.writeLedgerKey(pub, path, name), nil
}

// CreateOffline creates a new reference to an offline keypair
// It returns the created key info
func (kb dbKeybase) CreateOffline(name string, pub crypto.PubKey) (Info, error) {
return kb.writeOfflineKey(pub, name), nil
}

// Recover converts a seedphrase to a private key and persists it,
// encrypted with the given passphrase. Functions like Create, but
// seedphrase is input not output.
func (kb dbKeybase) Recover(name, passphrase, seedphrase string) (Info, error) {
words := strings.Split(strings.TrimSpace(seedphrase), " ")
secret, err := kb.codec.WordsToBytes(words)
if err != nil {
return Info{}, err
return nil, err
}

// secret is comprised of the actual secret with the type
// appended.
// ie [secret] = [type] + [secret]
typ, secret := secret[0], secret[1:]
algo := byteToCryptoAlgo(typ)
algo := byteToSignAlgo(typ)
priv, err := generate(algo, secret)
if err != nil {
return Info{}, err
return nil, err
}

// encrypt and persist key.
public := kb.writeKey(priv, name, passphrase)
return public, err
public := kb.writeLocalKey(priv, name, passphrase)
return public, nil
}

// List returns the keys from storage in alphabetical order.
Expand All @@ -87,7 +112,6 @@ func (kb dbKeybase) List() ([]Info, error) {
iter := kb.db.Iterator(nil, nil)
defer iter.Close()
for ; iter.Valid(); iter.Next() {
// key := iter.Key()
info, err := readInfo(iter.Value())
if err != nil {
return nil, err
Expand All @@ -110,17 +134,46 @@ func (kb dbKeybase) Sign(name, passphrase string, msg []byte) (sig crypto.Signat
if err != nil {
return
}
if info.PrivKeyArmor == "" {
err = fmt.Errorf("private key not available")
return
var priv crypto.PrivKey
switch info.(type) {
case localInfo:
linfo := info.(localInfo)
if linfo.PrivKeyArmor == "" {
err = fmt.Errorf("private key not available")
return
}
priv, err = unarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase)
if err != nil {
return nil, nil, err
}
case ledgerInfo:
linfo := info.(ledgerInfo)
priv, err = crypto.NewPrivKeyLedgerSecp256k1(linfo.Path)
if err != nil {
return
}
case offlineInfo:
linfo := info.(offlineInfo)
fmt.Printf("Bytes to sign:\n%s", msg)
buf := bufio.NewReader(os.Stdin)
fmt.Printf("\nEnter Amino-encoded signature:\n")
// Will block until user inputs the signature
signed, err := buf.ReadString('\n')
if err != nil {
return nil, nil, err
}
cdc.MustUnmarshalBinary([]byte(signed), sig)
return sig, linfo.GetPubKey(), nil
}
priv, err := unarmorDecryptPrivKey(info.PrivKeyArmor, passphrase)
sig, err = priv.Sign(msg)
if err != nil {
return
return nil, nil, err
}
sig = priv.Sign(msg)
pub = priv.PubKey()
return
pub, err = priv.PubKey()
if err != nil {
return nil, nil, err
}
return sig, pub, nil
}

func (kb dbKeybase) Export(name string) (armor string, err error) {
Expand All @@ -143,7 +196,7 @@ func (kb dbKeybase) ExportPubKey(name string) (armor string, err error) {
if err != nil {
return
}
return armorPubKeyBytes(info.PubKey.Bytes()), nil
return armorPubKeyBytes(info.GetPubKey().Bytes()), nil
}

func (kb dbKeybase) Import(name string, armor string) (err error) {
Expand Down Expand Up @@ -175,23 +228,37 @@ func (kb dbKeybase) ImportPubKey(name string, armor string) (err error) {
if err != nil {
return
}
kb.writePubKey(pubKey, name)
kb.writeOfflineKey(pubKey, name)
return
}

// Delete removes key forever, but we must present the
// proper passphrase before deleting it (for security).
// A passphrase of 'yes' is used to delete stored
// references to offline and Ledger / HW wallet keys
func (kb dbKeybase) Delete(name, passphrase string) error {
// verify we have the proper password before deleting
info, err := kb.Get(name)
if err != nil {
return err
}
_, err = unarmorDecryptPrivKey(info.PrivKeyArmor, passphrase)
if err != nil {
return err
switch info.(type) {
case localInfo:
linfo := info.(localInfo)
_, err = unarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase)
if err != nil {
return err
}
kb.db.DeleteSync(infoKey(name))
return nil
case ledgerInfo:
case offlineInfo:
if passphrase != "yes" {
return fmt.Errorf("enter exactly 'yes' to delete the key")
}
kb.db.DeleteSync(infoKey(name))
return nil
}
kb.db.DeleteSync(infoKey(name))
return nil
}

Expand All @@ -205,36 +272,51 @@ func (kb dbKeybase) Update(name, oldpass, newpass string) error {
if err != nil {
return err
}
key, err := unarmorDecryptPrivKey(info.PrivKeyArmor, oldpass)
if err != nil {
return err
switch info.(type) {
case localInfo:
linfo := info.(localInfo)
key, err := unarmorDecryptPrivKey(linfo.PrivKeyArmor, oldpass)
if err != nil {
return err
}
kb.writeLocalKey(key, name, newpass)
return nil
default:
return fmt.Errorf("Locally stored key required")
}

kb.writeKey(key, name, newpass)
return nil
}

func (kb dbKeybase) writePubKey(pub crypto.PubKey, name string) Info {
func (kb dbKeybase) writeLocalKey(priv crypto.PrivKey, name, passphrase string) Info {
// encrypt private key using passphrase
privArmor := encryptArmorPrivKey(priv, passphrase)
// make Info
info := newInfo(name, pub, "")

// write them both
kb.db.SetSync(infoKey(name), info.bytes())
pub, err := priv.PubKey()
if err != nil {
panic(err)
}
info := newLocalInfo(name, pub, privArmor)
kb.writeInfo(info, name)
return info
}

func (kb dbKeybase) writeKey(priv crypto.PrivKey, name, passphrase string) Info {
// generate the encrypted privkey
privArmor := encryptArmorPrivKey(priv, passphrase)
// make Info
info := newInfo(name, priv.PubKey(), privArmor)
func (kb dbKeybase) writeLedgerKey(pub crypto.PubKey, path crypto.DerivationPath, name string) Info {
info := newLedgerInfo(name, pub, path)
kb.writeInfo(info, name)
return info
}

// write them both
kb.db.SetSync(infoKey(name), info.bytes())
func (kb dbKeybase) writeOfflineKey(pub crypto.PubKey, name string) Info {
info := newOfflineInfo(name, pub)
kb.writeInfo(info, name)
return info
}

func generate(algo CryptoAlgo, secret []byte) (crypto.PrivKey, error) {
func (kb dbKeybase) writeInfo(info Info, name string) {
// write the info by key
kb.db.SetSync(infoKey(name), writeInfo(info))
}

func generate(algo SignAlgo, secret []byte) (crypto.PrivKey, error) {
switch algo {
case AlgoEd25519:
return crypto.GenPrivKeyEd25519FromSecret(secret), nil
Expand Down
Loading

0 comments on commit c21f67c

Please sign in to comment.