mirror of
https://github.com/fluencelabs/tendermint
synced 2025-04-25 06:42:16 +00:00
Merge PR #142: Delete keys package & Ledger integration; update tests
This commit is contained in:
commit
edb36d38b2
67
Gopkg.lock
generated
67
Gopkg.lock
generated
@ -1,18 +1,6 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/bartekn/go-bip39"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "a05967ea095d81c8fe4833776774cfaff8e5036c"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/brejski/hid"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "06112dcfcc50a7e0e4fd06e17f9791e788fdaafc"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/btcsuite/btcd"
|
name = "github.com/btcsuite/btcd"
|
||||||
@ -63,18 +51,6 @@
|
|||||||
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
|
revision = "1adfc126b41513cc696b209667c8656ea7aac67c"
|
||||||
version = "v1.0.0"
|
version = "v1.0.0"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/golang/snappy"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/jmhodges/levigo"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "c42d9e0ca023e2198120196f842701bb4c55d7b9"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/kr/logfmt"
|
name = "github.com/kr/logfmt"
|
||||||
@ -102,25 +78,6 @@
|
|||||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||||
version = "v1.2.2"
|
version = "v1.2.2"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/syndtr/goleveldb"
|
|
||||||
packages = [
|
|
||||||
"leveldb",
|
|
||||||
"leveldb/cache",
|
|
||||||
"leveldb/comparer",
|
|
||||||
"leveldb/errors",
|
|
||||||
"leveldb/filter",
|
|
||||||
"leveldb/iterator",
|
|
||||||
"leveldb/journal",
|
|
||||||
"leveldb/memdb",
|
|
||||||
"leveldb/opt",
|
|
||||||
"leveldb/storage",
|
|
||||||
"leveldb/table",
|
|
||||||
"leveldb/util"
|
|
||||||
]
|
|
||||||
revision = "e2150783cd35f5b607daca48afd8c57ec54cc995"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/tendermint/ed25519"
|
name = "github.com/tendermint/ed25519"
|
||||||
@ -134,31 +91,19 @@
|
|||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/tendermint/go-amino"
|
name = "github.com/tendermint/go-amino"
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "1715b7b78c65d6adcc5937315be4710234cefe09"
|
revision = "2106ca61d91029c931fd54968c2bb02dc96b1412"
|
||||||
version = "0.10.0-rc2"
|
version = "0.10.1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
name = "github.com/tendermint/tmlibs"
|
name = "github.com/tendermint/tmlibs"
|
||||||
packages = [
|
packages = [
|
||||||
"common",
|
"common",
|
||||||
"db",
|
|
||||||
"log",
|
"log",
|
||||||
"test"
|
"test"
|
||||||
]
|
]
|
||||||
revision = "692f1d86a6e2c0efa698fd1e4541b68c74ffaf38"
|
revision = "692f1d86a6e2c0efa698fd1e4541b68c74ffaf38"
|
||||||
version = "v0.8.4"
|
version = "v0.8.4"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/tyler-smith/go-bip39"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "8e7a99b3e716f36d3b080a9a70f9eb45abe4edcc"
|
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
name = "github.com/zondax/ledger-goclient"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "065cbf938a16f20335c40cfe180f9cd4955c6a5a"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/crypto"
|
name = "golang.org/x/crypto"
|
||||||
@ -168,25 +113,25 @@
|
|||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"internal/chacha20",
|
"internal/chacha20",
|
||||||
|
"internal/subtle",
|
||||||
"nacl/secretbox",
|
"nacl/secretbox",
|
||||||
"openpgp/armor",
|
"openpgp/armor",
|
||||||
"openpgp/errors",
|
"openpgp/errors",
|
||||||
"pbkdf2",
|
|
||||||
"poly1305",
|
"poly1305",
|
||||||
"ripemd160",
|
"ripemd160",
|
||||||
"salsa20/salsa"
|
"salsa20/salsa"
|
||||||
]
|
]
|
||||||
revision = "8ac0e0d97ce45cd83d1d7243c060cb8461dda5e9"
|
revision = "7f39a6fea4fe9364fb61e1def6a268a51b4f3a06"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/sys"
|
name = "golang.org/x/sys"
|
||||||
packages = ["cpu"]
|
packages = ["cpu"]
|
||||||
revision = "a9e25c09b96b8870693763211309e213c6ef299d"
|
revision = "ad87a3a340fa7f3bed189293fbfa7a9b7e021ae1"
|
||||||
|
|
||||||
[solve-meta]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
inputs-digest = "684746f2b651a722ee32eae056b437baf7a2213d68ddbfcefe04a1c3df27c93b"
|
inputs-digest = "027b22b86396a971d5d5c1d298947f531f39743975d65a22e98601140aa1b1a1"
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
12
Gopkg.toml
12
Gopkg.toml
@ -28,10 +28,6 @@
|
|||||||
name = "github.com/btcsuite/btcutil"
|
name = "github.com/btcsuite/btcutil"
|
||||||
branch = "master"
|
branch = "master"
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/pkg/errors"
|
|
||||||
version = "0.8.0"
|
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/stretchr/testify"
|
name = "github.com/stretchr/testify"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@ -48,14 +44,6 @@
|
|||||||
name = "github.com/tendermint/tmlibs"
|
name = "github.com/tendermint/tmlibs"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/tyler-smith/go-bip39"
|
|
||||||
branch = "master"
|
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/zondax/ledger-goclient"
|
|
||||||
revision = "065cbf938a16f20335c40cfe180f9cd4955c6a5a"
|
|
||||||
|
|
||||||
[prune]
|
[prune]
|
||||||
go-tests = true
|
go-tests = true
|
||||||
unused-packages = true
|
unused-packages = true
|
||||||
|
2
Makefile
2
Makefile
@ -51,7 +51,7 @@ get_vendor_deps:
|
|||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -p 1 $(shell go list ./... | grep -v vendor)
|
CGO_ENABLED=0 go test -p 1 $(shell go list ./... | grep -v vendor)
|
||||||
|
|
||||||
########################################
|
########################################
|
||||||
### Formatting, linting, and vetting
|
### Formatting, linting, and vetting
|
||||||
|
2
amino.go
2
amino.go
@ -28,8 +28,6 @@ func RegisterAmino(cdc *amino.Codec) {
|
|||||||
"tendermint/PrivKeyEd25519", nil)
|
"tendermint/PrivKeyEd25519", nil)
|
||||||
cdc.RegisterConcrete(PrivKeySecp256k1{},
|
cdc.RegisterConcrete(PrivKeySecp256k1{},
|
||||||
"tendermint/PrivKeySecp256k1", nil)
|
"tendermint/PrivKeySecp256k1", nil)
|
||||||
cdc.RegisterConcrete(PrivKeyLedgerSecp256k1{},
|
|
||||||
"tendermint/PrivKeyLedgerSecp256k1", nil)
|
|
||||||
|
|
||||||
cdc.RegisterInterface((*Signature)(nil), nil)
|
cdc.RegisterInterface((*Signature)(nil), nil)
|
||||||
cdc.RegisterConcrete(SignatureEd25519{},
|
cdc.RegisterConcrete(SignatureEd25519{},
|
||||||
|
@ -50,7 +50,6 @@ func ExamplePrintRegisteredTypes() {
|
|||||||
//| PubKeySecp256k1 | tendermint/PubKeySecp256k1 | 0xEB5AE987 | 0x21 | |
|
//| PubKeySecp256k1 | tendermint/PubKeySecp256k1 | 0xEB5AE987 | 0x21 | |
|
||||||
//| PrivKeyEd25519 | tendermint/PrivKeyEd25519 | 0xA3288910 | 0x40 | |
|
//| PrivKeyEd25519 | tendermint/PrivKeyEd25519 | 0xA3288910 | 0x40 | |
|
||||||
//| PrivKeySecp256k1 | tendermint/PrivKeySecp256k1 | 0xE1B0F79B | 0x20 | |
|
//| PrivKeySecp256k1 | tendermint/PrivKeySecp256k1 | 0xE1B0F79B | 0x20 | |
|
||||||
//| PrivKeyLedgerSecp256k1 | tendermint/PrivKeyLedgerSecp256k1 | 0x10CAB393 | variable | |
|
|
||||||
//| SignatureEd25519 | tendermint/SignatureEd25519 | 0x2031EA53 | 0x40 | |
|
//| SignatureEd25519 | tendermint/SignatureEd25519 | 0x2031EA53 | 0x40 | |
|
||||||
//| SignatureSecp256k1 | tendermint/SignatureSecp256k1 | 0x7FC4A495 | variable | |
|
//| SignatureSecp256k1 | tendermint/SignatureSecp256k1 | 0x7FC4A495 | variable | |
|
||||||
}
|
}
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
// Copyright 2011 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package bcrypt
|
|
||||||
|
|
||||||
import "encoding/base64"
|
|
||||||
|
|
||||||
const alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
||||||
|
|
||||||
var bcEncoding = base64.NewEncoding(alphabet)
|
|
||||||
|
|
||||||
func base64Encode(src []byte) []byte {
|
|
||||||
n := bcEncoding.EncodedLen(len(src))
|
|
||||||
dst := make([]byte, n)
|
|
||||||
bcEncoding.Encode(dst, src)
|
|
||||||
for dst[n-1] == '=' {
|
|
||||||
n--
|
|
||||||
}
|
|
||||||
return dst[:n]
|
|
||||||
}
|
|
||||||
|
|
||||||
func base64Decode(src []byte) ([]byte, error) {
|
|
||||||
numOfEquals := 4 - (len(src) % 4)
|
|
||||||
for i := 0; i < numOfEquals; i++ {
|
|
||||||
src = append(src, '=')
|
|
||||||
}
|
|
||||||
|
|
||||||
dst := make([]byte, bcEncoding.DecodedLen(len(src)))
|
|
||||||
n, err := bcEncoding.Decode(dst, src)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return dst[:n], nil
|
|
||||||
}
|
|
@ -1,292 +0,0 @@
|
|||||||
package bcrypt
|
|
||||||
|
|
||||||
// MODIFIED BY TENDERMINT TO EXPOSE NONCE
|
|
||||||
// Copyright 2011 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// Package bcrypt implements Provos and Mazières's bcrypt adaptive hashing
|
|
||||||
// algorithm. See http://www.usenix.org/event/usenix99/provos/provos.pdf
|
|
||||||
|
|
||||||
// The code is a port of Provos and Mazières's C implementation.
|
|
||||||
import (
|
|
||||||
"crypto/subtle"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/blowfish"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
MinCost int = 4 // the minimum allowable cost as passed in to GenerateFromPassword
|
|
||||||
MaxCost int = 31 // the maximum allowable cost as passed in to GenerateFromPassword
|
|
||||||
DefaultCost int = 10 // the cost that will actually be set if a cost below MinCost is passed into GenerateFromPassword
|
|
||||||
)
|
|
||||||
|
|
||||||
// The error returned from CompareHashAndPassword when a password and hash do
|
|
||||||
// not match.
|
|
||||||
var ErrMismatchedHashAndPassword = errors.New("crypto/bcrypt: hashedPassword is not the hash of the given password")
|
|
||||||
|
|
||||||
// The error returned from CompareHashAndPassword when a hash is too short to
|
|
||||||
// be a bcrypt hash.
|
|
||||||
var ErrHashTooShort = errors.New("crypto/bcrypt: hashedSecret too short to be a bcrypted password")
|
|
||||||
|
|
||||||
// The error returned from CompareHashAndPassword when a hash was created with
|
|
||||||
// a bcrypt algorithm newer than this implementation.
|
|
||||||
type HashVersionTooNewError byte
|
|
||||||
|
|
||||||
func (hv HashVersionTooNewError) Error() string {
|
|
||||||
return fmt.Sprintf("crypto/bcrypt: bcrypt algorithm version '%c' requested is newer than current version '%c'", byte(hv), majorVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The error returned from CompareHashAndPassword when a hash starts with something other than '$'
|
|
||||||
type InvalidHashPrefixError byte
|
|
||||||
|
|
||||||
func (ih InvalidHashPrefixError) Error() string {
|
|
||||||
return fmt.Sprintf("crypto/bcrypt: bcrypt hashes must start with '$', but hashedSecret started with '%c'", byte(ih))
|
|
||||||
}
|
|
||||||
|
|
||||||
type InvalidCostError int
|
|
||||||
|
|
||||||
func (ic InvalidCostError) Error() string {
|
|
||||||
return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed range (%d,%d)", int(ic), int(MinCost), int(MaxCost)) // nolint: unconvert
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
majorVersion = '2'
|
|
||||||
minorVersion = 'a'
|
|
||||||
maxSaltSize = 16
|
|
||||||
maxCryptedHashSize = 23
|
|
||||||
encodedSaltSize = 22
|
|
||||||
encodedHashSize = 31
|
|
||||||
minHashSize = 59
|
|
||||||
)
|
|
||||||
|
|
||||||
// magicCipherData is an IV for the 64 Blowfish encryption calls in
|
|
||||||
// bcrypt(). It's the string "OrpheanBeholderScryDoubt" in big-endian bytes.
|
|
||||||
var magicCipherData = []byte{
|
|
||||||
0x4f, 0x72, 0x70, 0x68,
|
|
||||||
0x65, 0x61, 0x6e, 0x42,
|
|
||||||
0x65, 0x68, 0x6f, 0x6c,
|
|
||||||
0x64, 0x65, 0x72, 0x53,
|
|
||||||
0x63, 0x72, 0x79, 0x44,
|
|
||||||
0x6f, 0x75, 0x62, 0x74,
|
|
||||||
}
|
|
||||||
|
|
||||||
type hashed struct {
|
|
||||||
hash []byte
|
|
||||||
salt []byte
|
|
||||||
cost int // allowed range is MinCost to MaxCost
|
|
||||||
major byte
|
|
||||||
minor byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateFromPassword returns the bcrypt hash of the password at the given
|
|
||||||
// cost. If the cost given is less than MinCost, the cost will be set to
|
|
||||||
// DefaultCost, instead. Use CompareHashAndPassword, as defined in this package,
|
|
||||||
// to compare the returned hashed password with its cleartext version.
|
|
||||||
func GenerateFromPassword(salt []byte, password []byte, cost int) ([]byte, error) {
|
|
||||||
if len(salt) != maxSaltSize {
|
|
||||||
return nil, fmt.Errorf("salt len must be %v", maxSaltSize)
|
|
||||||
}
|
|
||||||
p, err := newFromPassword(salt, password, cost)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return p.Hash(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompareHashAndPassword compares a bcrypt hashed password with its possible
|
|
||||||
// plaintext equivalent. Returns nil on success, or an error on failure.
|
|
||||||
func CompareHashAndPassword(hashedPassword, password []byte) error {
|
|
||||||
p, err := newFromHash(hashedPassword)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
otherHash, err := bcrypt(password, p.cost, p.salt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
otherP := &hashed{otherHash, p.salt, p.cost, p.major, p.minor}
|
|
||||||
if subtle.ConstantTimeCompare(p.Hash(), otherP.Hash()) == 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return ErrMismatchedHashAndPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cost returns the hashing cost used to create the given hashed
|
|
||||||
// password. When, in the future, the hashing cost of a password system needs
|
|
||||||
// to be increased in order to adjust for greater computational power, this
|
|
||||||
// function allows one to establish which passwords need to be updated.
|
|
||||||
func Cost(hashedPassword []byte) (int, error) {
|
|
||||||
p, err := newFromHash(hashedPassword)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return p.cost, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFromPassword(salt []byte, password []byte, cost int) (*hashed, error) {
|
|
||||||
if cost < MinCost {
|
|
||||||
cost = DefaultCost
|
|
||||||
}
|
|
||||||
p := new(hashed)
|
|
||||||
p.major = majorVersion
|
|
||||||
p.minor = minorVersion
|
|
||||||
|
|
||||||
err := checkCost(cost)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.cost = cost
|
|
||||||
|
|
||||||
p.salt = base64Encode(salt)
|
|
||||||
hash, err := bcrypt(password, p.cost, p.salt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
p.hash = hash
|
|
||||||
return p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFromHash(hashedSecret []byte) (*hashed, error) {
|
|
||||||
if len(hashedSecret) < minHashSize {
|
|
||||||
return nil, ErrHashTooShort
|
|
||||||
}
|
|
||||||
p := new(hashed)
|
|
||||||
n, err := p.decodeVersion(hashedSecret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hashedSecret = hashedSecret[n:]
|
|
||||||
n, err = p.decodeCost(hashedSecret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hashedSecret = hashedSecret[n:]
|
|
||||||
|
|
||||||
// The "+2" is here because we'll have to append at most 2 '=' to the salt
|
|
||||||
// when base64 decoding it in expensiveBlowfishSetup().
|
|
||||||
p.salt = make([]byte, encodedSaltSize, encodedSaltSize+2)
|
|
||||||
copy(p.salt, hashedSecret[:encodedSaltSize])
|
|
||||||
|
|
||||||
hashedSecret = hashedSecret[encodedSaltSize:]
|
|
||||||
p.hash = make([]byte, len(hashedSecret))
|
|
||||||
copy(p.hash, hashedSecret)
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func bcrypt(password []byte, cost int, salt []byte) ([]byte, error) {
|
|
||||||
cipherData := make([]byte, len(magicCipherData))
|
|
||||||
copy(cipherData, magicCipherData)
|
|
||||||
|
|
||||||
c, err := expensiveBlowfishSetup(password, uint32(cost), salt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 24; i += 8 {
|
|
||||||
for j := 0; j < 64; j++ {
|
|
||||||
c.Encrypt(cipherData[i:i+8], cipherData[i:i+8])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bug compatibility with C bcrypt implementations. We only encode 23 of
|
|
||||||
// the 24 bytes encrypted.
|
|
||||||
hsh := base64Encode(cipherData[:maxCryptedHashSize])
|
|
||||||
return hsh, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func expensiveBlowfishSetup(key []byte, cost uint32, salt []byte) (*blowfish.Cipher, error) {
|
|
||||||
|
|
||||||
csalt, err := base64Decode(salt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bug compatibility with C bcrypt implementations. They use the trailing
|
|
||||||
// NULL in the key string during expansion.
|
|
||||||
ckey := append(key, 0)
|
|
||||||
|
|
||||||
c, err := blowfish.NewSaltedCipher(ckey, csalt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var i, rounds uint64
|
|
||||||
rounds = 1 << cost
|
|
||||||
for i = 0; i < rounds; i++ {
|
|
||||||
blowfish.ExpandKey(ckey, c)
|
|
||||||
blowfish.ExpandKey(csalt, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *hashed) Hash() []byte {
|
|
||||||
arr := make([]byte, 60)
|
|
||||||
arr[0] = '$'
|
|
||||||
arr[1] = p.major
|
|
||||||
n := 2
|
|
||||||
if p.minor != 0 {
|
|
||||||
arr[2] = p.minor
|
|
||||||
n = 3
|
|
||||||
}
|
|
||||||
arr[n] = '$'
|
|
||||||
n += 1
|
|
||||||
copy(arr[n:], []byte(fmt.Sprintf("%02d", p.cost)))
|
|
||||||
n += 2
|
|
||||||
arr[n] = '$'
|
|
||||||
n += 1
|
|
||||||
copy(arr[n:], p.salt)
|
|
||||||
n += encodedSaltSize
|
|
||||||
copy(arr[n:], p.hash)
|
|
||||||
n += encodedHashSize
|
|
||||||
return arr[:n]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *hashed) decodeVersion(sbytes []byte) (int, error) {
|
|
||||||
if sbytes[0] != '$' {
|
|
||||||
return -1, InvalidHashPrefixError(sbytes[0])
|
|
||||||
}
|
|
||||||
if sbytes[1] > majorVersion {
|
|
||||||
return -1, HashVersionTooNewError(sbytes[1])
|
|
||||||
}
|
|
||||||
p.major = sbytes[1]
|
|
||||||
n := 3
|
|
||||||
if sbytes[2] != '$' {
|
|
||||||
p.minor = sbytes[2]
|
|
||||||
n++
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sbytes should begin where decodeVersion left off.
|
|
||||||
func (p *hashed) decodeCost(sbytes []byte) (int, error) {
|
|
||||||
cost, err := strconv.Atoi(string(sbytes[0:2]))
|
|
||||||
if err != nil {
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
err = checkCost(cost)
|
|
||||||
if err != nil {
|
|
||||||
return -1, err
|
|
||||||
}
|
|
||||||
p.cost = cost
|
|
||||||
return 3, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *hashed) String() string {
|
|
||||||
return fmt.Sprintf("&{hash: %#v, salt: %#v, cost: %d, major: %c, minor: %c}", string(p.hash), p.salt, p.cost, p.major, p.minor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkCost(cost int) error {
|
|
||||||
if cost < MinCost || cost > MaxCost {
|
|
||||||
return InvalidCostError(cost)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
package bip39
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bartekn/go-bip39"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ValidSentenceLen defines the mnemonic sentence lengths supported by this BIP 39 library.
|
|
||||||
type ValidSentenceLen uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
// FundRaiser is the sentence length used during the cosmos fundraiser (12 words).
|
|
||||||
FundRaiser ValidSentenceLen = 12
|
|
||||||
// FreshKey is the sentence length used for newly created keys (24 words).
|
|
||||||
FreshKey ValidSentenceLen = 24
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewMnemonic will return a string consisting of the mnemonic words for
|
|
||||||
// the given sentence length.
|
|
||||||
func NewMnemonic(len ValidSentenceLen) (words []string, err error) {
|
|
||||||
// len = (ENT + checksum) / 11
|
|
||||||
var ENT int
|
|
||||||
switch len {
|
|
||||||
case FundRaiser:
|
|
||||||
ENT = 128
|
|
||||||
case FreshKey:
|
|
||||||
ENT = 256
|
|
||||||
}
|
|
||||||
var entropy []byte
|
|
||||||
entropy, err = bip39.NewEntropy(ENT)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var mnemonic string
|
|
||||||
mnemonic, err = bip39.NewMnemonic(entropy)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
words = strings.Split(mnemonic, " ")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// MnemonicToSeed creates a BIP 39 seed from the passed mnemonic (with an empty BIP 39 password).
|
|
||||||
// This method does not validate the mnemonics checksum.
|
|
||||||
func MnemonicToSeed(mne string) (seed []byte) {
|
|
||||||
// we do not checksum here...
|
|
||||||
seed = bip39.NewSeed(mne, "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// MnemonicToSeedWithErrChecking returns the same seed as MnemonicToSeed.
|
|
||||||
// It creates a BIP 39 seed from the passed mnemonic (with an empty BIP 39 password).
|
|
||||||
//
|
|
||||||
// Different from MnemonicToSeed it validates the checksum.
|
|
||||||
// For details on the checksum see the BIP 39 spec.
|
|
||||||
func MnemonicToSeedWithErrChecking(mne string) (seed []byte, err error) {
|
|
||||||
seed, err = bip39.NewSeedWithErrorChecking(mne, "")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
|||||||
package bip39
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestWordCodec_NewMnemonic(t *testing.T) {
|
|
||||||
_, err := NewMnemonic(FundRaiser)
|
|
||||||
assert.NoError(t, err, "unexpected error generating fundraiser mnemonic")
|
|
||||||
|
|
||||||
_, err = NewMnemonic(FreshKey)
|
|
||||||
assert.NoError(t, err, "unexpected error generating new 24-word mnemonic")
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
package hd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/tyler-smith/go-bip39"
|
|
||||||
|
|
||||||
"github.com/tendermint/go-crypto"
|
|
||||||
)
|
|
||||||
|
|
||||||
type addrData struct {
|
|
||||||
Mnemonic string
|
|
||||||
Master string
|
|
||||||
Seed string
|
|
||||||
Priv string
|
|
||||||
Pub string
|
|
||||||
Addr string
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func initFundraiserTestVectors(t *testing.T) []addrData {
|
|
||||||
// NOTE: atom fundraiser address
|
|
||||||
// var hdPath string = "m/44'/118'/0'/0/0"
|
|
||||||
var hdToAddrTable []addrData
|
|
||||||
|
|
||||||
b, err := ioutil.ReadFile("test.json")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not read fundraiser test vector file (test.json): %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(b, &hdToAddrTable)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("could not decode test vectors (test.json): %s", err)
|
|
||||||
}
|
|
||||||
return hdToAddrTable
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFundraiserCompatibility(t *testing.T) {
|
|
||||||
hdToAddrTable := initFundraiserTestVectors(t)
|
|
||||||
|
|
||||||
for i, d := range hdToAddrTable {
|
|
||||||
privB, _ := hex.DecodeString(d.Priv)
|
|
||||||
pubB, _ := hex.DecodeString(d.Pub)
|
|
||||||
addrB, _ := hex.DecodeString(d.Addr)
|
|
||||||
seedB, _ := hex.DecodeString(d.Seed)
|
|
||||||
masterB, _ := hex.DecodeString(d.Master)
|
|
||||||
|
|
||||||
seed := bip39.NewSeed(d.Mnemonic, "")
|
|
||||||
|
|
||||||
t.Log("================================")
|
|
||||||
t.Logf("ROUND: %d MNEMONIC: %s", i, d.Mnemonic)
|
|
||||||
|
|
||||||
master, ch := ComputeMastersFromSeed(seed)
|
|
||||||
priv, err := DerivePrivateKeyForPath(master, ch, "44'/118'/0'/0/0")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
pub := crypto.PrivKeySecp256k1(priv).PubKey()
|
|
||||||
|
|
||||||
t.Log("\tNODEJS GOLANG\n")
|
|
||||||
t.Logf("SEED \t%X %X\n", seedB, seed)
|
|
||||||
t.Logf("MSTR \t%X %X\n", masterB, master)
|
|
||||||
t.Logf("PRIV \t%X %X\n", privB, priv)
|
|
||||||
t.Logf("PUB \t%X %X\n", pubB, pub)
|
|
||||||
|
|
||||||
assert.Equal(t, seedB, seed)
|
|
||||||
assert.Equal(t, master[:], masterB, fmt.Sprintf("Expected masters to match for %d", i))
|
|
||||||
assert.Equal(t, priv[:], privB, "Expected priv keys to match")
|
|
||||||
var pubBFixed [33]byte
|
|
||||||
copy(pubBFixed[:], pubB)
|
|
||||||
assert.Equal(t, pub, crypto.PubKeySecp256k1(pubBFixed), fmt.Sprintf("Expected pub keys to match for %d", i))
|
|
||||||
|
|
||||||
addr := pub.Address()
|
|
||||||
t.Logf("ADDR \t%X %X\n", addrB, addr)
|
|
||||||
assert.Equal(t, addr, crypto.Address(addrB), fmt.Sprintf("Expected addresses to match %d", i))
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
|||||||
// Package hd provides basic functionality Hierarchical Deterministic Wallets.
|
|
||||||
//
|
|
||||||
// The user must understand the overall concept of the BIP 32 and the BIP 44 specs:
|
|
||||||
// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
|
|
||||||
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
|
||||||
//
|
|
||||||
// In combination with the bip39 package in go-crypto this package provides the functionality for deriving keys using a
|
|
||||||
// BIP 44 HD path, or, more general, by passing a BIP 32 path.
|
|
||||||
//
|
|
||||||
// In particular, this package (together with bip39) provides all necessary functionality to derive keys from
|
|
||||||
// mnemonics generated during the cosmos fundraiser.
|
|
||||||
package hd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha512"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec"
|
|
||||||
"github.com/tendermint/go-crypto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BIP44Prefix is the parts of the BIP32 HD path that are fixed by what we used during the fundraiser.
|
|
||||||
const (
|
|
||||||
BIP44Prefix = "44'/118'/"
|
|
||||||
FullFundraiserPath = BIP44Prefix + "0'/0/0"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BIP44Params wraps BIP 44 params (5 level BIP 32 path).
|
|
||||||
// To receive a canonical string representation ala
|
|
||||||
// m / purpose' / coin_type' / account' / change / address_index
|
|
||||||
// call String() on a BIP44Params instance.
|
|
||||||
type BIP44Params struct {
|
|
||||||
purpose uint32
|
|
||||||
coinType uint32
|
|
||||||
account uint32
|
|
||||||
change bool
|
|
||||||
addressIdx uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewParams creates a BIP 44 parameter object from the params:
|
|
||||||
// m / purpose' / coin_type' / account' / change / address_index
|
|
||||||
func NewParams(purpose, coinType, account uint32, change bool, addressIdx uint32) *BIP44Params {
|
|
||||||
return &BIP44Params{
|
|
||||||
purpose: purpose,
|
|
||||||
coinType: coinType,
|
|
||||||
account: account,
|
|
||||||
change: change,
|
|
||||||
addressIdx: addressIdx,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFundraiserParams creates a BIP 44 parameter object from the params:
|
|
||||||
// m / 44' / 118' / account' / 0 / address_index
|
|
||||||
// The fixed parameters (purpose', coin_type', and change) are determined by what was used in the fundraiser.
|
|
||||||
func NewFundraiserParams(account uint32, addressIdx uint32) *BIP44Params {
|
|
||||||
return NewParams(44, 118, account, false, addressIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p BIP44Params) String() string {
|
|
||||||
var changeStr string
|
|
||||||
if p.change {
|
|
||||||
changeStr = "1"
|
|
||||||
} else {
|
|
||||||
changeStr = "0"
|
|
||||||
}
|
|
||||||
// m / purpose' / coin_type' / account' / change / address_index
|
|
||||||
return fmt.Sprintf("%d'/%d'/%d'/%s/%d",
|
|
||||||
p.purpose,
|
|
||||||
p.coinType,
|
|
||||||
p.account,
|
|
||||||
changeStr,
|
|
||||||
p.addressIdx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ComputeMastersFromSeed returns the master public key, master secret, and chain code in hex.
|
|
||||||
func ComputeMastersFromSeed(seed []byte) (secret [32]byte, chainCode [32]byte) {
|
|
||||||
masterSecret := []byte("Bitcoin seed")
|
|
||||||
secret, chainCode = i64(masterSecret, seed)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// DerivePrivateKeyForPath derives the private key by following the BIP 32/44 path from privKeyBytes,
|
|
||||||
// using the given chainCode.
|
|
||||||
func DerivePrivateKeyForPath(privKeyBytes [32]byte, chainCode [32]byte, path string) ([32]byte, error) {
|
|
||||||
data := privKeyBytes
|
|
||||||
parts := strings.Split(path, "/")
|
|
||||||
for _, part := range parts {
|
|
||||||
// do we have an apostrophe?
|
|
||||||
harden := part[len(part)-1:] == "'"
|
|
||||||
// harden == private derivation, else public derivation:
|
|
||||||
if harden {
|
|
||||||
part = part[:len(part)-1]
|
|
||||||
}
|
|
||||||
idx, err := strconv.Atoi(part)
|
|
||||||
if err != nil {
|
|
||||||
return [32]byte{}, fmt.Errorf("invalid BIP 32 path: %s", err)
|
|
||||||
}
|
|
||||||
if idx < 0 {
|
|
||||||
return [32]byte{}, errors.New("invalid BIP 32 path: index negative ot too large")
|
|
||||||
}
|
|
||||||
data, chainCode = derivePrivateKey(data, chainCode, uint32(idx), harden)
|
|
||||||
}
|
|
||||||
var derivedKey [32]byte
|
|
||||||
n := copy(derivedKey[:], data[:])
|
|
||||||
if n != 32 || len(data) != 32 {
|
|
||||||
return [32]byte{}, fmt.Errorf("expected a (secp256k1) key of length 32, got length: %v", len(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
return derivedKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// derivePrivateKey derives the private key with index and chainCode.
|
|
||||||
// If harden is true, the derivation is 'hardened'.
|
|
||||||
// It returns the new private key and new chain code.
|
|
||||||
// For more information on hardened keys see:
|
|
||||||
// - https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
|
|
||||||
func derivePrivateKey(privKeyBytes [32]byte, chainCode [32]byte, index uint32, harden bool) ([32]byte, [32]byte) {
|
|
||||||
var data []byte
|
|
||||||
if harden {
|
|
||||||
index = index | 0x80000000
|
|
||||||
data = append([]byte{byte(0)}, privKeyBytes[:]...)
|
|
||||||
} else {
|
|
||||||
// this can't return an error:
|
|
||||||
pubkey := crypto.PrivKeySecp256k1(privKeyBytes).PubKey()
|
|
||||||
|
|
||||||
public := pubkey.(crypto.PubKeySecp256k1)
|
|
||||||
data = public[:]
|
|
||||||
}
|
|
||||||
data = append(data, uint32ToBytes(index)...)
|
|
||||||
data2, chainCode2 := i64(chainCode[:], data)
|
|
||||||
x := addScalars(privKeyBytes[:], data2[:])
|
|
||||||
return x, chainCode2
|
|
||||||
}
|
|
||||||
|
|
||||||
// modular big endian addition
|
|
||||||
func addScalars(a []byte, b []byte) [32]byte {
|
|
||||||
aInt := new(big.Int).SetBytes(a)
|
|
||||||
bInt := new(big.Int).SetBytes(b)
|
|
||||||
sInt := new(big.Int).Add(aInt, bInt)
|
|
||||||
x := sInt.Mod(sInt, btcec.S256().N).Bytes()
|
|
||||||
x2 := [32]byte{}
|
|
||||||
copy(x2[32-len(x):], x)
|
|
||||||
return x2
|
|
||||||
}
|
|
||||||
|
|
||||||
func uint32ToBytes(i uint32) []byte {
|
|
||||||
b := [4]byte{}
|
|
||||||
binary.BigEndian.PutUint32(b[:], i)
|
|
||||||
return b[:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// i64 returns the two halfs of the SHA512 HMAC of key and data.
|
|
||||||
func i64(key []byte, data []byte) (IL [32]byte, IR [32]byte) {
|
|
||||||
mac := hmac.New(sha512.New, key)
|
|
||||||
// sha512 does not err
|
|
||||||
_, _ = mac.Write(data)
|
|
||||||
I := mac.Sum(nil)
|
|
||||||
copy(IL[:], I[:32])
|
|
||||||
copy(IR[:], I[32:])
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
package hd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"github.com/tendermint/go-crypto/keys/bip39"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ExampleStringifyPathParams() {
|
|
||||||
path := NewParams(44, 0, 0, false, 0)
|
|
||||||
fmt.Println(path.String())
|
|
||||||
// Output: 44'/0'/0'/0/0
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleSomeBIP32TestVecs() {
|
|
||||||
|
|
||||||
seed := bip39.MnemonicToSeed("barrel original fuel morning among eternal " +
|
|
||||||
"filter ball stove pluck matrix mechanic")
|
|
||||||
master, ch := ComputeMastersFromSeed(seed)
|
|
||||||
fmt.Println("keys from fundraiser test-vector (cosmos, bitcoin, ether)")
|
|
||||||
fmt.Println()
|
|
||||||
// cosmos
|
|
||||||
priv, _ := DerivePrivateKeyForPath(master, ch, FullFundraiserPath)
|
|
||||||
fmt.Println(hex.EncodeToString(priv[:]))
|
|
||||||
// bitcoin
|
|
||||||
priv, _ = DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/0")
|
|
||||||
fmt.Println(hex.EncodeToString(priv[:]))
|
|
||||||
// ether
|
|
||||||
priv, _ = DerivePrivateKeyForPath(master, ch, "44'/60'/0'/0/0")
|
|
||||||
fmt.Println(hex.EncodeToString(priv[:]))
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("keys generated via https://coinomi.com/recovery-phrase-tool.html")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
seed = bip39.MnemonicToSeed(
|
|
||||||
"advice process birth april short trust crater change bacon monkey medal garment " +
|
|
||||||
"gorilla ranch hour rival razor call lunar mention taste vacant woman sister")
|
|
||||||
master, ch = ComputeMastersFromSeed(seed)
|
|
||||||
priv, _ = DerivePrivateKeyForPath(master, ch, "44'/1'/1'/0/4")
|
|
||||||
fmt.Println(hex.EncodeToString(priv[:]))
|
|
||||||
|
|
||||||
seed = bip39.MnemonicToSeed("idea naive region square margin day captain habit " +
|
|
||||||
"gun second farm pact pulse someone armed")
|
|
||||||
master, ch = ComputeMastersFromSeed(seed)
|
|
||||||
priv, _ = DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/420")
|
|
||||||
fmt.Println(hex.EncodeToString(priv[:]))
|
|
||||||
|
|
||||||
fmt.Println()
|
|
||||||
fmt.Println("BIP 32 example")
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// bip32 path: m/0/7
|
|
||||||
seed = bip39.MnemonicToSeed("monitor flock loyal sick object grunt duty ride develop assault harsh history")
|
|
||||||
master, ch = ComputeMastersFromSeed(seed)
|
|
||||||
priv, _ = DerivePrivateKeyForPath(master, ch, "0/7")
|
|
||||||
fmt.Println(hex.EncodeToString(priv[:]))
|
|
||||||
|
|
||||||
// Output: keys from fundraiser test-vector (cosmos, bitcoin, ether)
|
|
||||||
//
|
|
||||||
// bfcb217c058d8bbafd5e186eae936106ca3e943889b0b4a093ae13822fd3170c
|
|
||||||
// e77c3de76965ad89997451de97b95bb65ede23a6bf185a55d80363d92ee37c3d
|
|
||||||
// 7fc4d8a8146dea344ba04c593517d3f377fa6cded36cd55aee0a0bb968e651bc
|
|
||||||
//
|
|
||||||
// keys generated via https://coinomi.com/recovery-phrase-tool.html
|
|
||||||
//
|
|
||||||
// a61f10c5fecf40c084c94fa54273b6f5d7989386be4a37669e6d6f7b0169c163
|
|
||||||
// 32c4599843de3ef161a629a461d12c60b009b676c35050be5f7ded3a3b23501f
|
|
||||||
//
|
|
||||||
// BIP 32 example
|
|
||||||
//
|
|
||||||
// c4c11d8c03625515905d7e89d25dfc66126fbc629ecca6db489a1a72fc4bda78
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
362
keys/keybase.go
362
keys/keybase.go
@ -1,362 +0,0 @@
|
|||||||
package keys
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/tendermint/go-crypto"
|
|
||||||
"github.com/tendermint/go-crypto/keys/bip39"
|
|
||||||
"github.com/tendermint/go-crypto/keys/hd"
|
|
||||||
dbm "github.com/tendermint/tmlibs/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ Keybase = dbKeybase{}
|
|
||||||
|
|
||||||
// Language is a language to create the BIP 39 mnemonic in.
|
|
||||||
// Currently, only english is supported though.
|
|
||||||
// Find a list of all supported languages in the BIP 39 spec (word lists).
|
|
||||||
type Language int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// English is the default language to create a mnemonic.
|
|
||||||
// It is the only supported language by this package.
|
|
||||||
English Language = iota + 1
|
|
||||||
// Japanese is currently not supported.
|
|
||||||
Japanese
|
|
||||||
// Korean is currently not supported.
|
|
||||||
Korean
|
|
||||||
// Spanish is currently not supported.
|
|
||||||
Spanish
|
|
||||||
// ChineseSimplified is currently not supported.
|
|
||||||
ChineseSimplified
|
|
||||||
// ChineseTraditional is currently not supported.
|
|
||||||
ChineseTraditional
|
|
||||||
// French is currently not supported.
|
|
||||||
French
|
|
||||||
// Italian is currently not supported.
|
|
||||||
Italian
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrUnsupportedSigningAlgo is raised when the caller tries to use a different signing scheme than secp256k1.
|
|
||||||
ErrUnsupportedSigningAlgo = errors.New("unsupported signing algo: only secp256k1 is supported")
|
|
||||||
// ErrUnsupportedLanguage is raised when the caller tries to use a different language than english for creating
|
|
||||||
// a mnemonic sentence.
|
|
||||||
ErrUnsupportedLanguage = errors.New("unsupported language: only english is supported")
|
|
||||||
)
|
|
||||||
|
|
||||||
// dbKeybase combines encryption and storage implementation to provide
|
|
||||||
// a full-featured key manager
|
|
||||||
type dbKeybase struct {
|
|
||||||
db dbm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new keybase instance using the passed DB for reading and writing keys.
|
|
||||||
func New(db dbm.DB) Keybase {
|
|
||||||
return dbKeybase{
|
|
||||||
db: db,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateMnemonic generates a new key and persists it to storage, encrypted
|
|
||||||
// using the provided password.
|
|
||||||
// It returns the generated 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) CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, mnemonic string, err error) {
|
|
||||||
if language != English {
|
|
||||||
return nil, "", ErrUnsupportedLanguage
|
|
||||||
}
|
|
||||||
if algo != Secp256k1 {
|
|
||||||
err = ErrUnsupportedSigningAlgo
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// default number of words (24):
|
|
||||||
mnemonicS, err := bip39.NewMnemonic(bip39.FreshKey)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mnemonic = strings.Join(mnemonicS, " ")
|
|
||||||
seed := bip39.MnemonicToSeed(mnemonic)
|
|
||||||
info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateFundraiserKey converts a mnemonic to a private key and persists it,
|
|
||||||
// encrypted with the given password.
|
|
||||||
// TODO(ismail)
|
|
||||||
func (kb dbKeybase) CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error) {
|
|
||||||
words := strings.Split(mnemonic, " ")
|
|
||||||
if len(words) != 12 {
|
|
||||||
err = fmt.Errorf("recovering only works with 12 word (fundraiser) mnemonics, got: %v words", len(words))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seed, err := bip39.MnemonicToSeedWithErrChecking(mnemonic)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kb dbKeybase) Derive(name, mnemonic, passwd string, params hd.BIP44Params) (info Info, err error) {
|
|
||||||
seed, err := bip39.MnemonicToSeedWithErrChecking(mnemonic)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
info, err = kb.persistDerivedKey(seed, passwd, name, params.String())
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 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 SigningAlgo) (Info, error) {
|
|
||||||
if algo != Secp256k1 {
|
|
||||||
return nil, ErrUnsupportedSigningAlgo
|
|
||||||
}
|
|
||||||
priv, err := crypto.NewPrivKeyLedgerSecp256k1(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pub := priv.PubKey()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func (kb *dbKeybase) persistDerivedKey(seed []byte, passwd, name, fullHdPath string) (info Info, err error) {
|
|
||||||
// create master key and derive first key:
|
|
||||||
masterPriv, ch := hd.ComputeMastersFromSeed(seed)
|
|
||||||
derivedPriv, err := hd.DerivePrivateKeyForPath(masterPriv, ch, fullHdPath)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we have a password, use it to encrypt the private key and store it
|
|
||||||
// else store the public key only
|
|
||||||
if passwd != "" {
|
|
||||||
info = kb.writeLocalKey(crypto.PrivKeySecp256k1(derivedPriv), name, passwd)
|
|
||||||
} else {
|
|
||||||
pubk := crypto.PrivKeySecp256k1(derivedPriv).PubKey()
|
|
||||||
info = kb.writeOfflineKey(pubk, name)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// List returns the keys from storage in alphabetical order.
|
|
||||||
func (kb dbKeybase) List() ([]Info, error) {
|
|
||||||
var res []Info
|
|
||||||
iter := kb.db.Iterator(nil, nil)
|
|
||||||
defer iter.Close()
|
|
||||||
for ; iter.Valid(); iter.Next() {
|
|
||||||
info, err := readInfo(iter.Value())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res = append(res, info)
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns the public information about one key.
|
|
||||||
func (kb dbKeybase) Get(name string) (Info, error) {
|
|
||||||
bs := kb.db.Get(infoKey(name))
|
|
||||||
return readInfo(bs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign signs the msg with the named key.
|
|
||||||
// It returns an error if the key doesn't exist or the decryption fails.
|
|
||||||
func (kb dbKeybase) Sign(name, passphrase string, msg []byte) (sig crypto.Signature, pub crypto.PubKey, err error) {
|
|
||||||
info, err := kb.Get(name)
|
|
||||||
if err != nil {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
sig, err = priv.Sign(msg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
pub = priv.PubKey()
|
|
||||||
return sig, pub, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kb dbKeybase) Export(name string) (armor string, err error) {
|
|
||||||
bz := kb.db.Get(infoKey(name))
|
|
||||||
if bz == nil {
|
|
||||||
return "", fmt.Errorf("no key to export with name %s", name)
|
|
||||||
}
|
|
||||||
return armorInfoBytes(bz), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExportPubKey returns public keys in ASCII armored format.
|
|
||||||
// Retrieve a Info object by its name and return the public key in
|
|
||||||
// a portable format.
|
|
||||||
func (kb dbKeybase) ExportPubKey(name string) (armor string, err error) {
|
|
||||||
bz := kb.db.Get(infoKey(name))
|
|
||||||
if bz == nil {
|
|
||||||
return "", fmt.Errorf("no key to export with name %s", name)
|
|
||||||
}
|
|
||||||
info, err := readInfo(bz)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return armorPubKeyBytes(info.GetPubKey().Bytes()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kb dbKeybase) Import(name string, armor string) (err error) {
|
|
||||||
bz := kb.db.Get(infoKey(name))
|
|
||||||
if len(bz) > 0 {
|
|
||||||
return errors.New("Cannot overwrite data for name " + name)
|
|
||||||
}
|
|
||||||
infoBytes, err := unarmorInfoBytes(armor)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
kb.db.Set(infoKey(name), infoBytes)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportPubKey imports ASCII-armored public keys.
|
|
||||||
// Store a new Info object holding a public key only, i.e. it will
|
|
||||||
// not be possible to sign with it as it lacks the secret key.
|
|
||||||
func (kb dbKeybase) ImportPubKey(name string, armor string) (err error) {
|
|
||||||
bz := kb.db.Get(infoKey(name))
|
|
||||||
if len(bz) > 0 {
|
|
||||||
return errors.New("Cannot overwrite data for name " + name)
|
|
||||||
}
|
|
||||||
pubBytes, err := unarmorPubKeyBytes(armor)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pubKey, err := crypto.PubKeyFromBytes(pubBytes)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update changes the passphrase with which an already stored key is
|
|
||||||
// encrypted.
|
|
||||||
//
|
|
||||||
// oldpass must be the current passphrase used for encryption,
|
|
||||||
// newpass will be the only valid passphrase from this time forward.
|
|
||||||
func (kb dbKeybase) Update(name, oldpass, newpass string) error {
|
|
||||||
info, err := kb.Get(name)
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kb dbKeybase) writeLocalKey(priv crypto.PrivKey, name, passphrase string) Info {
|
|
||||||
// encrypt private key using passphrase
|
|
||||||
privArmor := encryptArmorPrivKey(priv, passphrase)
|
|
||||||
// make Info
|
|
||||||
pub := priv.PubKey()
|
|
||||||
info := newLocalInfo(name, pub, privArmor)
|
|
||||||
kb.writeInfo(info, name)
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kb dbKeybase) writeLedgerKey(pub crypto.PubKey, path crypto.DerivationPath, name string) Info {
|
|
||||||
info := newLedgerInfo(name, pub, path)
|
|
||||||
kb.writeInfo(info, name)
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kb dbKeybase) writeOfflineKey(pub crypto.PubKey, name string) Info {
|
|
||||||
info := newOfflineInfo(name, pub)
|
|
||||||
kb.writeInfo(info, name)
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kb dbKeybase) writeInfo(info Info, name string) {
|
|
||||||
// write the info by key
|
|
||||||
kb.db.SetSync(infoKey(name), writeInfo(info))
|
|
||||||
}
|
|
||||||
|
|
||||||
func infoKey(name string) []byte {
|
|
||||||
return []byte(fmt.Sprintf("%s.info", name))
|
|
||||||
}
|
|
@ -1,383 +0,0 @@
|
|||||||
package keys_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/tendermint/go-crypto"
|
|
||||||
"github.com/tendermint/go-crypto/keys"
|
|
||||||
"github.com/tendermint/go-crypto/keys/hd"
|
|
||||||
|
|
||||||
dbm "github.com/tendermint/tmlibs/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestKeyManagement makes sure we can manipulate these keys well
|
|
||||||
func TestKeyManagement(t *testing.T) {
|
|
||||||
// make the storage with reasonable defaults
|
|
||||||
cstore := keys.New(
|
|
||||||
dbm.NewMemDB(),
|
|
||||||
)
|
|
||||||
|
|
||||||
algo := keys.Secp256k1
|
|
||||||
n1, n2, n3 := "personal", "business", "other"
|
|
||||||
p1, p2 := "1234", "really-secure!@#$"
|
|
||||||
|
|
||||||
// Check empty state
|
|
||||||
l, err := cstore.List()
|
|
||||||
require.Nil(t, err)
|
|
||||||
assert.Empty(t, l)
|
|
||||||
|
|
||||||
_, _, err = cstore.CreateMnemonic(n1, keys.English, p1, keys.Ed25519)
|
|
||||||
assert.Error(t, err, "ed25519 keys are currently not supported by keybase")
|
|
||||||
|
|
||||||
// create some keys
|
|
||||||
_, err = cstore.Get(n1)
|
|
||||||
assert.Error(t, err)
|
|
||||||
i, _, err := cstore.CreateMnemonic(n1, keys.English, p1, algo)
|
|
||||||
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, n1, i.GetName())
|
|
||||||
_, _, err = cstore.CreateMnemonic(n2, keys.English, p2, algo)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// we can get these keys
|
|
||||||
i2, err := cstore.Get(n2)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
_, err = cstore.Get(n3)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
|
|
||||||
// list shows them in order
|
|
||||||
keyS, err := cstore.List()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(keyS))
|
|
||||||
// note these are in alphabetical order
|
|
||||||
assert.Equal(t, n2, keyS[0].GetName())
|
|
||||||
assert.Equal(t, n1, keyS[1].GetName())
|
|
||||||
assert.Equal(t, i2.GetPubKey(), keyS[0].GetPubKey())
|
|
||||||
|
|
||||||
// deleting a key removes it
|
|
||||||
err = cstore.Delete("bad name", "foo")
|
|
||||||
require.NotNil(t, err)
|
|
||||||
err = cstore.Delete(n1, p1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
keyS, err = cstore.List()
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, 1, len(keyS))
|
|
||||||
_, err = cstore.Get(n1)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
// create an offline key
|
|
||||||
o1 := "offline"
|
|
||||||
priv1 := crypto.GenPrivKeyEd25519()
|
|
||||||
pub1 := priv1.PubKey()
|
|
||||||
i, err = cstore.CreateOffline(o1, pub1)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, pub1, i.GetPubKey())
|
|
||||||
require.Equal(t, o1, i.GetName())
|
|
||||||
keyS, err = cstore.List()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 2, len(keyS))
|
|
||||||
|
|
||||||
// delete the offline key
|
|
||||||
err = cstore.Delete(o1, "no")
|
|
||||||
require.NotNil(t, err)
|
|
||||||
err = cstore.Delete(o1, "yes")
|
|
||||||
require.NoError(t, err)
|
|
||||||
keyS, err = cstore.List()
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, 1, len(keyS))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSignVerify does some detailed checks on how we sign and validate
|
|
||||||
// signatures
|
|
||||||
func TestSignVerify(t *testing.T) {
|
|
||||||
cstore := keys.New(
|
|
||||||
dbm.NewMemDB(),
|
|
||||||
)
|
|
||||||
algo := keys.Secp256k1
|
|
||||||
|
|
||||||
n1, n2, n3 := "some dude", "a dudette", "dude-ish"
|
|
||||||
p1, p2, p3 := "1234", "foobar", "foobar"
|
|
||||||
|
|
||||||
// create two users and get their info
|
|
||||||
i1, _, err := cstore.CreateMnemonic(n1, keys.English, p1, algo)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
i2, _, err := cstore.CreateMnemonic(n2, keys.English, p2, algo)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
// Import a public key
|
|
||||||
armor, err := cstore.ExportPubKey(n2)
|
|
||||||
require.Nil(t, err)
|
|
||||||
cstore.ImportPubKey(n3, armor)
|
|
||||||
i3, err := cstore.Get(n3)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, i3.GetName(), n3)
|
|
||||||
|
|
||||||
|
|
||||||
// let's try to sign some messages
|
|
||||||
d1 := []byte("my first message")
|
|
||||||
d2 := []byte("some other important info!")
|
|
||||||
d3 := []byte("feels like I forgot something...")
|
|
||||||
|
|
||||||
// try signing both data with both keys...
|
|
||||||
s11, pub1, err := cstore.Sign(n1, p1, d1)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, i1.GetPubKey(), pub1)
|
|
||||||
|
|
||||||
s12, pub1, err := cstore.Sign(n1, p1, d2)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, i1.GetPubKey(), pub1)
|
|
||||||
|
|
||||||
s21, pub2, err := cstore.Sign(n2, p2, d1)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, i2.GetPubKey(), pub2)
|
|
||||||
|
|
||||||
s22, pub2, err := cstore.Sign(n2, p2, d2)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.Equal(t, i2.GetPubKey(), pub2)
|
|
||||||
|
|
||||||
// let's try to validate and make sure it only works when everything is proper
|
|
||||||
cases := []struct {
|
|
||||||
key crypto.PubKey
|
|
||||||
data []byte
|
|
||||||
sig crypto.Signature
|
|
||||||
valid bool
|
|
||||||
}{
|
|
||||||
// proper matches
|
|
||||||
{i1.GetPubKey(), d1, s11, true},
|
|
||||||
// change data, pubkey, or signature leads to fail
|
|
||||||
{i1.GetPubKey(), d2, s11, false},
|
|
||||||
{i2.GetPubKey(), d1, s11, false},
|
|
||||||
{i1.GetPubKey(), d1, s21, false},
|
|
||||||
// make sure other successes
|
|
||||||
{i1.GetPubKey(), d2, s12, true},
|
|
||||||
{i2.GetPubKey(), d1, s21, true},
|
|
||||||
{i2.GetPubKey(), d2, s22, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tc := range cases {
|
|
||||||
valid := tc.key.VerifyBytes(tc.data, tc.sig)
|
|
||||||
assert.Equal(t, tc.valid, valid, "%d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now try to sign data with a secret-less key
|
|
||||||
_, _, err = cstore.Sign(n3, p3, d3)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertPassword(t *testing.T, cstore keys.Keybase, name, pass, badpass string) {
|
|
||||||
err := cstore.Update(name, badpass, pass)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
err = cstore.Update(name, pass, pass)
|
|
||||||
assert.Nil(t, err, "%+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestExportImport tests exporting and importing keys.
|
|
||||||
func TestExportImport(t *testing.T) {
|
|
||||||
|
|
||||||
// make the storage with reasonable defaults
|
|
||||||
db := dbm.NewMemDB()
|
|
||||||
cstore := keys.New(
|
|
||||||
db,
|
|
||||||
)
|
|
||||||
|
|
||||||
info, _, err := cstore.CreateMnemonic("john", keys.English,"secretcpw", keys.Secp256k1)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, info.GetName(), "john")
|
|
||||||
|
|
||||||
john, err := cstore.Get("john")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, info.GetName(), "john")
|
|
||||||
johnAddr := info.GetPubKey().Address()
|
|
||||||
|
|
||||||
armor, err := cstore.Export("john")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = cstore.Import("john2", armor)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
john2, err := cstore.Get("john2")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, john.GetPubKey().Address(), johnAddr)
|
|
||||||
assert.Equal(t, john.GetName(), "john")
|
|
||||||
assert.Equal(t, john, john2)
|
|
||||||
}
|
|
||||||
//
|
|
||||||
func TestExportImportPubKey(t *testing.T) {
|
|
||||||
// make the storage with reasonable defaults
|
|
||||||
db := dbm.NewMemDB()
|
|
||||||
cstore := keys.New(
|
|
||||||
db,
|
|
||||||
)
|
|
||||||
|
|
||||||
// CreateMnemonic a private-public key pair and ensure consistency
|
|
||||||
notPasswd := "n9y25ah7"
|
|
||||||
info, _, err := cstore.CreateMnemonic("john", keys.English, notPasswd, keys.Secp256k1)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.NotEqual(t, info, "")
|
|
||||||
assert.Equal(t, info.GetName(), "john")
|
|
||||||
addr := info.GetPubKey().Address()
|
|
||||||
john, err := cstore.Get("john")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, john.GetName(), "john")
|
|
||||||
assert.Equal(t, john.GetPubKey().Address(), addr)
|
|
||||||
|
|
||||||
// Export the public key only
|
|
||||||
armor, err := cstore.ExportPubKey("john")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
// Import it under a different name
|
|
||||||
err = cstore.ImportPubKey("john-pubkey-only", armor)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
// Ensure consistency
|
|
||||||
john2, err := cstore.Get("john-pubkey-only")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
// Compare the public keys
|
|
||||||
assert.True(t, john.GetPubKey().Equals(john2.GetPubKey()))
|
|
||||||
// Ensure the original key hasn't changed
|
|
||||||
john, err = cstore.Get("john")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, john.GetPubKey().Address(), addr)
|
|
||||||
assert.Equal(t, john.GetName(), "john")
|
|
||||||
|
|
||||||
// Ensure keys cannot be overwritten
|
|
||||||
err = cstore.ImportPubKey("john-pubkey-only", armor)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAdvancedKeyManagement verifies update, import, export functionality
|
|
||||||
func TestAdvancedKeyManagement(t *testing.T) {
|
|
||||||
|
|
||||||
// make the storage with reasonable defaults
|
|
||||||
cstore := keys.New(
|
|
||||||
dbm.NewMemDB(),
|
|
||||||
)
|
|
||||||
|
|
||||||
algo := keys.Secp256k1
|
|
||||||
n1, n2 := "old-name", "new name"
|
|
||||||
p1, p2 := "1234", "foobar"
|
|
||||||
|
|
||||||
// make sure key works with initial password
|
|
||||||
_, _, err := cstore.CreateMnemonic(n1, keys.English, p1, algo)
|
|
||||||
require.Nil(t, err, "%+v", err)
|
|
||||||
assertPassword(t, cstore, n1, p1, p2)
|
|
||||||
|
|
||||||
// update password requires the existing password
|
|
||||||
err = cstore.Update(n1, "jkkgkg", p2)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
assertPassword(t, cstore, n1, p1, p2)
|
|
||||||
|
|
||||||
// then it changes the password when correct
|
|
||||||
err = cstore.Update(n1, p1, p2)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
// p2 is now the proper one!
|
|
||||||
assertPassword(t, cstore, n1, p2, p1)
|
|
||||||
|
|
||||||
// exporting requires the proper name and passphrase
|
|
||||||
_, err = cstore.Export(n1 + ".notreal")
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
_, err = cstore.Export(" " + n1)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
_, err = cstore.Export(n1 + " ")
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
_, err = cstore.Export("")
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
exported, err := cstore.Export(n1)
|
|
||||||
require.Nil(t, err, "%+v", err)
|
|
||||||
|
|
||||||
// import succeeds
|
|
||||||
err = cstore.Import(n2, exported)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// second import fails
|
|
||||||
err = cstore.Import(n2, exported)
|
|
||||||
assert.NotNil(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestSeedPhrase verifies restoring from a seed phrase
|
|
||||||
func TestSeedPhrase(t *testing.T) {
|
|
||||||
|
|
||||||
// make the storage with reasonable defaults
|
|
||||||
cstore := keys.New(
|
|
||||||
dbm.NewMemDB(),
|
|
||||||
)
|
|
||||||
|
|
||||||
algo := keys.Secp256k1
|
|
||||||
n1, n2 := "lost-key", "found-again"
|
|
||||||
p1, p2 := "1234", "foobar"
|
|
||||||
|
|
||||||
// make sure key works with initial password
|
|
||||||
info, mnemonic, err := cstore.CreateMnemonic(n1, keys.English, p1, algo)
|
|
||||||
require.Nil(t, err, "%+v", err)
|
|
||||||
assert.Equal(t, n1, info.GetName())
|
|
||||||
assert.NotEmpty(t, mnemonic)
|
|
||||||
|
|
||||||
// now, let us delete this key
|
|
||||||
err = cstore.Delete(n1, p1)
|
|
||||||
require.Nil(t, err, "%+v", err)
|
|
||||||
_, err = cstore.Get(n1)
|
|
||||||
require.NotNil(t, err)
|
|
||||||
|
|
||||||
// let us re-create it from the mnemonic-phrase
|
|
||||||
params := *hd.NewFundraiserParams(0 ,0 )
|
|
||||||
newInfo, err := cstore.Derive(n2,mnemonic, p2, params)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, n2, newInfo.GetName())
|
|
||||||
assert.Equal(t, info.GetPubKey().Address(), newInfo.GetPubKey().Address())
|
|
||||||
assert.Equal(t, info.GetPubKey(), newInfo.GetPubKey())
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExampleNew() {
|
|
||||||
// Select the encryption and storage for your cryptostore
|
|
||||||
cstore := keys.New(
|
|
||||||
dbm.NewMemDB(),
|
|
||||||
)
|
|
||||||
|
|
||||||
sec := keys.Secp256k1
|
|
||||||
|
|
||||||
// Add keys and see they return in alphabetical order
|
|
||||||
bob, _, err := cstore.CreateMnemonic("Bob", keys.English, "friend", sec)
|
|
||||||
if err != nil {
|
|
||||||
// this should never happen
|
|
||||||
fmt.Println(err)
|
|
||||||
} else {
|
|
||||||
// return info here just like in List
|
|
||||||
fmt.Println(bob.GetName())
|
|
||||||
}
|
|
||||||
cstore.CreateMnemonic("Alice", keys.English, "secret", sec)
|
|
||||||
cstore.CreateMnemonic("Carl", keys.English, "mitm", sec)
|
|
||||||
info, _ := cstore.List()
|
|
||||||
for _, i := range info {
|
|
||||||
fmt.Println(i.GetName())
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to use passphrase to generate a signature
|
|
||||||
tx := []byte("deadbeef")
|
|
||||||
sig, pub, err := cstore.Sign("Bob", "friend", tx)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("don't accept real passphrase")
|
|
||||||
}
|
|
||||||
|
|
||||||
// and we can validate the signature with publicly available info
|
|
||||||
binfo, _ := cstore.Get("Bob")
|
|
||||||
if !binfo.GetPubKey().Equals(bob.GetPubKey()) {
|
|
||||||
fmt.Println("Get and Create return different keys")
|
|
||||||
}
|
|
||||||
|
|
||||||
if pub.Equals(binfo.GetPubKey()) {
|
|
||||||
fmt.Println("signed by Bob")
|
|
||||||
}
|
|
||||||
if !pub.VerifyBytes(tx, sig) {
|
|
||||||
fmt.Println("invalid signature")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
// Bob
|
|
||||||
// Alice
|
|
||||||
// Bob
|
|
||||||
// Carl
|
|
||||||
// signed by Bob
|
|
||||||
}
|
|
12
keys/keys.go
12
keys/keys.go
@ -1,12 +0,0 @@
|
|||||||
package keys
|
|
||||||
|
|
||||||
// SigningAlgo defines an algorithm to derive key-pairs which can be used for cryptographic signing.
|
|
||||||
type SigningAlgo string
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Secp256k1 uses the Bitcoin secp256k1 ECDSA parameters.
|
|
||||||
Secp256k1 = SigningAlgo("secp256k1")
|
|
||||||
// Ed25519 represents the Ed25519 signature system.
|
|
||||||
// It is currently not supported for end-user keys (wallets/ledgers).
|
|
||||||
Ed25519 = SigningAlgo("ed25519")
|
|
||||||
)
|
|
@ -1,2 +0,0 @@
|
|||||||
output = "text"
|
|
||||||
keydir = ".mykeys"
|
|
115
keys/mintkey.go
115
keys/mintkey.go
@ -1,115 +0,0 @@
|
|||||||
package keys
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
cmn "github.com/tendermint/tmlibs/common"
|
|
||||||
|
|
||||||
"github.com/tendermint/go-crypto"
|
|
||||||
"github.com/tendermint/go-crypto/keys/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
blockTypePrivKey = "TENDERMINT PRIVATE KEY"
|
|
||||||
blockTypeKeyInfo = "TENDERMINT KEY INFO"
|
|
||||||
blockTypePubKey = "TENDERMINT PUBLIC KEY"
|
|
||||||
)
|
|
||||||
|
|
||||||
func armorInfoBytes(bz []byte) string {
|
|
||||||
return armorBytes(bz, blockTypeKeyInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func armorPubKeyBytes(bz []byte) string {
|
|
||||||
return armorBytes(bz, blockTypePubKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func armorBytes(bz []byte, blockType string) string {
|
|
||||||
header := map[string]string{
|
|
||||||
"type": "Info",
|
|
||||||
"version": "0.0.0",
|
|
||||||
}
|
|
||||||
return crypto.EncodeArmor(blockType, header, bz)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unarmorInfoBytes(armorStr string) (bz []byte, err error) {
|
|
||||||
return unarmorBytes(armorStr, blockTypeKeyInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unarmorPubKeyBytes(armorStr string) (bz []byte, err error) {
|
|
||||||
return unarmorBytes(armorStr, blockTypePubKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unarmorBytes(armorStr, blockType string) (bz []byte, err error) {
|
|
||||||
bType, header, bz, err := crypto.DecodeArmor(armorStr)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if bType != blockType {
|
|
||||||
err = fmt.Errorf("Unrecognized armor type %q, expected: %q", bType, blockType)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if header["version"] != "0.0.0" {
|
|
||||||
err = fmt.Errorf("Unrecognized version: %v", header["version"])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptArmorPrivKey(privKey crypto.PrivKey, passphrase string) string {
|
|
||||||
saltBytes, encBytes := encryptPrivKey(privKey, passphrase)
|
|
||||||
header := map[string]string{
|
|
||||||
"kdf": "bcrypt",
|
|
||||||
"salt": fmt.Sprintf("%X", saltBytes),
|
|
||||||
}
|
|
||||||
armorStr := crypto.EncodeArmor(blockTypePrivKey, header, encBytes)
|
|
||||||
return armorStr
|
|
||||||
}
|
|
||||||
|
|
||||||
func unarmorDecryptPrivKey(armorStr string, passphrase string) (crypto.PrivKey, error) {
|
|
||||||
var privKey crypto.PrivKey
|
|
||||||
blockType, header, encBytes, err := crypto.DecodeArmor(armorStr)
|
|
||||||
if err != nil {
|
|
||||||
return privKey, err
|
|
||||||
}
|
|
||||||
if blockType != blockTypePrivKey {
|
|
||||||
return privKey, fmt.Errorf("Unrecognized armor type: %v", blockType)
|
|
||||||
}
|
|
||||||
if header["kdf"] != "bcrypt" {
|
|
||||||
return privKey, fmt.Errorf("Unrecognized KDF type: %v", header["KDF"])
|
|
||||||
}
|
|
||||||
if header["salt"] == "" {
|
|
||||||
return privKey, fmt.Errorf("Missing salt bytes")
|
|
||||||
}
|
|
||||||
saltBytes, err := hex.DecodeString(header["salt"])
|
|
||||||
if err != nil {
|
|
||||||
return privKey, fmt.Errorf("Error decoding salt: %v", err.Error())
|
|
||||||
}
|
|
||||||
privKey, err = decryptPrivKey(saltBytes, encBytes, passphrase)
|
|
||||||
return privKey, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func encryptPrivKey(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte) {
|
|
||||||
saltBytes = crypto.CRandBytes(16)
|
|
||||||
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 12) // TODO parameterize. 12 is good today (2016)
|
|
||||||
if err != nil {
|
|
||||||
cmn.Exit("Error generating bcrypt key from passphrase: " + err.Error())
|
|
||||||
}
|
|
||||||
key = crypto.Sha256(key) // Get 32 bytes
|
|
||||||
privKeyBytes := privKey.Bytes()
|
|
||||||
return saltBytes, crypto.EncryptSymmetric(privKeyBytes, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) {
|
|
||||||
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 12) // TODO parameterize. 12 is good today (2016)
|
|
||||||
if err != nil {
|
|
||||||
cmn.Exit("Error generating bcrypt key from passphrase: " + err.Error())
|
|
||||||
}
|
|
||||||
key = crypto.Sha256(key) // Get 32 bytes
|
|
||||||
privKeyBytes, err := crypto.DecryptSymmetric(encBytes, key)
|
|
||||||
if err != nil {
|
|
||||||
return privKey, err
|
|
||||||
}
|
|
||||||
privKey, err = crypto.PrivKeyFromBytes(privKeyBytes)
|
|
||||||
return privKey, err
|
|
||||||
}
|
|
142
keys/types.go
142
keys/types.go
@ -1,142 +0,0 @@
|
|||||||
package keys
|
|
||||||
|
|
||||||
import (
|
|
||||||
crypto "github.com/tendermint/go-crypto"
|
|
||||||
"github.com/tendermint/go-crypto/keys/hd"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Keybase exposes operations on a generic keystore
|
|
||||||
type Keybase interface {
|
|
||||||
|
|
||||||
// CRUD on the keystore
|
|
||||||
List() ([]Info, error)
|
|
||||||
Get(name string) (Info, error)
|
|
||||||
Delete(name, passphrase string) error
|
|
||||||
|
|
||||||
// Sign some bytes, looking up the private key to use
|
|
||||||
Sign(name, passphrase string, msg []byte) (crypto.Signature, crypto.PubKey, error)
|
|
||||||
|
|
||||||
// CreateMnemonic creates a new mnemonic, and derives a hierarchical deterministic
|
|
||||||
// key from that.
|
|
||||||
CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, seed string, err error)
|
|
||||||
// CreateFundraiserKey takes a mnemonic and derives, a password
|
|
||||||
CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error)
|
|
||||||
// Derive derives a key from the passed mnemonic using a BIP44 path.
|
|
||||||
Derive(name, mnemonic, passwd string, params hd.BIP44Params) (Info, error)
|
|
||||||
// Create, store, and return a new Ledger key reference
|
|
||||||
CreateLedger(name string, path crypto.DerivationPath, algo SigningAlgo) (info Info, err error)
|
|
||||||
|
|
||||||
// Create, store, and return a new offline key reference
|
|
||||||
CreateOffline(name string, pubkey crypto.PubKey) (info Info, err error)
|
|
||||||
|
|
||||||
// The following operations will *only* work on locally-stored keys
|
|
||||||
Update(name, oldpass, newpass string) error
|
|
||||||
Import(name string, armor string) (err error)
|
|
||||||
ImportPubKey(name string, armor string) (err error)
|
|
||||||
Export(name string) (armor string, err error)
|
|
||||||
ExportPubKey(name string) (armor string, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Info is the publicly exposed information about a keypair
|
|
||||||
type Info interface {
|
|
||||||
// Human-readable type for key listing
|
|
||||||
GetType() string
|
|
||||||
// Name of the key
|
|
||||||
GetName() string
|
|
||||||
// Public key
|
|
||||||
GetPubKey() crypto.PubKey
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Info = &localInfo{}
|
|
||||||
var _ Info = &ledgerInfo{}
|
|
||||||
var _ Info = &offlineInfo{}
|
|
||||||
|
|
||||||
// localInfo is the public information about a locally stored key
|
|
||||||
type localInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
PubKey crypto.PubKey `json:"pubkey"`
|
|
||||||
PrivKeyArmor string `json:"privkey.armor"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLocalInfo(name string, pub crypto.PubKey, privArmor string) Info {
|
|
||||||
return &localInfo{
|
|
||||||
Name: name,
|
|
||||||
PubKey: pub,
|
|
||||||
PrivKeyArmor: privArmor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i localInfo) GetType() string {
|
|
||||||
return "local"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i localInfo) GetName() string {
|
|
||||||
return i.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i localInfo) GetPubKey() crypto.PubKey {
|
|
||||||
return i.PubKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// ledgerInfo is the public information about a Ledger key
|
|
||||||
type ledgerInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
PubKey crypto.PubKey `json:"pubkey"`
|
|
||||||
Path crypto.DerivationPath `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLedgerInfo(name string, pub crypto.PubKey, path crypto.DerivationPath) Info {
|
|
||||||
return &ledgerInfo{
|
|
||||||
Name: name,
|
|
||||||
PubKey: pub,
|
|
||||||
Path: path,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i ledgerInfo) GetType() string {
|
|
||||||
return "ledger"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i ledgerInfo) GetName() string {
|
|
||||||
return i.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i ledgerInfo) GetPubKey() crypto.PubKey {
|
|
||||||
return i.PubKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// offlineInfo is the public information about an offline key
|
|
||||||
type offlineInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
PubKey crypto.PubKey `json:"pubkey"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOfflineInfo(name string, pub crypto.PubKey) Info {
|
|
||||||
return &offlineInfo{
|
|
||||||
Name: name,
|
|
||||||
PubKey: pub,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i offlineInfo) GetType() string {
|
|
||||||
return "offline"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i offlineInfo) GetName() string {
|
|
||||||
return i.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i offlineInfo) GetPubKey() crypto.PubKey {
|
|
||||||
return i.PubKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// encoding info
|
|
||||||
func writeInfo(i Info) []byte {
|
|
||||||
return cdc.MustMarshalBinary(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// decoding info
|
|
||||||
func readInfo(bz []byte) (info Info, err error) {
|
|
||||||
err = cdc.UnmarshalBinary(bz, &info)
|
|
||||||
return
|
|
||||||
}
|
|
16
keys/wire.go
16
keys/wire.go
@ -1,16 +0,0 @@
|
|||||||
package keys
|
|
||||||
|
|
||||||
import (
|
|
||||||
amino "github.com/tendermint/go-amino"
|
|
||||||
crypto "github.com/tendermint/go-crypto"
|
|
||||||
)
|
|
||||||
|
|
||||||
var cdc = amino.NewCodec()
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
crypto.RegisterAmino(cdc)
|
|
||||||
cdc.RegisterInterface((*Info)(nil), nil)
|
|
||||||
cdc.RegisterConcrete(localInfo{}, "crypto/keys/localInfo", nil)
|
|
||||||
cdc.RegisterConcrete(ledgerInfo{}, "crypto/keys/ledgerInfo", nil)
|
|
||||||
cdc.RegisterConcrete(offlineInfo{}, "crypto/keys/offlineInfo", nil)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package crypto
|
|
||||||
|
|
||||||
import (
|
|
||||||
ledger "github.com/zondax/ledger-goclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
var device *ledger.Ledger
|
|
||||||
|
|
||||||
// Ledger derivation path
|
|
||||||
type DerivationPath = []uint32
|
|
||||||
|
|
||||||
// getLedger gets a copy of the device, and caches it
|
|
||||||
func getLedger() (*ledger.Ledger, error) {
|
|
||||||
var err error
|
|
||||||
if device == nil {
|
|
||||||
device, err = ledger.FindLedger()
|
|
||||||
}
|
|
||||||
return device, err
|
|
||||||
}
|
|
@ -1,124 +0,0 @@
|
|||||||
package crypto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
secp256k1 "github.com/btcsuite/btcd/btcec"
|
|
||||||
ledger "github.com/zondax/ledger-goclient"
|
|
||||||
)
|
|
||||||
|
|
||||||
func pubkeyLedgerSecp256k1(device *ledger.Ledger, path DerivationPath) (pub PubKey, err error) {
|
|
||||||
key, err := device.GetPublicKeySECP256K1(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching public key: %v", err)
|
|
||||||
}
|
|
||||||
var p PubKeySecp256k1
|
|
||||||
// Reserialize in the 33-byte compressed format
|
|
||||||
cmp, err := secp256k1.ParsePubKey(key[:], secp256k1.S256())
|
|
||||||
copy(p[:], cmp.SerializeCompressed())
|
|
||||||
pub = p
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func signLedgerSecp256k1(device *ledger.Ledger, path DerivationPath, msg []byte) (sig Signature, err error) {
|
|
||||||
bsig, err := device.SignSECP256K1(path, msg)
|
|
||||||
if err != nil {
|
|
||||||
return sig, err
|
|
||||||
}
|
|
||||||
sig = SignatureSecp256k1FromBytes(bsig)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// PrivKeyLedgerSecp256k1 implements PrivKey, calling the ledger nano
|
|
||||||
// we cache the PubKey from the first call to use it later
|
|
||||||
type PrivKeyLedgerSecp256k1 struct {
|
|
||||||
// PubKey should be private, but we want to encode it via go-amino
|
|
||||||
// so we can view the address later, even without having the ledger
|
|
||||||
// attached
|
|
||||||
CachedPubKey PubKey
|
|
||||||
Path DerivationPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPrivKeyLedgerSecp256k1 will generate a new key and store the
|
|
||||||
// public key for later use.
|
|
||||||
func NewPrivKeyLedgerSecp256k1(path DerivationPath) (PrivKey, error) {
|
|
||||||
var pk PrivKeyLedgerSecp256k1
|
|
||||||
pk.Path = path
|
|
||||||
// cache the pubkey for later use
|
|
||||||
pubKey, err := pk.getPubKey()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
pk.CachedPubKey = pubKey
|
|
||||||
return &pk, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateKey allows us to verify the sanity of a key
|
|
||||||
// after loading it from disk
|
|
||||||
func (pk PrivKeyLedgerSecp256k1) ValidateKey() error {
|
|
||||||
// getPubKey will return an error if the ledger is not
|
|
||||||
pub, err := pk.getPubKey()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// verify this matches cached address
|
|
||||||
if !pub.Equals(pk.CachedPubKey) {
|
|
||||||
return fmt.Errorf("cached key does not match retrieved key")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssertIsPrivKeyInner fulfils PrivKey Interface
|
|
||||||
func (pk *PrivKeyLedgerSecp256k1) AssertIsPrivKeyInner() {}
|
|
||||||
|
|
||||||
// Bytes fulfils PrivKey Interface - but it stores the cached pubkey so we can verify
|
|
||||||
// the same key when we reconnect to a ledger
|
|
||||||
func (pk PrivKeyLedgerSecp256k1) Bytes() []byte {
|
|
||||||
return cdc.MustMarshalBinaryBare(pk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign calls the ledger and stores the PubKey for future use
|
|
||||||
//
|
|
||||||
// Communication is checked on NewPrivKeyLedger and PrivKeyFromBytes,
|
|
||||||
// returning an error, so this should only trigger if the privkey is held
|
|
||||||
// in memory for a while before use.
|
|
||||||
func (pk PrivKeyLedgerSecp256k1) Sign(msg []byte) (Signature, error) {
|
|
||||||
dev, err := getLedger()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sig, err := signLedgerSecp256k1(dev, pk.Path, msg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return sig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PubKey returns the stored PubKey
|
|
||||||
func (pk PrivKeyLedgerSecp256k1) PubKey() PubKey {
|
|
||||||
return pk.CachedPubKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPubKey reads the pubkey the ledger itself
|
|
||||||
// since this involves IO, it may return an error, which is not exposed
|
|
||||||
// in the PubKey interface, so this function allows better error handling
|
|
||||||
func (pk PrivKeyLedgerSecp256k1) getPubKey() (key PubKey, err error) {
|
|
||||||
dev, err := getLedger()
|
|
||||||
if err != nil {
|
|
||||||
return key, fmt.Errorf("cannot connect to Ledger device - error: %v", err)
|
|
||||||
}
|
|
||||||
key, err = pubkeyLedgerSecp256k1(dev, pk.Path)
|
|
||||||
if err != nil {
|
|
||||||
return key, fmt.Errorf("please open Cosmos app on the Ledger device - error: %v", err)
|
|
||||||
}
|
|
||||||
return key, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Equals fulfils PrivKey Interface - makes sure both keys refer to the
|
|
||||||
// same
|
|
||||||
func (pk PrivKeyLedgerSecp256k1) Equals(other PrivKey) bool {
|
|
||||||
if ledger, ok := other.(*PrivKeyLedgerSecp256k1); ok {
|
|
||||||
return pk.CachedPubKey.Equals(ledger.CachedPubKey)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
package crypto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRealLedgerSecp256k1(t *testing.T) {
|
|
||||||
|
|
||||||
if os.Getenv("WITH_LEDGER") == "" {
|
|
||||||
t.Skip("Set WITH_LEDGER to run code on real ledger")
|
|
||||||
}
|
|
||||||
msg := []byte("kuhehfeohg")
|
|
||||||
|
|
||||||
path := DerivationPath{44, 60, 0, 0, 0}
|
|
||||||
|
|
||||||
priv, err := NewPrivKeyLedgerSecp256k1(path)
|
|
||||||
require.Nil(t, err, "%+v", err)
|
|
||||||
pub := priv.PubKey()
|
|
||||||
sig, err := priv.Sign(msg)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
valid := pub.VerifyBytes(msg, sig)
|
|
||||||
assert.True(t, valid)
|
|
||||||
|
|
||||||
// now, let's serialize the key and make sure it still works
|
|
||||||
bs := priv.Bytes()
|
|
||||||
priv2, err := PrivKeyFromBytes(bs)
|
|
||||||
require.Nil(t, err, "%+v", err)
|
|
||||||
|
|
||||||
// make sure we get the same pubkey when we load from disk
|
|
||||||
pub2 := priv2.PubKey()
|
|
||||||
require.Equal(t, pub, pub2)
|
|
||||||
|
|
||||||
// signing with the loaded key should match the original pubkey
|
|
||||||
sig, err = priv2.Sign(msg)
|
|
||||||
require.Nil(t, err)
|
|
||||||
valid = pub.VerifyBytes(msg, sig)
|
|
||||||
assert.True(t, valid)
|
|
||||||
|
|
||||||
// make sure pubkeys serialize properly as well
|
|
||||||
bs = pub.Bytes()
|
|
||||||
bpub, err := PubKeyFromBytes(bs)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, pub, bpub)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRealLedgerErrorHandling calls. These tests assume
|
|
||||||
// the ledger is not plugged in....
|
|
||||||
func TestRealLedgerErrorHandling(t *testing.T) {
|
|
||||||
if os.Getenv("WITH_LEDGER") != "" {
|
|
||||||
t.Skip("Skipping on WITH_LEDGER as it tests unplugged cases")
|
|
||||||
}
|
|
||||||
|
|
||||||
// first, try to generate a key, must return an error
|
|
||||||
// (no panic)
|
|
||||||
path := DerivationPath{44, 60, 0, 0, 0}
|
|
||||||
_, err := NewPrivKeyLedgerSecp256k1(path)
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user