text: package bertyprotocol
  
  import (
  	"context"
  	"crypto/sha256"
  	"encoding/base64"
  	"encoding/json"
  	"fmt"
  	"io/ioutil"
  	"net/http"
  	"net/url"
  	"strings"
  
  	"golang.org/x/net/context/ctxhttp"
  
  	"berty.tech/berty/v2/go/internal/cryptoutil"
  	"berty.tech/berty/v2/go/pkg/bertytypes"
  	"berty.tech/berty/v2/go/pkg/errcode"
  )
  
  const (
  	AuthResponseType        = "code"
  	AuthGrantType           = "authorization_code"
  	AuthRedirect            = "berty://services-auth/"
  	AuthClientID            = "berty"
  	AuthCodeChallengeMethod = "S256"
  )
  
  type authExchangeResponse struct {
  	AccessToken      string            `json:"access_token"`
  	Scope            string            `json:"scope"`
  	Error            string            `json:"error"`
  	ErrorDescription string            `json:"error_description"`
  	Services         map[string]string `json:"services"`
  }
  
  type authSession struct {
  	state        string
  	codeVerifier string // codeVerifier base64 encoded random value
  	baseURL      string
  }
  
  func authSessionCodeChallenge(codeVerifier string) string {
  	codeChallengeArr := sha256.Sum256([]byte(codeVerifier))
  	codeChallenge := make([]byte, sha256.Size)
  	for i, c := range codeChallengeArr {
  		codeChallenge[i] = c
  	}
  
  	return base64.RawURLEncoding.EncodeToString(codeChallenge)
  }
  
  func authSessionCodeVerifierAndChallenge() (string, string, error) {
  	verifierArr, err := cryptoutil.GenerateNonce()
  	if err != nil {
  		return "", "", err
  	}
  
  	codeVerifierBytes := make([]byte, cryptoutil.NonceSize)
  	for i, c := range verifierArr {
  		codeVerifierBytes[i] = c
  	}
  
  	verifier := base64.RawURLEncoding.EncodeToString(codeVerifierBytes)
  
  	return verifier, authSessionCodeChallenge(verifier), nil
  }
  
  func newAuthSession(baseURL string) (*authSession, string, error) {
  	state, err := cryptoutil.GenerateNonce()
  	if err != nil {
  		return nil, "", err
  	}
  
  	verifier, challenge, err := authSessionCodeVerifierAndChallenge()
  	if err != nil {
  		return nil, "", err
  	}
  
  	stateBytes := make([]byte, cryptoutil.NonceSize)
  	for i, c := range state {
  		stateBytes[i] = c
  	}
  
  	auth := &authSession{
  		baseURL:      baseURL,
  		state:        base64.RawURLEncoding.EncodeToString(stateBytes),
  		codeVerifier: verifier,
  	}
  
  	return auth, challenge, nil
  }
  
  func (s *service) authInitURL(baseURL string) (string, error) {
  	parsedAuthURL, err := url.Parse(baseURL)
  	if err != nil {
  		return "", errcode.ErrServicesAuthInvalidURL
  	}
  
  	switch parsedAuthURL.Scheme {
  	case "http", "https":
  	default:
  		return "", errcode.ErrServicesAuthInvalidURL
  	}
  
  	if parsedAuthURL.Host == "" {
  		return "", errcode.ErrServicesAuthInvalidURL
  	}
  
  	if strings.HasSuffix(baseURL, "/") {
  		baseURL = baseURL[:len(baseURL)-1]
  	}
  
  	auth, codeChallenge, err := newAuthSession(baseURL)
  	if err != nil {
  		return "", err
  	}
  
  	s.authSession.Store(auth)
  
  	return fmt.Sprintf("%s%s?response_type=%s&client_id=%s&redirect_uri=%s&state=%s&code_challenge=%s&code_challenge_method=%s",
  		baseURL,
  		AuthHTTPPathAuthorize,
  		AuthResponseType,
  		AuthClientID,
  		url.QueryEscape(AuthRedirect),
  		auth.state,
  		codeChallenge,
  		AuthCodeChallengeMethod,
  	), nil
  }
  
  func (s *service) AuthServiceCompleteFlow(ctx context.Context, request *bertytypes.AuthServiceCompleteFlow_Request) (*bertytypes.AuthServiceCompleteFlow_Reply, error) {
  	u, err := url.Parse(request.CallbackURL)
  	if err != nil {
  		return nil, err
  	}
  
  	if e := u.Query().Get("error"); e != "" {
  		return nil, errcode.ErrServicesAuthServer.Wrap(fmt.Errorf("got error: %s (%s)", e, u.Query().Get("error_description")))
  	}
  
  	code, state := u.Query().Get("code"), u.Query().Get("state")
  
  	authUntyped := s.authSession.Load()
  	if authUntyped == nil {
  		return nil, errcode.ErrServicesAuthNotInitialized
  	}
  
  	auth, ok := authUntyped.(*authSession)
  	if !ok {
  		return nil, errcode.ErrServicesAuthNotInitialized
  	}
  
  	if auth.state != state {
  		return nil, errcode.ErrServicesAuthWrongState
  	}
  
  	endpoint := fmt.Sprintf("%s%s", auth.baseURL, AuthHTTPPathTokenExchange)
  	res, err := ctxhttp.PostForm(ctx, http.DefaultClient, endpoint, url.Values{
  		"grant_type":    {AuthGrantType},
  		"code":          {code},
  		"client_id":     {AuthClientID},
  		"code_verifier": {auth.codeVerifier},
  	})
  	if err != nil {
  		return nil, errcode.ErrStreamWrite.Wrap(err)
  	}
  
  	defer res.Body.Close()
  
  	if res.StatusCode >= 300 {
  		return nil, errcode.ErrServicesAuthInvalidResponse.Wrap(fmt.Errorf("invalid status code %d", res.StatusCode))
  	}
  
  	body, err := ioutil.ReadAll(res.Body)
  	if err != nil {
  		return nil, errcode.ErrStreamRead.Wrap(err)
  	}
  
  	resMsg := &authExchangeResponse{}
  	if err := json.Unmarshal(body, &resMsg); err != nil {
  		return nil, errcode.ErrDeserialization.Wrap(err)
  	}
  
  	if resMsg.Error != "" {
  		return nil, errcode.ErrServicesAuthServer.Wrap(err)
  	}
  
  	if resMsg.AccessToken == "" {
  		return nil, errcode.ErrServicesAuthInvalidResponse.Wrap(fmt.Errorf("missing access token in response"))
  	}
  
  	if len(resMsg.Services) == 0 {
  		return nil, errcode.ErrServicesAuthInvalidResponse.Wrap(fmt.Errorf("no services returned along token"))
  	}
  
  	services := make([]*bertytypes.ServiceTokenSupportedService, len(resMsg.Services))
  	i := 0
  	for k, v := range resMsg.Services {
  		services[i] = &bertytypes.ServiceTokenSupportedService{
  			ServiceType:     k,
  			ServiceEndpoint: v,
  		}
  		i++
  	}
  
  	if _, err := s.accountGroup.metadataStore.SendAccountServiceTokenAdded(ctx, &bertytypes.ServiceToken{
  		Token:             resMsg.AccessToken,
  		AuthenticationURL: auth.baseURL,
  		SupportedServices: services,
  		Expiration:        -1,
  	}); err != nil {
  		return nil, err
  	}
  
  	return &bertytypes.AuthServiceCompleteFlow_Reply{}, nil
  }
  
  func (s *service) AuthServiceInitFlow(ctx context.Context, request *bertytypes.AuthServiceInitFlow_Request) (*bertytypes.AuthServiceInitFlow_Reply, error) {
  	u, err := s.authInitURL(request.AuthURL)
  	if err != nil {
  		return nil, err
  	}
  
  	return &bertytypes.AuthServiceInitFlow_Reply{
  		URL:       u,
  		SecureURL: strings.HasPrefix(u, "https://"),
  	}, nil
  }
  
  func (s *service) ServicesTokenList(request *bertytypes.ServicesTokenList_Request, server ProtocolService_ServicesTokenListServer) error {
  	for _, t := range s.accountGroup.metadataStore.listServiceTokens() {
  		if server.Context().Err() != nil {
  			break
  		}
  
  		if err := server.Send(&bertytypes.ServicesTokenList_Reply{
  			TokenID: t.TokenID(),
  			Service: t,
  		}); err != nil {
  			return err
  		}
  	}
  
  	return nil
  }
  