From 945798f913d5cabd79635f45045b680b02396bf9 Mon Sep 17 00:00:00 2001 From: Gustav Simonsson Date: Wed, 31 Dec 2014 15:39:33 +0100 Subject: [PATCH 1/6] Add new key_store interface and two new key stores * Add new generic key_store interface * Add new plaintext key store storing unprotected keys on disk * Add new encrypted key store storing encrypted keys on disk * Add new entropy mixing function using OS and go runtime sources --- .gitignore | 5 + crypto/key.go | 205 ++++++++++++++++++++ crypto/key_store_passphrase_encryped.go | 244 ++++++++++++++++++++++++ crypto/key_store_plaintext_file.go | 113 +++++++++++ crypto/key_store_test.go | 118 ++++++++++++ 5 files changed, 685 insertions(+) create mode 100644 crypto/key.go create mode 100644 crypto/key_store_passphrase_encryped.go create mode 100644 crypto/key_store_plaintext_file.go create mode 100644 crypto/key_store_test.go diff --git a/.gitignore b/.gitignore index fea7df6c2d..f84fe60409 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ .DS_Store */**/.DS_Store .ethtest + +#* +.#* +*# +*~ diff --git a/crypto/key.go b/crypto/key.go new file mode 100644 index 0000000000..d4845ee226 --- /dev/null +++ b/crypto/key.go @@ -0,0 +1,205 @@ +/* + This file is part of go-ethereum + + go-ethereum is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + go-ethereum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with go-ethereum. If not, see . +*/ +/** + * @authors + * Gustav Simonsson + * @date 2015 + * + */ + +package crypto + +import ( + "bytes" + "code.google.com/p/go-uuid/uuid" + "crypto/ecdsa" + "crypto/elliptic" + crand "crypto/rand" + "encoding/binary" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "runtime" + "strings" + "time" +) + +type Key struct { + Id *uuid.UUID // Version 4 "random" for unique id not derived from key data + Flags [4]byte // RFU + // we only store privkey as pubkey/address can be derived from it + // privkey in this struct is always in plaintext + PrivateKey *ecdsa.PrivateKey +} + +type KeyPlainJSON struct { + Id string + Flags string + PrivateKey string +} + +type CipherJSON struct { + Salt string + IV string + CipherText string +} + +type KeyProtectedJSON struct { + Id string + Flags string + Crypto CipherJSON +} + +func (k *Key) Address() []byte { + pubBytes := FromECDSAPub(&k.PrivateKey.PublicKey) + return Sha3(pubBytes)[12:] +} + +func (k *Key) MarshalJSON() (j []byte, err error) { + stringStruct := KeyPlainJSON{ + k.Id.String(), + hex.EncodeToString(k.Flags[:]), + hex.EncodeToString(FromECDSA(k.PrivateKey)), + } + j, _ = json.Marshal(stringStruct) + return +} + +func (k *Key) UnmarshalJSON(j []byte) (err error) { + keyJSON := new(KeyPlainJSON) + err = json.Unmarshal(j, &keyJSON) + if err != nil { + return + } + + u := new(uuid.UUID) + *u = uuid.Parse(keyJSON.Id) + if *u == nil { + err = errors.New("UUID parsing failed") + return + } + k.Id = u + + flagsBytes, err := hex.DecodeString(keyJSON.Flags) + if err != nil { + return + } + + PrivateKeyBytes, err := hex.DecodeString(keyJSON.PrivateKey) + if err != nil { + return + } + + copy(k.Flags[:], flagsBytes[0:4]) + k.PrivateKey = ToECDSA(PrivateKeyBytes) + + return +} + +func NewKey() *Key { + randBytes := GetEntropyCSPRNG(32) + reader := bytes.NewReader(randBytes) + _, x, y, err := elliptic.GenerateKey(S256(), reader) + if err != nil { + panic("key generation: elliptic.GenerateKey failed: " + err.Error()) + } + privateKeyMarshalled := elliptic.Marshal(S256(), x, y) + privateKeyECDSA := ToECDSA(privateKeyMarshalled) + + key := new(Key) + id := uuid.NewRandom() + key.Id = &id + // flags := new([4]byte) + // key.Flags = flags + key.PrivateKey = privateKeyECDSA + return key +} + +// plain crypto/rand. this is /dev/urandom on Unix-like systems. +func GetEntropyCSPRNG(n int) []byte { + mainBuff := make([]byte, n) + _, err := io.ReadFull(crand.Reader, mainBuff) + if err != nil { + panic("key generation: reading from crypto/rand failed: " + err.Error()) + } + return mainBuff +} + +// TODO: verify. Do not use until properly discussed. +// we start with crypt/rand, then mix in additional sources of entropy. +// These sources are from three types: OS, go runtime and ethereum client state. +func GetEntropyTinFoilHat() []byte { + startTime := time.Now().UnixNano() + // for each source, we XOR in it's SHA3 hash. + mainBuff := GetEntropyCSPRNG(32) + // 1. OS entropy sources + startTimeBytes := make([]byte, 32) + binary.PutVarint(startTimeBytes, startTime) + startTimeHash := Sha3(startTimeBytes) + mix32Byte(mainBuff, startTimeHash) + + pid := os.Getpid() + pidBytes := make([]byte, 32) + binary.PutUvarint(pidBytes, uint64(pid)) + pidHash := Sha3(pidBytes) + mix32Byte(mainBuff, pidHash) + + osEnv := os.Environ() + osEnvBytes := []byte(strings.Join(osEnv, "")) + osEnvHash := Sha3(osEnvBytes) + mix32Byte(mainBuff, osEnvHash) + + // not all OS have hostname in env variables + osHostName, err := os.Hostname() + if err != nil { + osHostNameBytes := []byte(osHostName) + osHostNameHash := Sha3(osHostNameBytes) + mix32Byte(mainBuff, osHostNameHash) + } + + // 2. go runtime entropy sources + memStats := new(runtime.MemStats) + runtime.ReadMemStats(memStats) + memStatsBytes := []byte(fmt.Sprintf("%v", memStats)) + memStatsHash := Sha3(memStatsBytes) + mix32Byte(mainBuff, memStatsHash) + + // 3. Mix in ethereum / client state + // TODO: list of network peers structs (IP, port, etc) + // TODO: merkle patricia tree root hash for world state and tx list + + // 4. Yo dawg we heard you like entropy so we'll grab some entropy from how + // long it took to grab the above entropy. And a yield, for good measure. + runtime.Gosched() + diffTime := time.Now().UnixNano() - startTime + diffTimeBytes := make([]byte, 32) + binary.PutVarint(diffTimeBytes, diffTime) + diffTimeHash := Sha3(diffTimeBytes) + mix32Byte(mainBuff, diffTimeHash) + + return mainBuff +} + +func mix32Byte(buff []byte, mixBuff []byte) []byte { + for i := 0; i < 32; i++ { + buff[i] ^= mixBuff[i] + } + return buff +} diff --git a/crypto/key_store_passphrase_encryped.go b/crypto/key_store_passphrase_encryped.go new file mode 100644 index 0000000000..b17536c3f4 --- /dev/null +++ b/crypto/key_store_passphrase_encryped.go @@ -0,0 +1,244 @@ +/* + This file is part of go-ethereum + + go-ethereum is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + go-ethereum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with go-ethereum. If not, see . +*/ +/** + * @authors + * Gustav Simonsson + * @date 2015 + * + */ +/* + +This key store behaves as KeyStorePlaintextFile with the difference that +the private key is encrypted and encoded as a JSON object within the +key JSON object. + +Cryptography: + +1. Encryption key is scrypt derived key from user passphrase. Scrypt parameters + (work factors) [1][2] are defined as constants below. +2. Scrypt salt is 32 random bytes from CSPRNG. It is appended to ciphertext. +3. Checksum is SHA3 of the private key bytes. +4. Plaintext is concatenation of private key bytes and checksum. +5. Encryption algo is AES 256 CBC [3][4] +6. CBC IV is 16 random bytes from CSPRNG. It is appended to ciphertext. +7. Plaintext padding is PKCS #7 [5][6] + +Encoding: + +1. On disk, ciphertext, salt and IV are encoded as a JSON object. + cat a key file to see the structure. +2. byte arrays are ASCII HEX encoded as JSON strings. +3. The EC private key bytes are in uncompressed form [7]. + They are a big-endian byte slice of the absolute value of D [8][9]. +4. The checksum is the last 32 bytes of the plaintext byte array and the + private key is the preceeding bytes. + +References: + +1. http://www.tarsnap.com/scrypt/scrypt-slides.pdf +2. http://stackoverflow.com/questions/11126315/what-are-optimal-scrypt-work-factors +3. http://en.wikipedia.org/wiki/Advanced_Encryption_Standard +4. http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher-block_chaining_.28CBC.29 +5. https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes +6. http://tools.ietf.org/html/rfc2315 +7. http://bitcoin.stackexchange.com/questions/3059/what-is-a-compressed-bitcoin-key +8. http://golang.org/pkg/crypto/ecdsa/#PrivateKey +9. https://golang.org/pkg/math/big/#Int.Bytes + +*/ + +package crypto + +import ( + "bytes" + "code.google.com/p/go-uuid/uuid" + "code.google.com/p/go.crypto/scrypt" + "crypto/aes" + "crypto/cipher" + "encoding/hex" + "encoding/json" + "errors" + "os" + "path" +) + +const scryptN int = 262144 // 2^18 +const scryptr int = 8 +const scryptp int = 1 +const scryptdkLen int = 32 + +type KeyStorePassphrase struct { + keysDirPath string +} + +func (ks KeyStorePassphrase) GenerateNewKey(auth string) (key *Key, err error) { + key, err = GenerateNewKeyDefault(ks, auth) + return +} + +func (ks KeyStorePassphrase) GetKey(keyId *uuid.UUID, auth string) (key *Key, err error) { + keyBytes, flags, err := DecryptKey(ks, keyId, auth) + key = new(Key) + key.Id = keyId + copy(key.Flags[:], flags[0:4]) + key.PrivateKey = ToECDSA(keyBytes) + return +} + +func (ks KeyStorePassphrase) StoreKey(key *Key, auth string) (err error) { + authArray := []byte(auth) + salt := GetEntropyCSPRNG(32) + derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptr, scryptp, scryptdkLen) + if err != nil { + return + } + + keyBytes := FromECDSA(key.PrivateKey) + keyBytesHash := Sha3(keyBytes) + toEncrypt := PKCS7Pad(append(keyBytes, keyBytesHash...)) + + AES256Block, err := aes.NewCipher(derivedKey) + if err != nil { + return + } + + iv := GetEntropyCSPRNG(aes.BlockSize) // 16 + AES256CBCEncrypter := cipher.NewCBCEncrypter(AES256Block, iv) + cipherText := make([]byte, len(toEncrypt)) + AES256CBCEncrypter.CryptBlocks(cipherText, toEncrypt) + + cipherStruct := CipherJSON{ + hex.EncodeToString(salt), + hex.EncodeToString(iv), + hex.EncodeToString(cipherText), + } + keyStruct := KeyProtectedJSON{ + key.Id.String(), + hex.EncodeToString(key.Flags[:]), + cipherStruct, + } + keyJSON, err := json.Marshal(keyStruct) + if err != nil { + return + } + + err = WriteKeyFile(key.Id.String(), ks.keysDirPath, keyJSON) + return +} + +func (ks KeyStorePassphrase) DeleteKey(keyId *uuid.UUID, auth string) (err error) { + // only delete if correct passphrase is given + _, _, err = DecryptKey(ks, keyId, auth) + if err != nil { + return + } + + keyDirPath := path.Join(ks.keysDirPath, keyId.String()) + err = os.RemoveAll(keyDirPath) + return +} + +func DecryptKey(ks KeyStorePassphrase, keyId *uuid.UUID, auth string) (keyBytes []byte, flags []byte, err error) { + fileContent, err := GetKeyFile(ks.keysDirPath, keyId) + if err != nil { + return + } + + keyProtected := new(KeyProtectedJSON) + err = json.Unmarshal(fileContent, keyProtected) + + flags, err = hex.DecodeString(keyProtected.Flags) + if err != nil { + return + } + + salt, err := hex.DecodeString(keyProtected.Crypto.Salt) + if err != nil { + return + } + + iv, err := hex.DecodeString(keyProtected.Crypto.IV) + if err != nil { + return + } + + cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) + if err != nil { + return + } + + authArray := []byte(auth) + derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptr, scryptp, scryptdkLen) + if err != nil { + return + } + + AES256Block, err := aes.NewCipher(derivedKey) + if err != nil { + return + } + + AES256CBCDecrypter := cipher.NewCBCDecrypter(AES256Block, iv) + paddedPlainText := make([]byte, len(cipherText)) + AES256CBCDecrypter.CryptBlocks(paddedPlainText, cipherText) + + plainText := PKCS7Unpad(paddedPlainText) + if plainText == nil { + err = errors.New("Decryption failed: PKCS7Unpad failed after decryption") + return + } + + keyBytes = plainText[:len(plainText)-32] + keyBytesHash := plainText[len(plainText)-32:] + if !bytes.Equal(Sha3(keyBytes), keyBytesHash) { + err = errors.New("Decryption failed: checksum mismatch") + return + } + return keyBytes, flags, err +} + +// From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes +func PKCS7Pad(in []byte) []byte { + padding := 16 - (len(in) % 16) + if padding == 0 { + padding = 16 + } + for i := 0; i < padding; i++ { + in = append(in, byte(padding)) + } + return in +} + +func PKCS7Unpad(in []byte) []byte { + if len(in) == 0 { + return nil + } + + padding := in[len(in)-1] + if int(padding) > len(in) || padding > aes.BlockSize { + return nil + } else if padding == 0 { + return nil + } + + for i := len(in) - 1; i > len(in)-int(padding)-1; i-- { + if in[i] != padding { + return nil + } + } + return in[:len(in)-int(padding)] +} diff --git a/crypto/key_store_plaintext_file.go b/crypto/key_store_plaintext_file.go new file mode 100644 index 0000000000..7ca5e56791 --- /dev/null +++ b/crypto/key_store_plaintext_file.go @@ -0,0 +1,113 @@ +/* + This file is part of go-ethereum + + go-ethereum is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + go-ethereum is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with go-ethereum. If not, see . +*/ +/** + * @authors + * Gustav Simonsson + * @date 2015 + * + */ + +package crypto + +import ( + "code.google.com/p/go-uuid/uuid" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/user" + "path" +) + +// TODO: rename to KeyStore when replacing existing KeyStore +type KeyStore2 interface { + GenerateNewKey(string) (*Key, error) // create and store new key, optionally using auth string + GetKey(*uuid.UUID, string) (*Key, error) // key from id and auth string + StoreKey(*Key, string) error // store key optionally using auth string + DeleteKey(*uuid.UUID, string) error // delete key by id and auth string +} + +type KeyStorePlaintext struct { + keysDirPath string +} + +// TODO: copied from cmd/ethereum/flags.go +func DefaultDataDir() string { + usr, _ := user.Current() + return path.Join(usr.HomeDir, ".ethereum") +} + +func (ks KeyStorePlaintext) GenerateNewKey(auth string) (key *Key, err error) { + key, err = GenerateNewKeyDefault(ks, auth) + return +} + +func GenerateNewKeyDefault(ks KeyStore2, auth string) (key *Key, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("GenerateNewKey error: %v", r) + } + }() + key = NewKey() + err = ks.StoreKey(key, auth) + return +} + +func (ks KeyStorePlaintext) GetKey(keyId *uuid.UUID, auth string) (key *Key, err error) { + fileContent, err := GetKeyFile(ks.keysDirPath, keyId) + if err != nil { + return + } + + key = new(Key) + err = json.Unmarshal(fileContent, key) + return +} + +func (ks KeyStorePlaintext) StoreKey(key *Key, auth string) (err error) { + keyJSON, err := json.Marshal(key) + if err != nil { + return + } + err = WriteKeyFile(key.Id.String(), ks.keysDirPath, keyJSON) + return +} + +func (ks KeyStorePlaintext) DeleteKey(keyId *uuid.UUID, auth string) (err error) { + keyDirPath := path.Join(ks.keysDirPath, keyId.String()) + err = os.RemoveAll(keyDirPath) + return +} + +func GetKeyFile(keysDirPath string, keyId *uuid.UUID) (fileContent []byte, err error) { + idString := keyId.String() + keyDirPath := path.Join(keysDirPath, idString) + keyFilePath := path.Join(keyDirPath, idString) + fileContent, err = ioutil.ReadFile(keyFilePath) + return +} + +func WriteKeyFile(idString string, keysDirPath string, content []byte) (err error) { + keyDirPath := path.Join(keysDirPath, idString) + keyFilePath := path.Join(keyDirPath, idString) + err = os.MkdirAll(keyDirPath, 0700) // read, write and dir search for user + if err != nil { + return + } + err = ioutil.WriteFile(keyFilePath, content, 0600) // read, write for user + return +} diff --git a/crypto/key_store_test.go b/crypto/key_store_test.go new file mode 100644 index 0000000000..4127354447 --- /dev/null +++ b/crypto/key_store_test.go @@ -0,0 +1,118 @@ +package crypto + +import ( + "fmt" + "reflect" + "testing" +) + +func TestKeyStorePlaintext(t *testing.T) { + ks := new(KeyStorePlaintext) + ks.keysDirPath = DefaultDataDir() + pass := "" // not used but required by API + k1, err := ks.GenerateNewKey(pass) + if err != nil { + fmt.Println(err.Error()) + t.FailNow() + } + + k2 := new(Key) + k2, err = ks.GetKey(k1.Id, pass) + if err != nil { + fmt.Println(err.Error()) + t.FailNow() + } + + if !reflect.DeepEqual(k1.Id, k2.Id) { + fmt.Println("key Id mismatch") + t.FailNow() + } + + if k1.Flags != k2.Flags { + fmt.Println("key Flags mismatch") + t.FailNow() + } + + if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { + fmt.Println("key PrivateKey mismatch") + t.FailNow() + } + + err = ks.DeleteKey(k2.Id, pass) + if err != nil { + fmt.Println(err.Error()) + t.FailNow() + } +} + +func TestKeyStorePassphrase(t *testing.T) { + ks := new(KeyStorePassphrase) + ks.keysDirPath = DefaultDataDir() + pass := "foo" + k1, err := ks.GenerateNewKey(pass) + if err != nil { + fmt.Println(err.Error()) + t.FailNow() + } + + k2 := new(Key) + k2, err = ks.GetKey(k1.Id, pass) + if err != nil { + fmt.Println(err.Error()) + t.FailNow() + } + + if !reflect.DeepEqual(k1.Id, k2.Id) { + fmt.Println("key Id mismatch") + t.FailNow() + } + + if k1.Flags != k2.Flags { + fmt.Println("key Flags mismatch") + t.FailNow() + } + + if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { + fmt.Println("key PrivateKey mismatch") + t.FailNow() + } + + err = ks.DeleteKey(k2.Id, pass) // also to clean up created files + if err != nil { + fmt.Println(err.Error()) + t.FailNow() + } +} + +func TestKeyStorePassphraseDecryptionFail(t *testing.T) { + ks := new(KeyStorePassphrase) + ks.keysDirPath = DefaultDataDir() + pass := "foo" + k1, err := ks.GenerateNewKey(pass) + if err != nil { + fmt.Println(err.Error()) + t.FailNow() + } + + _, err = ks.GetKey(k1.Id, "bar") // wrong passphrase + // fmt.Println(err.Error()) + if err == nil { + t.FailNow() + } + + err = ks.DeleteKey(k1.Id, "bar") // wrong passphrase + if err == nil { + fmt.Println(err.Error()) + t.FailNow() + } + + err = ks.DeleteKey(k1.Id, pass) // to clean up + if err != nil { + fmt.Println(err.Error()) + t.FailNow() + } +} + +func TestKeyMixedEntropy(t *testing.T) { + GetEntropyTinFoilHat() +} From a1c2749380523178f87ae3fdfb02bc6641362924 Mon Sep 17 00:00:00 2001 From: Gustav Simonsson Date: Wed, 7 Jan 2015 16:06:26 +0100 Subject: [PATCH 2/6] Address pull request comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Simplify scrypt constants with const block * Add key store constructors and make their types private * Simplify key store and file namings to be less Java Enterpriseā„¢ * Change test error logging to use t.Error(err) * Reduce number of naked returns (just like my ex-gf) * Simplify file reading path code --- crypto/key.go | 22 +++--- ...se_encryped.go => key_store_passphrase.go} | 73 +++++++++++-------- ...e_plaintext_file.go => key_store_plain.go} | 49 +++++++------ crypto/key_store_test.go | 31 ++++---- 4 files changed, 91 insertions(+), 84 deletions(-) rename crypto/{key_store_passphrase_encryped.go => key_store_passphrase.go} (83%) rename crypto/{key_store_plaintext_file.go => key_store_plain.go} (71%) diff --git a/crypto/key.go b/crypto/key.go index d4845ee226..1a8b770e00 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -49,7 +49,7 @@ type Key struct { PrivateKey *ecdsa.PrivateKey } -type KeyPlainJSON struct { +type PlainKeyJSON struct { Id string Flags string PrivateKey string @@ -61,7 +61,7 @@ type CipherJSON struct { CipherText string } -type KeyProtectedJSON struct { +type EncryptedKeyJSON struct { Id string Flags string Crypto CipherJSON @@ -73,44 +73,44 @@ func (k *Key) Address() []byte { } func (k *Key) MarshalJSON() (j []byte, err error) { - stringStruct := KeyPlainJSON{ + stringStruct := PlainKeyJSON{ k.Id.String(), hex.EncodeToString(k.Flags[:]), hex.EncodeToString(FromECDSA(k.PrivateKey)), } - j, _ = json.Marshal(stringStruct) - return + j, err = json.Marshal(stringStruct) + return j, err } func (k *Key) UnmarshalJSON(j []byte) (err error) { - keyJSON := new(KeyPlainJSON) + keyJSON := new(PlainKeyJSON) err = json.Unmarshal(j, &keyJSON) if err != nil { - return + return err } u := new(uuid.UUID) *u = uuid.Parse(keyJSON.Id) if *u == nil { err = errors.New("UUID parsing failed") - return + return err } k.Id = u flagsBytes, err := hex.DecodeString(keyJSON.Flags) if err != nil { - return + return err } PrivateKeyBytes, err := hex.DecodeString(keyJSON.PrivateKey) if err != nil { - return + return err } copy(k.Flags[:], flagsBytes[0:4]) k.PrivateKey = ToECDSA(PrivateKeyBytes) - return + return err } func NewKey() *Key { diff --git a/crypto/key_store_passphrase_encryped.go b/crypto/key_store_passphrase.go similarity index 83% rename from crypto/key_store_passphrase_encryped.go rename to crypto/key_store_passphrase.go index b17536c3f4..eaf73422f6 100644 --- a/crypto/key_store_passphrase_encryped.go +++ b/crypto/key_store_passphrase.go @@ -76,35 +76,46 @@ import ( "path" ) -const scryptN int = 262144 // 2^18 -const scryptr int = 8 -const scryptp int = 1 -const scryptdkLen int = 32 +const ( + // 2^18 / 8 / 1 uses 256MB memory and approx 1s CPU time on a modern CPU. + scryptN = 1 << 18 + scryptr = 8 + scryptp = 1 + scryptdkLen = 32 +) -type KeyStorePassphrase struct { +type keyStorePassphrase struct { keysDirPath string } -func (ks KeyStorePassphrase) GenerateNewKey(auth string) (key *Key, err error) { - key, err = GenerateNewKeyDefault(ks, auth) - return +func NewKeyStorePassphrase(path string) KeyStore2 { + ks := new(keyStorePassphrase) + ks.keysDirPath = path + return ks } -func (ks KeyStorePassphrase) GetKey(keyId *uuid.UUID, auth string) (key *Key, err error) { +func (ks keyStorePassphrase) GenerateNewKey(auth string) (key *Key, err error) { + return GenerateNewKeyDefault(ks, auth) +} + +func (ks keyStorePassphrase) GetKey(keyId *uuid.UUID, auth string) (key *Key, err error) { keyBytes, flags, err := DecryptKey(ks, keyId, auth) + if err != nil { + return nil, err + } key = new(Key) key.Id = keyId copy(key.Flags[:], flags[0:4]) key.PrivateKey = ToECDSA(keyBytes) - return + return key, err } -func (ks KeyStorePassphrase) StoreKey(key *Key, auth string) (err error) { +func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (err error) { authArray := []byte(auth) salt := GetEntropyCSPRNG(32) derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptr, scryptp, scryptdkLen) if err != nil { - return + return err } keyBytes := FromECDSA(key.PrivateKey) @@ -113,7 +124,7 @@ func (ks KeyStorePassphrase) StoreKey(key *Key, auth string) (err error) { AES256Block, err := aes.NewCipher(derivedKey) if err != nil { - return + return err } iv := GetEntropyCSPRNG(aes.BlockSize) // 16 @@ -126,70 +137,68 @@ func (ks KeyStorePassphrase) StoreKey(key *Key, auth string) (err error) { hex.EncodeToString(iv), hex.EncodeToString(cipherText), } - keyStruct := KeyProtectedJSON{ + keyStruct := EncryptedKeyJSON{ key.Id.String(), hex.EncodeToString(key.Flags[:]), cipherStruct, } keyJSON, err := json.Marshal(keyStruct) if err != nil { - return + return err } - err = WriteKeyFile(key.Id.String(), ks.keysDirPath, keyJSON) - return + return WriteKeyFile(key.Id.String(), ks.keysDirPath, keyJSON) } -func (ks KeyStorePassphrase) DeleteKey(keyId *uuid.UUID, auth string) (err error) { +func (ks keyStorePassphrase) DeleteKey(keyId *uuid.UUID, auth string) (err error) { // only delete if correct passphrase is given _, _, err = DecryptKey(ks, keyId, auth) if err != nil { - return + return err } keyDirPath := path.Join(ks.keysDirPath, keyId.String()) - err = os.RemoveAll(keyDirPath) - return + return os.RemoveAll(keyDirPath) } -func DecryptKey(ks KeyStorePassphrase, keyId *uuid.UUID, auth string) (keyBytes []byte, flags []byte, err error) { +func DecryptKey(ks keyStorePassphrase, keyId *uuid.UUID, auth string) (keyBytes []byte, flags []byte, err error) { fileContent, err := GetKeyFile(ks.keysDirPath, keyId) if err != nil { - return + return nil, nil, err } - keyProtected := new(KeyProtectedJSON) + keyProtected := new(EncryptedKeyJSON) err = json.Unmarshal(fileContent, keyProtected) flags, err = hex.DecodeString(keyProtected.Flags) if err != nil { - return + return nil, nil, err } salt, err := hex.DecodeString(keyProtected.Crypto.Salt) if err != nil { - return + return nil, nil, err } iv, err := hex.DecodeString(keyProtected.Crypto.IV) if err != nil { - return + return nil, nil, err } cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) if err != nil { - return + return nil, nil, err } authArray := []byte(auth) derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptr, scryptp, scryptdkLen) if err != nil { - return + return nil, nil, err } AES256Block, err := aes.NewCipher(derivedKey) if err != nil { - return + return nil, nil, err } AES256CBCDecrypter := cipher.NewCBCDecrypter(AES256Block, iv) @@ -199,14 +208,14 @@ func DecryptKey(ks KeyStorePassphrase, keyId *uuid.UUID, auth string) (keyBytes plainText := PKCS7Unpad(paddedPlainText) if plainText == nil { err = errors.New("Decryption failed: PKCS7Unpad failed after decryption") - return + return nil, nil, err } keyBytes = plainText[:len(plainText)-32] keyBytesHash := plainText[len(plainText)-32:] if !bytes.Equal(Sha3(keyBytes), keyBytesHash) { err = errors.New("Decryption failed: checksum mismatch") - return + return nil, nil, err } return keyBytes, flags, err } diff --git a/crypto/key_store_plaintext_file.go b/crypto/key_store_plain.go similarity index 71% rename from crypto/key_store_plaintext_file.go rename to crypto/key_store_plain.go index 7ca5e56791..00d9767b68 100644 --- a/crypto/key_store_plaintext_file.go +++ b/crypto/key_store_plain.go @@ -41,7 +41,7 @@ type KeyStore2 interface { DeleteKey(*uuid.UUID, string) error // delete key by id and auth string } -type KeyStorePlaintext struct { +type keyStorePlain struct { keysDirPath string } @@ -51,9 +51,14 @@ func DefaultDataDir() string { return path.Join(usr.HomeDir, ".ethereum") } -func (ks KeyStorePlaintext) GenerateNewKey(auth string) (key *Key, err error) { - key, err = GenerateNewKeyDefault(ks, auth) - return +func NewKeyStorePlain(path string) KeyStore2 { + ks := new(keyStorePlain) + ks.keysDirPath = path + return ks +} + +func (ks keyStorePlain) GenerateNewKey(auth string) (key *Key, err error) { + return GenerateNewKeyDefault(ks, auth) } func GenerateNewKeyDefault(ks KeyStore2, auth string) (key *Key, err error) { @@ -64,50 +69,46 @@ func GenerateNewKeyDefault(ks KeyStore2, auth string) (key *Key, err error) { }() key = NewKey() err = ks.StoreKey(key, auth) - return + return key, err } -func (ks KeyStorePlaintext) GetKey(keyId *uuid.UUID, auth string) (key *Key, err error) { +func (ks keyStorePlain) GetKey(keyId *uuid.UUID, auth string) (key *Key, err error) { fileContent, err := GetKeyFile(ks.keysDirPath, keyId) if err != nil { - return + return nil, err } key = new(Key) err = json.Unmarshal(fileContent, key) - return + return key, err } -func (ks KeyStorePlaintext) StoreKey(key *Key, auth string) (err error) { +func (ks keyStorePlain) StoreKey(key *Key, auth string) (err error) { keyJSON, err := json.Marshal(key) if err != nil { - return + return err } err = WriteKeyFile(key.Id.String(), ks.keysDirPath, keyJSON) - return + return err } -func (ks KeyStorePlaintext) DeleteKey(keyId *uuid.UUID, auth string) (err error) { +func (ks keyStorePlain) DeleteKey(keyId *uuid.UUID, auth string) (err error) { keyDirPath := path.Join(ks.keysDirPath, keyId.String()) err = os.RemoveAll(keyDirPath) - return + return err } func GetKeyFile(keysDirPath string, keyId *uuid.UUID) (fileContent []byte, err error) { - idString := keyId.String() - keyDirPath := path.Join(keysDirPath, idString) - keyFilePath := path.Join(keyDirPath, idString) - fileContent, err = ioutil.ReadFile(keyFilePath) - return + id := keyId.String() + return ioutil.ReadFile(path.Join(keysDirPath, id, id)) } -func WriteKeyFile(idString string, keysDirPath string, content []byte) (err error) { - keyDirPath := path.Join(keysDirPath, idString) - keyFilePath := path.Join(keyDirPath, idString) +func WriteKeyFile(id string, keysDirPath string, content []byte) (err error) { + keyDirPath := path.Join(keysDirPath, id) + keyFilePath := path.Join(keyDirPath, id) err = os.MkdirAll(keyDirPath, 0700) // read, write and dir search for user if err != nil { - return + return err } - err = ioutil.WriteFile(keyFilePath, content, 0600) // read, write for user - return + return ioutil.WriteFile(keyFilePath, content, 0600) // read, write for user } diff --git a/crypto/key_store_test.go b/crypto/key_store_test.go index 4127354447..8469d23696 100644 --- a/crypto/key_store_test.go +++ b/crypto/key_store_test.go @@ -6,20 +6,19 @@ import ( "testing" ) -func TestKeyStorePlaintext(t *testing.T) { - ks := new(KeyStorePlaintext) - ks.keysDirPath = DefaultDataDir() +func TestKeyStorePlain(t *testing.T) { + ks := NewKeyStorePlain(DefaultDataDir()) pass := "" // not used but required by API k1, err := ks.GenerateNewKey(pass) if err != nil { - fmt.Println(err.Error()) + t.Error(err) t.FailNow() } k2 := new(Key) k2, err = ks.GetKey(k1.Id, pass) if err != nil { - fmt.Println(err.Error()) + t.Error(err) t.FailNow() } @@ -40,25 +39,24 @@ func TestKeyStorePlaintext(t *testing.T) { err = ks.DeleteKey(k2.Id, pass) if err != nil { - fmt.Println(err.Error()) + t.Error(err) t.FailNow() } } func TestKeyStorePassphrase(t *testing.T) { - ks := new(KeyStorePassphrase) - ks.keysDirPath = DefaultDataDir() + ks := NewKeyStorePassphrase(DefaultDataDir()) pass := "foo" k1, err := ks.GenerateNewKey(pass) if err != nil { - fmt.Println(err.Error()) + t.Error(err) t.FailNow() } k2 := new(Key) k2, err = ks.GetKey(k1.Id, pass) if err != nil { - fmt.Println(err.Error()) + t.Error(err) t.FailNow() } @@ -79,36 +77,35 @@ func TestKeyStorePassphrase(t *testing.T) { err = ks.DeleteKey(k2.Id, pass) // also to clean up created files if err != nil { - fmt.Println(err.Error()) + t.Error(err) t.FailNow() } } func TestKeyStorePassphraseDecryptionFail(t *testing.T) { - ks := new(KeyStorePassphrase) - ks.keysDirPath = DefaultDataDir() + ks := NewKeyStorePassphrase(DefaultDataDir()) pass := "foo" k1, err := ks.GenerateNewKey(pass) if err != nil { - fmt.Println(err.Error()) + t.Error(err) t.FailNow() } _, err = ks.GetKey(k1.Id, "bar") // wrong passphrase - // fmt.Println(err.Error()) + // t.Error(err) if err == nil { t.FailNow() } err = ks.DeleteKey(k1.Id, "bar") // wrong passphrase if err == nil { - fmt.Println(err.Error()) + t.Error(err) t.FailNow() } err = ks.DeleteKey(k1.Id, pass) // to clean up if err != nil { - fmt.Println(err.Error()) + t.Error(err) t.FailNow() } } From 47d3b3dd58172c2e7c1f72fb072bd9385aff8205 Mon Sep 17 00:00:00 2001 From: Gustav Simonsson Date: Thu, 15 Jan 2015 17:45:45 +0100 Subject: [PATCH 3/6] Address pull request comments * Remove flags field from key struct * Change JSON struct fields from string to []byte * Change GenerateNewKey API to take io.Reader for random source * Remove mixing entropy source function * Use testing Fatal in tests --- crypto/key.go | 150 ++++++--------------------------- crypto/key_store_passphrase.go | 75 ++++++++--------- crypto/key_store_plain.go | 12 +-- crypto/key_store_test.go | 66 ++++----------- 4 files changed, 88 insertions(+), 215 deletions(-) diff --git a/crypto/key.go b/crypto/key.go index 1a8b770e00..0e7c04275a 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -28,43 +28,31 @@ import ( "code.google.com/p/go-uuid/uuid" "crypto/ecdsa" "crypto/elliptic" - crand "crypto/rand" - "encoding/binary" - "encoding/hex" "encoding/json" - "errors" - "fmt" "io" - "os" - "runtime" - "strings" - "time" ) type Key struct { - Id *uuid.UUID // Version 4 "random" for unique id not derived from key data - Flags [4]byte // RFU + Id *uuid.UUID // Version 4 "random" for unique id not derived from key data // we only store privkey as pubkey/address can be derived from it // privkey in this struct is always in plaintext PrivateKey *ecdsa.PrivateKey } -type PlainKeyJSON struct { - Id string - Flags string - PrivateKey string +type plainKeyJSON struct { + Id []byte + PrivateKey []byte } -type CipherJSON struct { - Salt string - IV string - CipherText string +type cipherJSON struct { + Salt []byte + IV []byte + CipherText []byte } -type EncryptedKeyJSON struct { - Id string - Flags string - Crypto CipherJSON +type encryptedKeyJSON struct { + Id []byte + Crypto cipherJSON } func (k *Key) Address() []byte { @@ -73,48 +61,40 @@ func (k *Key) Address() []byte { } func (k *Key) MarshalJSON() (j []byte, err error) { - stringStruct := PlainKeyJSON{ - k.Id.String(), - hex.EncodeToString(k.Flags[:]), - hex.EncodeToString(FromECDSA(k.PrivateKey)), + jStruct := plainKeyJSON{ + *k.Id, + FromECDSA(k.PrivateKey), } - j, err = json.Marshal(stringStruct) + j, err = json.Marshal(jStruct) return j, err } func (k *Key) UnmarshalJSON(j []byte) (err error) { - keyJSON := new(PlainKeyJSON) + keyJSON := new(plainKeyJSON) err = json.Unmarshal(j, &keyJSON) if err != nil { return err } u := new(uuid.UUID) - *u = uuid.Parse(keyJSON.Id) - if *u == nil { - err = errors.New("UUID parsing failed") - return err - } + *u = keyJSON.Id k.Id = u - flagsBytes, err := hex.DecodeString(keyJSON.Flags) - if err != nil { - return err - } - - PrivateKeyBytes, err := hex.DecodeString(keyJSON.PrivateKey) - if err != nil { - return err - } - - copy(k.Flags[:], flagsBytes[0:4]) - k.PrivateKey = ToECDSA(PrivateKeyBytes) + k.PrivateKey = ToECDSA(keyJSON.PrivateKey) return err } -func NewKey() *Key { - randBytes := GetEntropyCSPRNG(32) +func NewKey(rand io.Reader) *Key { + randBytes := make([]byte, 32) + n, err := rand.Read(randBytes) + if err != nil { + panic("key generation: could not read from random source: " + err.Error()) + } else { + if n != 32 { + panic("key generation: read less than required bytes from random source: " + err.Error()) + } + } reader := bytes.NewReader(randBytes) _, x, y, err := elliptic.GenerateKey(S256(), reader) if err != nil { @@ -126,80 +106,6 @@ func NewKey() *Key { key := new(Key) id := uuid.NewRandom() key.Id = &id - // flags := new([4]byte) - // key.Flags = flags key.PrivateKey = privateKeyECDSA return key } - -// plain crypto/rand. this is /dev/urandom on Unix-like systems. -func GetEntropyCSPRNG(n int) []byte { - mainBuff := make([]byte, n) - _, err := io.ReadFull(crand.Reader, mainBuff) - if err != nil { - panic("key generation: reading from crypto/rand failed: " + err.Error()) - } - return mainBuff -} - -// TODO: verify. Do not use until properly discussed. -// we start with crypt/rand, then mix in additional sources of entropy. -// These sources are from three types: OS, go runtime and ethereum client state. -func GetEntropyTinFoilHat() []byte { - startTime := time.Now().UnixNano() - // for each source, we XOR in it's SHA3 hash. - mainBuff := GetEntropyCSPRNG(32) - // 1. OS entropy sources - startTimeBytes := make([]byte, 32) - binary.PutVarint(startTimeBytes, startTime) - startTimeHash := Sha3(startTimeBytes) - mix32Byte(mainBuff, startTimeHash) - - pid := os.Getpid() - pidBytes := make([]byte, 32) - binary.PutUvarint(pidBytes, uint64(pid)) - pidHash := Sha3(pidBytes) - mix32Byte(mainBuff, pidHash) - - osEnv := os.Environ() - osEnvBytes := []byte(strings.Join(osEnv, "")) - osEnvHash := Sha3(osEnvBytes) - mix32Byte(mainBuff, osEnvHash) - - // not all OS have hostname in env variables - osHostName, err := os.Hostname() - if err != nil { - osHostNameBytes := []byte(osHostName) - osHostNameHash := Sha3(osHostNameBytes) - mix32Byte(mainBuff, osHostNameHash) - } - - // 2. go runtime entropy sources - memStats := new(runtime.MemStats) - runtime.ReadMemStats(memStats) - memStatsBytes := []byte(fmt.Sprintf("%v", memStats)) - memStatsHash := Sha3(memStatsBytes) - mix32Byte(mainBuff, memStatsHash) - - // 3. Mix in ethereum / client state - // TODO: list of network peers structs (IP, port, etc) - // TODO: merkle patricia tree root hash for world state and tx list - - // 4. Yo dawg we heard you like entropy so we'll grab some entropy from how - // long it took to grab the above entropy. And a yield, for good measure. - runtime.Gosched() - diffTime := time.Now().UnixNano() - startTime - diffTimeBytes := make([]byte, 32) - binary.PutVarint(diffTimeBytes, diffTime) - diffTimeHash := Sha3(diffTimeBytes) - mix32Byte(mainBuff, diffTimeHash) - - return mainBuff -} - -func mix32Byte(buff []byte, mixBuff []byte) []byte { - for i := 0; i < 32; i++ { - buff[i] ^= mixBuff[i] - } - return buff -} diff --git a/crypto/key_store_passphrase.go b/crypto/key_store_passphrase.go index eaf73422f6..1e7c50f965 100644 --- a/crypto/key_store_passphrase.go +++ b/crypto/key_store_passphrase.go @@ -69,9 +69,10 @@ import ( "code.google.com/p/go.crypto/scrypt" "crypto/aes" "crypto/cipher" - "encoding/hex" + crand "crypto/rand" "encoding/json" "errors" + "io" "os" "path" ) @@ -94,25 +95,24 @@ func NewKeyStorePassphrase(path string) KeyStore2 { return ks } -func (ks keyStorePassphrase) GenerateNewKey(auth string) (key *Key, err error) { - return GenerateNewKeyDefault(ks, auth) +func (ks keyStorePassphrase) GenerateNewKey(rand io.Reader, auth string) (key *Key, err error) { + return GenerateNewKeyDefault(ks, rand, auth) } func (ks keyStorePassphrase) GetKey(keyId *uuid.UUID, auth string) (key *Key, err error) { - keyBytes, flags, err := DecryptKey(ks, keyId, auth) + keyBytes, err := DecryptKey(ks, keyId, auth) if err != nil { return nil, err } key = new(Key) key.Id = keyId - copy(key.Flags[:], flags[0:4]) key.PrivateKey = ToECDSA(keyBytes) return key, err } func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (err error) { authArray := []byte(auth) - salt := GetEntropyCSPRNG(32) + salt := getEntropyCSPRNG(32) derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptr, scryptp, scryptdkLen) if err != nil { return err @@ -127,19 +127,18 @@ func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (err error) { return err } - iv := GetEntropyCSPRNG(aes.BlockSize) // 16 + iv := getEntropyCSPRNG(aes.BlockSize) // 16 AES256CBCEncrypter := cipher.NewCBCEncrypter(AES256Block, iv) cipherText := make([]byte, len(toEncrypt)) AES256CBCEncrypter.CryptBlocks(cipherText, toEncrypt) - cipherStruct := CipherJSON{ - hex.EncodeToString(salt), - hex.EncodeToString(iv), - hex.EncodeToString(cipherText), + cipherStruct := cipherJSON{ + salt, + iv, + cipherText, } - keyStruct := EncryptedKeyJSON{ - key.Id.String(), - hex.EncodeToString(key.Flags[:]), + keyStruct := encryptedKeyJSON{ + *key.Id, cipherStruct, } keyJSON, err := json.Marshal(keyStruct) @@ -152,7 +151,7 @@ func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (err error) { func (ks keyStorePassphrase) DeleteKey(keyId *uuid.UUID, auth string) (err error) { // only delete if correct passphrase is given - _, _, err = DecryptKey(ks, keyId, auth) + _, err = DecryptKey(ks, keyId, auth) if err != nil { return err } @@ -161,44 +160,30 @@ func (ks keyStorePassphrase) DeleteKey(keyId *uuid.UUID, auth string) (err error return os.RemoveAll(keyDirPath) } -func DecryptKey(ks keyStorePassphrase, keyId *uuid.UUID, auth string) (keyBytes []byte, flags []byte, err error) { +func DecryptKey(ks keyStorePassphrase, keyId *uuid.UUID, auth string) (keyBytes []byte, err error) { fileContent, err := GetKeyFile(ks.keysDirPath, keyId) if err != nil { - return nil, nil, err + return nil, err } - keyProtected := new(EncryptedKeyJSON) + keyProtected := new(encryptedKeyJSON) err = json.Unmarshal(fileContent, keyProtected) - flags, err = hex.DecodeString(keyProtected.Flags) - if err != nil { - return nil, nil, err - } + salt := keyProtected.Crypto.Salt - salt, err := hex.DecodeString(keyProtected.Crypto.Salt) - if err != nil { - return nil, nil, err - } + iv := keyProtected.Crypto.IV - iv, err := hex.DecodeString(keyProtected.Crypto.IV) - if err != nil { - return nil, nil, err - } - - cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) - if err != nil { - return nil, nil, err - } + cipherText := keyProtected.Crypto.CipherText authArray := []byte(auth) derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptr, scryptp, scryptdkLen) if err != nil { - return nil, nil, err + return nil, err } AES256Block, err := aes.NewCipher(derivedKey) if err != nil { - return nil, nil, err + return nil, err } AES256CBCDecrypter := cipher.NewCBCDecrypter(AES256Block, iv) @@ -208,16 +193,26 @@ func DecryptKey(ks keyStorePassphrase, keyId *uuid.UUID, auth string) (keyBytes plainText := PKCS7Unpad(paddedPlainText) if plainText == nil { err = errors.New("Decryption failed: PKCS7Unpad failed after decryption") - return nil, nil, err + return nil, err } keyBytes = plainText[:len(plainText)-32] keyBytesHash := plainText[len(plainText)-32:] if !bytes.Equal(Sha3(keyBytes), keyBytesHash) { err = errors.New("Decryption failed: checksum mismatch") - return nil, nil, err + return nil, err } - return keyBytes, flags, err + return keyBytes, err +} + +// plain crypto/rand. this is /dev/urandom on Unix-like systems. +func getEntropyCSPRNG(n int) []byte { + mainBuff := make([]byte, n) + _, err := io.ReadFull(crand.Reader, mainBuff) + if err != nil { + panic("key generation: reading from crypto/rand failed: " + err.Error()) + } + return mainBuff } // From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes diff --git a/crypto/key_store_plain.go b/crypto/key_store_plain.go index 00d9767b68..2aa813f5e8 100644 --- a/crypto/key_store_plain.go +++ b/crypto/key_store_plain.go @@ -27,6 +27,7 @@ import ( "code.google.com/p/go-uuid/uuid" "encoding/json" "fmt" + "io" "io/ioutil" "os" "os/user" @@ -35,7 +36,8 @@ import ( // TODO: rename to KeyStore when replacing existing KeyStore type KeyStore2 interface { - GenerateNewKey(string) (*Key, error) // create and store new key, optionally using auth string + // create new key using io.Reader entropy source and optionally using auth string + GenerateNewKey(io.Reader, string) (*Key, error) GetKey(*uuid.UUID, string) (*Key, error) // key from id and auth string StoreKey(*Key, string) error // store key optionally using auth string DeleteKey(*uuid.UUID, string) error // delete key by id and auth string @@ -57,17 +59,17 @@ func NewKeyStorePlain(path string) KeyStore2 { return ks } -func (ks keyStorePlain) GenerateNewKey(auth string) (key *Key, err error) { - return GenerateNewKeyDefault(ks, auth) +func (ks keyStorePlain) GenerateNewKey(rand io.Reader, auth string) (key *Key, err error) { + return GenerateNewKeyDefault(ks, rand, auth) } -func GenerateNewKeyDefault(ks KeyStore2, auth string) (key *Key, err error) { +func GenerateNewKeyDefault(ks KeyStore2, rand io.Reader, auth string) (key *Key, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("GenerateNewKey error: %v", r) } }() - key = NewKey() + key = NewKey(rand) err = ks.StoreKey(key, auth) return key, err } diff --git a/crypto/key_store_test.go b/crypto/key_store_test.go index 8469d23696..16c0e476d4 100644 --- a/crypto/key_store_test.go +++ b/crypto/key_store_test.go @@ -1,7 +1,7 @@ package crypto import ( - "fmt" + crand "crypto/rand" "reflect" "testing" ) @@ -9,107 +9,77 @@ import ( func TestKeyStorePlain(t *testing.T) { ks := NewKeyStorePlain(DefaultDataDir()) pass := "" // not used but required by API - k1, err := ks.GenerateNewKey(pass) + k1, err := ks.GenerateNewKey(crand.Reader, pass) if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } k2 := new(Key) k2, err = ks.GetKey(k1.Id, pass) if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } if !reflect.DeepEqual(k1.Id, k2.Id) { - fmt.Println("key Id mismatch") - t.FailNow() - } - - if k1.Flags != k2.Flags { - fmt.Println("key Flags mismatch") - t.FailNow() + t.Fatal(err) } if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { - fmt.Println("key PrivateKey mismatch") - t.FailNow() + t.Fatal(err) } err = ks.DeleteKey(k2.Id, pass) if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } } func TestKeyStorePassphrase(t *testing.T) { ks := NewKeyStorePassphrase(DefaultDataDir()) pass := "foo" - k1, err := ks.GenerateNewKey(pass) + k1, err := ks.GenerateNewKey(crand.Reader, pass) if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } - k2 := new(Key) k2, err = ks.GetKey(k1.Id, pass) if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } - if !reflect.DeepEqual(k1.Id, k2.Id) { - fmt.Println("key Id mismatch") - t.FailNow() - } - - if k1.Flags != k2.Flags { - fmt.Println("key Flags mismatch") - t.FailNow() + t.Fatal(err) } if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { - fmt.Println("key PrivateKey mismatch") - t.FailNow() + t.Fatal(err) } err = ks.DeleteKey(k2.Id, pass) // also to clean up created files if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } } func TestKeyStorePassphraseDecryptionFail(t *testing.T) { ks := NewKeyStorePassphrase(DefaultDataDir()) pass := "foo" - k1, err := ks.GenerateNewKey(pass) + k1, err := ks.GenerateNewKey(crand.Reader, pass) if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } _, err = ks.GetKey(k1.Id, "bar") // wrong passphrase - // t.Error(err) if err == nil { - t.FailNow() + t.Fatal(err) } err = ks.DeleteKey(k1.Id, "bar") // wrong passphrase if err == nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } err = ks.DeleteKey(k1.Id, pass) // to clean up if err != nil { - t.Error(err) - t.FailNow() + t.Fatal(err) } } - -func TestKeyMixedEntropy(t *testing.T) { - GetEntropyTinFoilHat() -} From 9caf32befebb41fbb013b299caf2fff5fe840430 Mon Sep 17 00:00:00 2001 From: Gustav Simonsson Date: Thu, 15 Jan 2015 19:58:38 +0100 Subject: [PATCH 4/6] Update code comments --- crypto/key_store_passphrase.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crypto/key_store_passphrase.go b/crypto/key_store_passphrase.go index 1e7c50f965..c7f740ae36 100644 --- a/crypto/key_store_passphrase.go +++ b/crypto/key_store_passphrase.go @@ -22,9 +22,8 @@ */ /* -This key store behaves as KeyStorePlaintextFile with the difference that -the private key is encrypted and encoded as a JSON object within the -key JSON object. +This key store behaves as KeyStorePlain with the difference that +the private key is encrypted and on disk uses another JSON encoding. Cryptography: @@ -39,9 +38,9 @@ Cryptography: Encoding: -1. On disk, ciphertext, salt and IV are encoded as a JSON object. +1. On disk, ciphertext, salt and IV are encoded in a nested JSON object. cat a key file to see the structure. -2. byte arrays are ASCII HEX encoded as JSON strings. +2. byte arrays are base64 JSON strings. 3. The EC private key bytes are in uncompressed form [7]. They are a big-endian byte slice of the absolute value of D [8][9]. 4. The checksum is the last 32 bytes of the plaintext byte array and the @@ -205,7 +204,6 @@ func DecryptKey(ks keyStorePassphrase, keyId *uuid.UUID, auth string) (keyBytes return keyBytes, err } -// plain crypto/rand. this is /dev/urandom on Unix-like systems. func getEntropyCSPRNG(n int) []byte { mainBuff := make([]byte, n) _, err := io.ReadFull(crand.Reader, mainBuff) From 3cf038f300950c5a14b99fb0249ec09f663ef6af Mon Sep 17 00:00:00 2001 From: Gustav Simonsson Date: Mon, 19 Jan 2015 20:24:30 +0100 Subject: [PATCH 5/6] Address pull request comments * Allocate with composite literal instead of new * Remove check of number of bytes read from rand --- crypto/key.go | 6 +----- crypto/key_store_passphrase.go | 12 +++++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/crypto/key.go b/crypto/key.go index 0e7c04275a..d371ad4dc9 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -87,13 +87,9 @@ func (k *Key) UnmarshalJSON(j []byte) (err error) { func NewKey(rand io.Reader) *Key { randBytes := make([]byte, 32) - n, err := rand.Read(randBytes) + _, err := rand.Read(randBytes) if err != nil { panic("key generation: could not read from random source: " + err.Error()) - } else { - if n != 32 { - panic("key generation: read less than required bytes from random source: " + err.Error()) - } } reader := bytes.NewReader(randBytes) _, x, y, err := elliptic.GenerateKey(S256(), reader) diff --git a/crypto/key_store_passphrase.go b/crypto/key_store_passphrase.go index c7f740ae36..68135a4f04 100644 --- a/crypto/key_store_passphrase.go +++ b/crypto/key_store_passphrase.go @@ -89,8 +89,9 @@ type keyStorePassphrase struct { } func NewKeyStorePassphrase(path string) KeyStore2 { - ks := new(keyStorePassphrase) - ks.keysDirPath = path + ks := &keyStorePassphrase{ + keysDirPath : path, + } return ks } @@ -103,9 +104,10 @@ func (ks keyStorePassphrase) GetKey(keyId *uuid.UUID, auth string) (key *Key, er if err != nil { return nil, err } - key = new(Key) - key.Id = keyId - key.PrivateKey = ToECDSA(keyBytes) + key = &Key{ + Id : keyId, + PrivateKey : ToECDSA(keyBytes), + } return key, err } From d48140cab39923bb06bb4df8667efd2df000d17b Mon Sep 17 00:00:00 2001 From: Gustav Simonsson Date: Mon, 19 Jan 2015 22:12:22 +0100 Subject: [PATCH 6/6] Address pull request comments * Further simplify "constructor" function's allocation of structs * Fix formatting --- crypto/key_store_passphrase.go | 9 +++------ crypto/key_store_plain.go | 4 +--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/crypto/key_store_passphrase.go b/crypto/key_store_passphrase.go index 68135a4f04..e30a0a785f 100644 --- a/crypto/key_store_passphrase.go +++ b/crypto/key_store_passphrase.go @@ -89,10 +89,7 @@ type keyStorePassphrase struct { } func NewKeyStorePassphrase(path string) KeyStore2 { - ks := &keyStorePassphrase{ - keysDirPath : path, - } - return ks + return &keyStorePassphrase{path} } func (ks keyStorePassphrase) GenerateNewKey(rand io.Reader, auth string) (key *Key, err error) { @@ -105,8 +102,8 @@ func (ks keyStorePassphrase) GetKey(keyId *uuid.UUID, auth string) (key *Key, er return nil, err } key = &Key{ - Id : keyId, - PrivateKey : ToECDSA(keyBytes), + Id: keyId, + PrivateKey: ToECDSA(keyBytes), } return key, err } diff --git a/crypto/key_store_plain.go b/crypto/key_store_plain.go index 2aa813f5e8..b6e2a309a9 100644 --- a/crypto/key_store_plain.go +++ b/crypto/key_store_plain.go @@ -54,9 +54,7 @@ func DefaultDataDir() string { } func NewKeyStorePlain(path string) KeyStore2 { - ks := new(keyStorePlain) - ks.keysDirPath = path - return ks + return &keyStorePlain{path} } func (ks keyStorePlain) GenerateNewKey(rand io.Reader, auth string) (key *Key, err error) {