github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/oauth/confirm_flagship.go (about)

     1  package oauth
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/base32"
     6  	"io"
     7  	"time"
     8  
     9  	"github.com/cozy/cozy-stack/model/instance"
    10  	"github.com/cozy/cozy-stack/model/job"
    11  	"github.com/cozy/cozy-stack/model/settings"
    12  	"github.com/cozy/cozy-stack/pkg/crypto"
    13  	"github.com/pquerna/otp"
    14  	"github.com/pquerna/otp/totp"
    15  	"golang.org/x/crypto/hkdf"
    16  )
    17  
    18  var macConfig = crypto.MACConfig{
    19  	Name:   "confirm-flagship",
    20  	MaxAge: 0,
    21  	MaxLen: 256,
    22  }
    23  
    24  var totpOptions = totp.ValidateOpts{
    25  	Period:    30, // 30s
    26  	Skew:      10, // 30s +- 10*30s = [-5min; 5,5min]
    27  	Digits:    otp.DigitsSix,
    28  	Algorithm: otp.AlgorithmSHA256,
    29  }
    30  
    31  // SendConfirmFlagshipCode sends by mail a code to the owner of the instance.
    32  // It returns the generated token which can be used to check the code.
    33  func SendConfirmFlagshipCode(inst *instance.Instance, clientID string) ([]byte, error) {
    34  	token, code, err := GenerateConfirmCode(inst, clientID)
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	publicName, _ := settings.PublicName(inst)
    40  	msg, err := job.NewMessage(map[string]interface{}{
    41  		"mode":          "noreply",
    42  		"template_name": "confirm_flagship",
    43  		"template_values": map[string]interface{}{
    44  			"Code":       code,
    45  			"PublicName": publicName,
    46  		},
    47  	})
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  	_, err = job.System().PushJob(inst, &job.JobRequest{
    52  		WorkerType: "sendmail",
    53  		Message:    msg,
    54  	})
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	return token, nil
    59  }
    60  
    61  // CheckFlagshipCode returns true if the code is correct and can be used to set
    62  // the flagship flag on the given OAuth client.
    63  func CheckFlagshipCode(inst *instance.Instance, clientID string, token []byte, code string) bool {
    64  	salt, err := crypto.DecodeAuthMessage(macConfig, inst.SessionSecret(), token, nil)
    65  	if err != nil {
    66  		return false
    67  	}
    68  
    69  	input := []byte(clientID + "-" + string(inst.SessionSecret()))
    70  	h := hkdf.New(sha256.New, input, salt, nil)
    71  	key := make([]byte, 32)
    72  	_, err = io.ReadFull(h, key)
    73  	if err != nil {
    74  		return false
    75  	}
    76  	secret := base32.StdEncoding.EncodeToString(key)
    77  
    78  	ok, err := totp.ValidateCustom(code, secret, time.Now().UTC(), totpOptions)
    79  	return ok && err == nil
    80  }
    81  
    82  // GenerateConfirmCode generate a 6-digits code and the token to check it. They
    83  // can be used to manually confirm that an OAuth client is the flagship app.
    84  func GenerateConfirmCode(inst *instance.Instance, clientID string) ([]byte, string, error) {
    85  	salt := crypto.GenerateRandomBytes(sha256.Size)
    86  	token, err := crypto.EncodeAuthMessage(macConfig, inst.SessionSecret(), salt, nil)
    87  	if err != nil {
    88  		return nil, "", err
    89  	}
    90  
    91  	input := []byte(clientID + "-" + string(inst.SessionSecret()))
    92  	h := hkdf.New(sha256.New, input, salt, nil)
    93  	key := make([]byte, 32)
    94  	_, err = io.ReadFull(h, key)
    95  	if err != nil {
    96  		return nil, "", err
    97  	}
    98  	secret := base32.StdEncoding.EncodeToString(key)
    99  
   100  	code, err := totp.GenerateCodeCustom(secret, time.Now().UTC(), totpOptions)
   101  	return token, code, err
   102  }