Skip to content

Commit

Permalink
Merge pull request #137 from ubclaunchpad/133-secure-private-key-storage
Browse files Browse the repository at this point in the history
133 secure private key storage
  • Loading branch information
bfbachmann committed Dec 7, 2017
2 parents 0723e09 + 6824206 commit a5445a0
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 6 deletions.
127 changes: 127 additions & 0 deletions app/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ func RunConsole(a *App) *ishell.Shell {
editUser(ctx, a)
},
})
shell.AddCmd(&ishell.Cmd{
Name: "cryptowallet",
Help: "enable or disable password protection for private key storage",
Func: func(ctx *ishell.Context) {
cryptoWallet(ctx, a)
},
})

// Set interrupt handler
shell.Interrupt(func(ctx *ishell.Context, count int, input string) {
Expand All @@ -83,6 +90,96 @@ func RunConsole(a *App) *ishell.Shell {
return shell
}

func encryptUser(ctx *ishell.Context, app *App, password string) error {
err := app.CurrentUser.EncryptPrivateKey(password)
if err != nil {
return err
}

if err := app.CurrentUser.Save(userFileName); err != nil {
app.CurrentUser.DecryptPrivateKey(password)
ctx.Print(err)
return err
}

return nil
}

func decryptUser(ctx *ishell.Context, app *App, password string) error {
err := app.CurrentUser.DecryptPrivateKey(password)
if err != nil {
return err
}

if err := app.CurrentUser.Save(userFileName); err != nil {
app.CurrentUser.EncryptPrivateKey(password)
ctx.Print(err)
return err
}

return nil
}

func cryptoWallet(ctx *ishell.Context, app *App) {
if len(ctx.Args) < 1 {
ctx.Println("\nUsage: cryptowallet [command]")
ctx.Println("\nCOMMANDS:")
ctx.Println("\t enable \t Enable cryptowallet")
ctx.Println("\t disable \t Disable cryptowallet")
return
}

switch ctx.Args[0] {
case "enable":
if app.CurrentUser.CryptoWallet {
ctx.Print("CryptoWallet is already enabled")
} else {
ctx.Print("Please enter password: ")
password := ctx.ReadPassword()
err := encryptUser(ctx, app, password)
if err != nil {
ctx.Print("Unable to decrypt private key")
} else {
ctx.Print("Successfully enabled cryptowallet")
}
}
case "disable":
if !app.CurrentUser.CryptoWallet {
ctx.Print("CryptoWallet is already disabled")
return
}
ctx.Print("Please enter password: ")
password := ctx.ReadPassword()
err := decryptUser(ctx, app, password)

// Invalid password, try again
if InvalidPassword(err) {
ctx.Print("Inavalid password, please try again: ")
password = ctx.ReadPassword()
err = decryptUser(ctx, app, password)
}
if err != nil {
ctx.Println("Unable to decrypt private key")
return
}
ctx.Print("Successfully disabled cryptowallet")
return
case "status":
var s string
if app.CurrentUser.CryptoWallet {
s = "enabled"
} else {
s = "disabled"
}
ctx.Printf("cryptowallet status: %s", s)
default:
ctx.Println("\nUsage: cryptowallet [command]")
ctx.Println("\nCOMMANDS:")
ctx.Println("\t enable \t Enable cryptowallet")
ctx.Println("\t disable \t Disable cryptowallet")
}
}

func send(ctx *ishell.Context, app *App) {
if len(ctx.Args) < 2 {
ctx.Println("Usage: send [amount] [public address]")
Expand All @@ -100,9 +197,39 @@ func send(ctx *ishell.Context, app *App) {
amount *= float64(blockchain.CoinValue)
addr := ctx.Args[1]

cryptoWallet := false
password := ""
if app.CurrentUser.CryptoWallet {
ctx.Print("Please enter cryptowallet password: ")
password = ctx.ReadPassword()
err = app.CurrentUser.DecryptPrivateKey(password)

// Invalid password, try again
if InvalidPassword(err) {
ctx.Print("Inavalid password, please try again: ")
password = ctx.ReadPassword()
err = app.CurrentUser.DecryptPrivateKey(password)
}
if err != nil {
ctx.Println("Cannot proceed with transaction, unable to decrypt private key")
return
}
cryptoWallet = true
}

// Try to make a payment.
ctx.Println("Sending amount", coinValue(uint64(amount)), "to", addr)
err = app.Pay(addr, uint64(amount))

// Re-enable crypto wallet
if cryptoWallet {
err := app.CurrentUser.EncryptPrivateKey(password)
if err != nil {
ctx.Println("Error re-encrypting private key locally")
panic(err)
}
}

if err != nil {
emoji.Println(":disappointed: ", err)
} else {
Expand Down
1 change: 1 addition & 0 deletions app/console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func TestRunConsoleHasCommands(t *testing.T) {
"address",
"clear",
"connect",
"cryptowallet",
"exit",
"help",
"miner",
Expand Down
99 changes: 99 additions & 0 deletions app/crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package app

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha512"
"strings"

"golang.org/x/crypto/pbkdf2"
)

/*
https://golang.org/src/crypto/cipher/example_test.go
*/

const (
nonceSize = 12
saltSize = 16
)

// Encrypt encrypts cipherText with a given password
func Encrypt(plainText []byte, password string) ([]byte, error) {

passwordBytes := []byte(password)

salt := make([]byte, saltSize)
_, err := rand.Read(salt)
if err != nil {
return nil, err
}

key := pbkdf2.Key(passwordBytes, salt, 4096, 32, sha512.New)

block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

nonce := make([]byte, nonceSize)
_, err = rand.Read(nonce)
if err != nil {
return nil, err
}

cipherText := aesgcm.Seal(nil, nonce, plainText, nil)
cipherText = append(cipherText, salt...)
cipherText = append(cipherText, nonce...)

return cipherText, nil
}

// Decrypt decrypts cipherText with a given password
func Decrypt(cipherText []byte, password string) ([]byte, error) {

passwordBytes := []byte(password)

ctLen := len(cipherText)
nonce := cipherText[ctLen-nonceSize:]
salt := cipherText[ctLen-nonceSize-saltSize : ctLen-nonceSize]

key := pbkdf2.Key(passwordBytes, salt, 4096, 32, sha512.New)

block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}

aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

plainText, err := aesgcm.Open(
nil,
nonce,
cipherText[:ctLen-saltSize-nonceSize],
nil,
)
if err != nil {
return nil, err
}

return plainText, nil
}

// InvalidPassword is returned from Decrypt if an invalid password is used to
// decrypt the ciphertext
func InvalidPassword(err error) bool {
if err == nil {
return false
}
return strings.Compare(err.Error(), "cipher: message authentication failed") == 0
}
29 changes: 29 additions & 0 deletions app/crypto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package app_test

import (
"testing"

"github.com/ubclaunchpad/cumulus/app"
)

func TestInvalidPassword(t *testing.T) {
plainText := "Hello, world!"
plainTextBytes := []byte(plainText)

cipherText, _ := app.Encrypt(plainTextBytes, "correctPassword")
plainTextNew, err := app.Decrypt(cipherText, "incorrectPassword")

if !app.InvalidPassword(err) {
t.Fail()
}

if plainTextNew != nil {
t.Fail()
}

plainTextNew, err = app.Decrypt(cipherText, "correctPassword")

if string(plainTextNew[:]) != plainText {
t.Fail()
}
}
50 changes: 44 additions & 6 deletions app/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log"
"math/big"
"os"

crand "crypto/rand"
Expand All @@ -16,17 +17,19 @@ import (

// User holds basic user information.
type User struct {
Wallet *blockchain.Wallet
Name string
BlockSize uint32
Wallet *blockchain.Wallet
Name string
BlockSize uint32
CryptoWallet bool
}

// NewUser creates a new user
func NewUser() *User {
return &User{
Wallet: blockchain.NewWallet(),
BlockSize: blockchain.DefaultBlockSize,
Name: "Default User",
Wallet: blockchain.NewWallet(),
BlockSize: blockchain.DefaultBlockSize,
Name: "Default User",
CryptoWallet: false,
}
}

Expand All @@ -35,10 +38,44 @@ func (u *User) Public() blockchain.Address {
return u.Wallet.Public()
}

// EncryptPrivateKey encrypts the user's private key
func (u *User) EncryptPrivateKey(password string) error {
if !u.CryptoWallet {
pk := u.Wallet.PrivateKey.D.Bytes()
pk, err := Encrypt(pk, password)
if err != nil {
return err
}

u.Wallet.PrivateKey.D = new(big.Int).SetBytes(pk)
u.CryptoWallet = true
}
return nil
}

// DecryptPrivateKey decrypts the user's private key
func (u *User) DecryptPrivateKey(password string) error {
if u.CryptoWallet {
pk := u.Wallet.PrivateKey.D.Bytes()
pk, err := Decrypt(pk, password)
if err != nil {
return err
}

u.Wallet.PrivateKey.D = new(big.Int).SetBytes(pk)
u.CryptoWallet = false
}
return nil
}

// Save writes the user to a file of the given name in the current working
// directory in JSON format. It returns an error if one occurred, or a pointer
// to the file that was written to on success.
func (u *User) Save(fileName string) error {
if _, err := os.Stat(fileName); !os.IsNotExist(err) {
os.Remove(fileName)
}

userFile, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
return err
Expand Down Expand Up @@ -74,6 +111,7 @@ func LoadUser(fileName string) (*User, error) {
if err := dec.Decode(&u); err != nil {
return nil, err
}

return &u, nil
}

Expand Down
20 changes: 20 additions & 0 deletions app/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,23 @@ func TestSaveAndLoad(t *testing.T) {
assert.Equal(t, user1, user2)
assert.Nil(t, os.Remove("userTestFile.json"))
}

func TestSave(t *testing.T) {
user1 := NewUser()

assert.Nil(t, user1.Save("userTestFile.json"))
user2, err := LoadUser("userTestFile.json")
assert.Nil(t, err)

assert.Nil(t, user1.EncryptPrivateKey("password"))
assert.Nil(t, user1.Save("userTestFile.json"))

assert.Nil(t, user1.DecryptPrivateKey("password"))
assert.Nil(t, user1.Save("userTestFile.json"))

user1, err = LoadUser("userTestFile.json")
assert.Nil(t, err)

assert.Equal(t, user1, user2)
assert.Nil(t, os.Remove("userTestFile.json"))
}

0 comments on commit a5445a0

Please sign in to comment.