prysm-pulse/validator/rpc/auth.go
dv8silencer 7664eab32d
Introduce checks for existing wallets into two edge cases (#7349)
* Add check for preexisting wallet

* Reminder

* Create another check.  Adjust tests

* Wording in comment

* Remove unnecessary logic.  Add another regression test.

* Spacing

* Prepare to merge master

* Refactor wallet checks

* Fixed test

* Revert back to original location of check

* Fix test

* Fix test

* Address linter feedback

* Align CreateWallet() with recent changes

* Align RecoverWallet with recent changes

* Correct test error message

* Refactor test to minimize duplication

* rename to isValid and adjust function code

* refactor

* Improve IsValid()

* Fix tests

* Remove comment

* Fix documentation of IsValid()

* Align behavior of OpenWalletOrElseCli

* Fix tests

* Realigning error msg with prior behavior

* Fix another test

* Correct logic

* small change in logic

* Fix err messages

* Create consts for repeated strings

* Add comments to new constants

* gofmt

* Adjust WalletConfig behavior so it is closer to prior-to-PR.  Adjust test accordingly

* adjust error messages

Co-authored-by: dv8silencer <15720668+dv8silencer@users.noreply.github.com>
2020-09-30 16:13:37 +02:00

152 lines
5.5 KiB
Go

package rpc
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
pb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2"
"github.com/prysmaticlabs/prysm/shared/fileutil"
"github.com/prysmaticlabs/prysm/shared/params"
"github.com/prysmaticlabs/prysm/shared/promptutil"
"github.com/prysmaticlabs/prysm/shared/timeutils"
"github.com/prysmaticlabs/prysm/validator/accounts/v2/wallet"
"golang.org/x/crypto/bcrypt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
tokenExpiryLength = 20 * time.Minute
hashCost = 8
)
// Signup to authenticate access to the validator RPC API using bcrypt and
// a sufficiently strong password check.
func (s *Server) Signup(ctx context.Context, req *pb.AuthRequest) (*pb.AuthResponse, error) {
// First, we check if the validator already has a password. In this case,
// the user should NOT be able to signup and the function will return an error.
if fileutil.FileExists(filepath.Join(defaultWalletPath, wallet.HashedPasswordFileName)) {
return nil, status.Error(codes.PermissionDenied, "Validator already has a password set, cannot signup")
}
// We check the strength of the password to ensure it is high-entropy,
// has the required character count, and contains only unicode characters.
if err := promptutil.ValidatePasswordInput(req.Password); err != nil {
return nil, status.Error(codes.InvalidArgument, "Could not validate password input")
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), hashCost)
if err != nil {
return nil, errors.Wrap(err, "could not generate hashed password")
}
hashFilePath := filepath.Join(defaultWalletPath, wallet.HashedPasswordFileName)
// Write the config file to disk.
if err := os.MkdirAll(defaultWalletPath, os.ModePerm); err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
if err := ioutil.WriteFile(hashFilePath, hashedPassword, params.BeaconIoConfig().ReadWritePermissions); err != nil {
return nil, status.Errorf(codes.Internal, "could not write hashed password for wallet to disk: %v", err)
}
return s.sendAuthResponse()
}
// Login to authenticate with the validator RPC API using a password.
func (s *Server) Login(ctx context.Context, req *pb.AuthRequest) (*pb.AuthResponse, error) {
hashedPasswordPath := filepath.Join(defaultWalletPath, wallet.HashedPasswordFileName)
if fileutil.FileExists(hashedPasswordPath) {
hashedPassword, err := fileutil.ReadFileAsBytes(hashedPasswordPath)
if err != nil {
return nil, status.Error(codes.Internal, "Could not retrieve hashed password from disk")
}
// Compare the stored hashed password, with the hashed version of the password that was received.
if err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(req.Password)); err != nil {
return nil, status.Error(codes.Unauthenticated, "Incorrect password")
}
}
if err := s.initializeWallet(ctx, &wallet.Config{
WalletDir: defaultWalletPath,
WalletPassword: req.Password,
}); err != nil {
if strings.Contains(err.Error(), "invalid checksum") {
return nil, status.Error(codes.Unauthenticated, "Incorrect password")
}
return nil, status.Errorf(codes.Internal, "Could not initialize wallet: %v", err)
}
return s.sendAuthResponse()
}
// Sends an auth response via gRPC containing a new JWT token.
func (s *Server) sendAuthResponse() (*pb.AuthResponse, error) {
// If everything is fine here, construct the auth token.
tokenString, expirationTime, err := s.createTokenString()
if err != nil {
return nil, status.Error(codes.Internal, "Could not create jwt token string")
}
return &pb.AuthResponse{
Token: tokenString,
TokenExpiration: expirationTime,
}, nil
}
// Creates a JWT token string using the JWT key with an expiration timestamp.
func (s *Server) createTokenString() (string, uint64, error) {
// Create a new token object, specifying signing method and the claims
// you would like it to contain.
expirationTime := timeutils.Now().Add(tokenExpiryLength)
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(),
})
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(s.jwtKey)
if err != nil {
return "", 0, err
}
return tokenString, uint64(expirationTime.Unix()), nil
}
// Initialize a wallet and send it over a global feed.
func (s *Server) initializeWallet(ctx context.Context, cfg *wallet.Config) error {
// We first ensure the user has a wallet.
exists, err := wallet.Exists(cfg.WalletDir)
if err != nil {
return errors.Wrap(err, wallet.CheckExistsErrMsg)
}
if !exists {
return wallet.ErrNoWalletFound
}
valid, err := wallet.IsValid(cfg.WalletDir)
if err == wallet.ErrNoWalletFound {
return wallet.ErrNoWalletFound
}
if err != nil {
return errors.Wrap(err, wallet.CheckValidityErrMsg)
}
if !valid {
return errors.New(wallet.InvalidWalletErrMsg)
}
// We fire an event with the opened wallet over
// a global feed signifying wallet initialization.
w, err := wallet.OpenWallet(ctx, &wallet.Config{
WalletDir: cfg.WalletDir,
WalletPassword: cfg.WalletPassword,
})
if err != nil {
return errors.Wrap(err, "could not open wallet")
}
s.walletInitialized = true
km, err := w.InitializeKeymanager(ctx, true /* skip mnemonic confirm */)
if err != nil {
return errors.Wrap(err, "could not initialize keymanager")
}
s.keymanager = km
s.wallet = w
s.walletInitializedFeed.Send(w)
return nil
}