github.com/status-im/status-go@v1.1.0/server/pairing/challenge_management.go (about)

     1  package pairing
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net"
     9  	"net/http"
    10  
    11  	"github.com/btcsuite/btcutil/base58"
    12  	"github.com/gorilla/sessions"
    13  	"go.uber.org/zap"
    14  )
    15  
    16  const (
    17  	// Session names
    18  	sessionChallenge = "challenge"
    19  	sessionBlocked   = "blocked"
    20  )
    21  
    22  type ChallengeError struct {
    23  	Text     string
    24  	HTTPCode int
    25  }
    26  
    27  func (ce *ChallengeError) Error() string {
    28  	return fmt.Sprintf("%s : %d", ce.Text, ce.HTTPCode)
    29  }
    30  
    31  func makeCookieStore() (*sessions.CookieStore, error) {
    32  	auth := make([]byte, 64)
    33  	_, err := rand.Read(auth)
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  
    38  	enc := make([]byte, 32)
    39  	_, err = rand.Read(enc)
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  
    44  	return sessions.NewCookieStore(auth, enc), nil
    45  }
    46  
    47  // ChallengeGiver is responsible for generating challenges and checking challenge responses
    48  type ChallengeGiver struct {
    49  	cookieStore *sessions.CookieStore
    50  	encryptor   *PayloadEncryptor
    51  	logger      *zap.Logger
    52  	authedIP    net.IP
    53  }
    54  
    55  func NewChallengeGiver(e *PayloadEncryptor, logger *zap.Logger) (*ChallengeGiver, error) {
    56  	cs, err := makeCookieStore()
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	return &ChallengeGiver{
    62  		cookieStore: cs,
    63  		encryptor:   e.Renew(),
    64  		logger:      logger,
    65  	}, nil
    66  }
    67  
    68  func (cg *ChallengeGiver) getIP(r *http.Request) (net.IP, error) {
    69  	h, _, err := net.SplitHostPort(r.RemoteAddr)
    70  	if err != nil {
    71  		cg.logger.Error("getIP: h, _, err := net.SplitHostPort(r.RemoteAddr)", zap.Error(err), zap.String("r.RemoteAddr", r.RemoteAddr))
    72  		return nil, &ChallengeError{"error", http.StatusInternalServerError}
    73  	}
    74  	return net.ParseIP(h), nil
    75  }
    76  
    77  func (cg *ChallengeGiver) registerClientIP(r *http.Request) error {
    78  	IP, err := cg.getIP(r)
    79  	if err != nil {
    80  		return err
    81  	}
    82  	cg.authedIP = IP
    83  	return nil
    84  }
    85  
    86  func (cg *ChallengeGiver) validateClientIP(r *http.Request) error {
    87  	// If we haven't registered yet register the IP
    88  	if cg.authedIP == nil || len(cg.authedIP) == 0 {
    89  		err := cg.registerClientIP(r)
    90  		if err != nil {
    91  			return err
    92  		}
    93  	}
    94  
    95  	// Then compare the current req RemoteIP with the authed IP
    96  	IP, err := cg.getIP(r)
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	if !cg.authedIP.Equal(IP) {
   102  		cg.logger.Error(
   103  			"request RemoteAddr does not match authedIP: expected '%s', received '%s'",
   104  			zap.String("expected", cg.authedIP.String()),
   105  			zap.String("received", IP.String()),
   106  		)
   107  		return &ChallengeError{"forbidden", http.StatusForbidden}
   108  	}
   109  	return nil
   110  }
   111  
   112  func (cg *ChallengeGiver) getSession(r *http.Request) (*sessions.Session, error) {
   113  	s, err := cg.cookieStore.Get(r, sessionChallenge)
   114  	if err != nil {
   115  		cg.logger.Error("checkChallengeResponse: cg.cookieStore.Get(r, sessionChallenge)", zap.Error(err), zap.String("sessionChallenge", sessionChallenge))
   116  		return nil, &ChallengeError{"error", http.StatusInternalServerError}
   117  	}
   118  	return s, nil
   119  }
   120  
   121  func (cg *ChallengeGiver) generateNewChallenge(s *sessions.Session, w http.ResponseWriter, r *http.Request) ([]byte, error) {
   122  	challenge := make([]byte, 64)
   123  	_, err := rand.Read(challenge)
   124  	if err != nil {
   125  		cg.logger.Error("regenerateNewChallenge: _, err = rand.Read(challenge)", zap.Error(err))
   126  		return nil, &ChallengeError{"error", http.StatusInternalServerError}
   127  	}
   128  
   129  	s.Values[sessionChallenge] = challenge
   130  	err = s.Save(r, w)
   131  	if err != nil {
   132  		cg.logger.Error("regenerateNewChallenge: err = s.Save(r, w)", zap.Error(err))
   133  		return nil, &ChallengeError{"error", http.StatusInternalServerError}
   134  	}
   135  
   136  	return challenge, nil
   137  }
   138  
   139  func (cg *ChallengeGiver) block(s *sessions.Session, w http.ResponseWriter, r *http.Request) error {
   140  	s.Values[sessionBlocked] = true
   141  	err := s.Save(r, w)
   142  	if err != nil {
   143  		cg.logger.Error("block: err = s.Save(r, w)", zap.Error(err))
   144  		return &ChallengeError{"error", http.StatusInternalServerError}
   145  	}
   146  
   147  	return &ChallengeError{"forbidden", http.StatusForbidden}
   148  }
   149  
   150  func (cg *ChallengeGiver) checkChallengeResponse(w http.ResponseWriter, r *http.Request) error {
   151  	err := cg.validateClientIP(r)
   152  	if err != nil {
   153  		return err
   154  	}
   155  
   156  	s, err := cg.getSession(r)
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	blocked, ok := s.Values[sessionBlocked].(bool)
   162  	if ok && blocked {
   163  		return &ChallengeError{"forbidden", http.StatusForbidden}
   164  	}
   165  
   166  	// If the request header doesn't include a challenge don't punish the client, just throw a 403
   167  	clientChallengeResp := r.Header.Get(sessionChallenge)
   168  	if clientChallengeResp == "" {
   169  		return &ChallengeError{"forbidden", http.StatusForbidden}
   170  	}
   171  
   172  	dcr, err := cg.encryptor.decryptPlain(base58.Decode(clientChallengeResp))
   173  	if err != nil {
   174  		cg.logger.Error("checkChallengeResponse: cg.encryptor.decryptPlain(base58.Decode(clientChallengeResp))", zap.Error(err), zap.String("clientChallengeResp", clientChallengeResp))
   175  		return &ChallengeError{"error", http.StatusInternalServerError}
   176  	}
   177  
   178  	// If the challenge is not in the session store don't punish the client, just throw a 403
   179  	challenge, ok := s.Values[sessionChallenge].([]byte)
   180  	if !ok {
   181  		return &ChallengeError{"forbidden", http.StatusForbidden}
   182  	}
   183  
   184  	// Only if we have both a challenge in the session store and in the request header
   185  	// do we entertain blocking the client. Because then we know someone is trying to be sneaky.
   186  	if !bytes.Equal(dcr, challenge) {
   187  		return cg.block(s, w, r)
   188  	}
   189  
   190  	// If every is ok, generate a new challenge for the next req
   191  	_, err = cg.generateNewChallenge(s, w, r)
   192  	return err
   193  }
   194  
   195  func (cg *ChallengeGiver) getChallenge(w http.ResponseWriter, r *http.Request) ([]byte, error) {
   196  	err := cg.validateClientIP(r)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	s, err := cg.getSession(r)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	challenge, ok := s.Values[sessionChallenge].([]byte)
   207  	if !ok {
   208  		challenge, err = cg.generateNewChallenge(s, w, r)
   209  		if err != nil {
   210  			return nil, err
   211  		}
   212  	}
   213  	return challenge, nil
   214  }
   215  
   216  // ChallengeTaker is responsible for storing and performing server challenges
   217  type ChallengeTaker struct {
   218  	encryptor       *PayloadEncryptor
   219  	serverChallenge []byte
   220  }
   221  
   222  func NewChallengeTaker(e *PayloadEncryptor) *ChallengeTaker {
   223  	return &ChallengeTaker{
   224  		encryptor: e.Renew(),
   225  	}
   226  }
   227  
   228  func (ct *ChallengeTaker) SetChallenge(resp *http.Response) error {
   229  	challenge, err := ioutil.ReadAll(resp.Body)
   230  	if err != nil {
   231  		return err
   232  	}
   233  	ct.serverChallenge = challenge
   234  	return nil
   235  }
   236  
   237  func (ct *ChallengeTaker) DoChallenge(req *http.Request) error {
   238  	if ct.serverChallenge != nil {
   239  		ec, err := ct.encryptor.encryptPlain(ct.serverChallenge)
   240  		if err != nil {
   241  			return err
   242  		}
   243  
   244  		req.Header.Set(sessionChallenge, base58.Encode(ec))
   245  	}
   246  	return nil
   247  }