github.com/prysmaticlabs/prysm@v1.4.4/validator/rpc/auth.go (about) 1 package rpc 2 3 import ( 4 "context" 5 "path/filepath" 6 "time" 7 8 "github.com/form3tech-oss/jwt-go" 9 "github.com/golang/protobuf/ptypes/empty" 10 "github.com/pkg/errors" 11 pb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2" 12 "github.com/prysmaticlabs/prysm/shared/fileutil" 13 "github.com/prysmaticlabs/prysm/shared/promptutil" 14 "github.com/prysmaticlabs/prysm/shared/timeutils" 15 "github.com/prysmaticlabs/prysm/validator/accounts/wallet" 16 "golang.org/x/crypto/bcrypt" 17 "google.golang.org/grpc/codes" 18 "google.golang.org/grpc/status" 19 ) 20 21 var ( 22 tokenExpiryLength = time.Hour 23 hashCost = 8 24 ) 25 26 const ( 27 // HashedRPCPassword for the validator RPC access. 28 HashedRPCPassword = "rpc-password-hash" 29 checkUserSignupInterval = time.Second * 30 30 ) 31 32 // Signup to authenticate access to the validator RPC API using bcrypt and 33 // a sufficiently strong password check. 34 func (s *Server) Signup(ctx context.Context, req *pb.AuthRequest) (*pb.AuthResponse, error) { 35 walletDir := s.walletDir 36 if req.Password != req.PasswordConfirmation { 37 return nil, status.Error(codes.InvalidArgument, "Password confirmation does not match") 38 } 39 // First, we check if the validator already has a password. In this case, 40 // the user should be logged in as normal. 41 if fileutil.FileExists(filepath.Join(walletDir, HashedRPCPassword)) { 42 return s.Login(ctx, req) 43 } 44 // We check the strength of the password to ensure it is high-entropy, 45 // has the required character count, and contains only unicode characters. 46 if err := promptutil.ValidatePasswordInput(req.Password); err != nil { 47 return nil, status.Errorf(codes.InvalidArgument, "Could not validate RPC password input: %v", err) 48 } 49 hasDir, err := fileutil.HasDir(walletDir) 50 if err != nil { 51 return nil, status.Error(codes.FailedPrecondition, "Could not check if wallet directory exists") 52 } 53 if !hasDir { 54 if err := fileutil.MkdirAll(walletDir); err != nil { 55 return nil, status.Errorf(codes.Internal, "could not write directory %s to disk: %v", walletDir, err) 56 } 57 } 58 // Write the password hash to disk. 59 if err := s.SaveHashedPassword(req.Password); err != nil { 60 return nil, status.Errorf(codes.Internal, "could not write hashed password to disk: %v", err) 61 } 62 return s.sendAuthResponse() 63 } 64 65 // Login to authenticate with the validator RPC API using a password. 66 func (s *Server) Login(ctx context.Context, req *pb.AuthRequest) (*pb.AuthResponse, error) { 67 walletDir := s.walletDir 68 // We check the strength of the password to ensure it is high-entropy, 69 // has the required character count, and contains only unicode characters. 70 if err := promptutil.ValidatePasswordInput(req.Password); err != nil { 71 return nil, status.Errorf(codes.InvalidArgument, "Could not validate RPC password input: %v", err) 72 } 73 hashedPasswordPath := filepath.Join(walletDir, HashedRPCPassword) 74 if !fileutil.FileExists(hashedPasswordPath) { 75 return nil, status.Error(codes.Internal, "Could not find hashed password on disk") 76 } 77 hashedPassword, err := fileutil.ReadFileAsBytes(hashedPasswordPath) 78 if err != nil { 79 return nil, status.Error(codes.Internal, "Could not retrieve hashed password from disk") 80 } 81 // Compare the stored hashed password, with the hashed version of the password that was received. 82 if err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(req.Password)); err != nil { 83 return nil, status.Error(codes.Unauthenticated, "Incorrect validator RPC password") 84 } 85 return s.sendAuthResponse() 86 } 87 88 // HasUsedWeb checks if the user has authenticated via the web interface. 89 func (s *Server) HasUsedWeb(ctx context.Context, _ *empty.Empty) (*pb.HasUsedWebResponse, error) { 90 walletExists, err := wallet.Exists(s.walletDir) 91 if err != nil { 92 return nil, status.Error(codes.Internal, "Could not check if wallet exists") 93 } 94 hashedPasswordPath := filepath.Join(s.walletDir, HashedRPCPassword) 95 return &pb.HasUsedWebResponse{ 96 HasSignedUp: fileutil.FileExists(hashedPasswordPath), 97 HasWallet: walletExists, 98 }, nil 99 } 100 101 // Logout a user by invalidating their JWT key. 102 func (s *Server) Logout(ctx context.Context, _ *empty.Empty) (*empty.Empty, error) { 103 // Invalidate the old JWT key, making all requests done with its token fail. 104 jwtKey, err := createRandomJWTKey() 105 if err != nil { 106 return nil, status.Error(codes.Internal, "Could not invalidate JWT key") 107 } 108 s.jwtKey = jwtKey 109 return &empty.Empty{}, nil 110 } 111 112 // Sends an auth response via gRPC containing a new JWT token. 113 func (s *Server) sendAuthResponse() (*pb.AuthResponse, error) { 114 // If everything is fine here, construct the auth token. 115 tokenString, expirationTime, err := s.createTokenString() 116 if err != nil { 117 return nil, status.Error(codes.Internal, "Could not create jwt token string") 118 } 119 return &pb.AuthResponse{ 120 Token: tokenString, 121 TokenExpiration: expirationTime, 122 }, nil 123 } 124 125 // ChangePassword allows changing the RPC password via the API as an authenticated method. 126 func (s *Server) ChangePassword(ctx context.Context, req *pb.ChangePasswordRequest) (*empty.Empty, error) { 127 if req.CurrentPassword == "" { 128 return nil, status.Error(codes.InvalidArgument, "Current password cannot be empty") 129 } 130 hashedPasswordPath := filepath.Join(s.walletDir, HashedRPCPassword) 131 if !fileutil.FileExists(hashedPasswordPath) { 132 return nil, status.Error(codes.FailedPrecondition, "Could not compare password from disk") 133 } 134 hashedPassword, err := fileutil.ReadFileAsBytes(hashedPasswordPath) 135 if err != nil { 136 return nil, status.Error(codes.FailedPrecondition, "Could not retrieve hashed password from disk") 137 } 138 if err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(req.CurrentPassword)); err != nil { 139 return nil, status.Error(codes.Unauthenticated, "Incorrect password") 140 } 141 if req.Password != req.PasswordConfirmation { 142 return nil, status.Error(codes.InvalidArgument, "Password does not match confirmation") 143 } 144 if err := promptutil.ValidatePasswordInput(req.Password); err != nil { 145 return nil, status.Errorf(codes.InvalidArgument, "Could not validate password input: %v", err) 146 } 147 // Write the new password hash to disk. 148 if err := s.SaveHashedPassword(req.Password); err != nil { 149 return nil, status.Errorf(codes.Internal, "could not write hashed password to disk: %v", err) 150 } 151 return &empty.Empty{}, nil 152 } 153 154 // SaveHashedPassword to disk for the validator RPC. 155 func (s *Server) SaveHashedPassword(password string) error { 156 hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), hashCost) 157 if err != nil { 158 return errors.Wrap(err, "could not generate hashed password") 159 } 160 hashFilePath := filepath.Join(s.walletDir, HashedRPCPassword) 161 return fileutil.WriteFile(hashFilePath, hashedPassword) 162 } 163 164 // Interval in which we should check if a user has not yet used the RPC Signup endpoint 165 // which means they are using the --web flag and someone could come in and signup for them 166 // if they have their web host:port exposed to the Internet. 167 func (s *Server) checkUserSignup(ctx context.Context) { 168 ticker := time.NewTicker(checkUserSignupInterval) 169 defer ticker.Stop() 170 for { 171 select { 172 case <-ticker.C: 173 hashedPasswordPath := filepath.Join(s.walletDir, HashedRPCPassword) 174 if fileutil.FileExists(hashedPasswordPath) { 175 return 176 } 177 log.Warnf( 178 "You are using the --web option but have not yet signed via a browser. "+ 179 "If your web host and port are exposed to the Internet, someone else can attempt to sign up "+ 180 "for you! You can visit http://%s:%d to view the Prysm web interface", 181 s.validatorGatewayHost, 182 s.validatorGatewayPort, 183 ) 184 case <-s.ctx.Done(): 185 return 186 } 187 } 188 } 189 190 // Creates a JWT token string using the JWT key with an expiration timestamp. 191 func (s *Server) createTokenString() (string, uint64, error) { 192 // Create a new token object, specifying signing method and the claims 193 // you would like it to contain. 194 expirationTime := timeutils.Now().Add(tokenExpiryLength) 195 token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{ 196 ExpiresAt: expirationTime.Unix(), 197 }) 198 // Sign and get the complete encoded token as a string using the secret 199 tokenString, err := token.SignedString(s.jwtKey) 200 if err != nil { 201 return "", 0, err 202 } 203 return tokenString, uint64(expirationTime.Unix()), nil 204 }