authelia/internal/model/totp_configuration.go

163 lines
4.5 KiB
Go

package model
import (
"database/sql"
"encoding/base64"
"image"
"net/url"
"strconv"
"time"
"github.com/pquerna/otp"
"gopkg.in/yaml.v3"
)
// TOTPConfiguration represents a users TOTP configuration row in the database.
type TOTPConfiguration struct {
ID int `db:"id" json:"-"`
CreatedAt time.Time `db:"created_at" json:"-"`
LastUsedAt sql.NullTime `db:"last_used_at" json:"-"`
Username string `db:"username" json:"-"`
Issuer string `db:"issuer" json:"-"`
Algorithm string `db:"algorithm" json:"-"`
Digits uint `db:"digits" json:"digits"`
Period uint `db:"period" json:"period"`
Secret []byte `db:"secret" json:"-"`
}
// LastUsed provides LastUsedAt as a *time.Time instead of sql.NullTime.
func (c *TOTPConfiguration) LastUsed() *time.Time {
if c.LastUsedAt.Valid {
value := time.Unix(c.LastUsedAt.Time.Unix(), int64(c.LastUsedAt.Time.Nanosecond()))
return &value
}
return nil
}
// URI shows the configuration in the URI representation.
func (c *TOTPConfiguration) URI() (uri string) {
v := url.Values{}
v.Set("secret", string(c.Secret))
v.Set("issuer", c.Issuer)
v.Set("period", strconv.FormatUint(uint64(c.Period), 10))
v.Set("algorithm", c.Algorithm)
v.Set("digits", strconv.Itoa(int(c.Digits)))
u := url.URL{
Scheme: "otpauth",
Host: "totp",
Path: "/" + c.Issuer + ":" + c.Username,
RawQuery: v.Encode(),
}
return u.String()
}
// UpdateSignInInfo adjusts the values of the TOTPConfiguration after a sign in.
func (c *TOTPConfiguration) UpdateSignInInfo(now time.Time) {
c.LastUsedAt = sql.NullTime{Time: now, Valid: true}
}
// Key returns the *otp.Key using TOTPConfiguration.URI with otp.NewKeyFromURL.
func (c *TOTPConfiguration) Key() (key *otp.Key, err error) {
return otp.NewKeyFromURL(c.URI())
}
// Image returns the image.Image of the TOTPConfiguration using the Image func from the return of TOTPConfiguration.Key.
func (c *TOTPConfiguration) Image(width, height int) (img image.Image, err error) {
var key *otp.Key
if key, err = c.Key(); err != nil {
return nil, err
}
return key.Image(width, height)
}
// ToData converts this TOTPConfiguration into the data format for exporting etc.
func (c *TOTPConfiguration) ToData() TOTPConfigurationData {
return TOTPConfigurationData{
CreatedAt: c.CreatedAt,
LastUsedAt: c.LastUsed(),
Username: c.Username,
Issuer: c.Issuer,
Algorithm: c.Algorithm,
Digits: c.Digits,
Period: c.Period,
Secret: base64.StdEncoding.EncodeToString(c.Secret),
}
}
// MarshalYAML marshals this model into YAML.
func (c *TOTPConfiguration) MarshalYAML() (any, error) {
return c.ToData(), nil
}
// UnmarshalYAML unmarshalls YAML into this model.
func (c *TOTPConfiguration) UnmarshalYAML(value *yaml.Node) (err error) {
o := &TOTPConfigurationData{}
if err = value.Decode(o); err != nil {
return err
}
if c.Secret, err = base64.StdEncoding.DecodeString(o.Secret); err != nil {
return err
}
c.CreatedAt = o.CreatedAt
c.Username = o.Username
c.Issuer = o.Issuer
c.Algorithm = o.Algorithm
c.Digits = o.Digits
c.Period = o.Period
if o.LastUsedAt != nil {
c.LastUsedAt = sql.NullTime{Valid: true, Time: *o.LastUsedAt}
}
return nil
}
// TOTPConfigurationData is used for marshalling/unmarshalling tasks.
type TOTPConfigurationData struct {
CreatedAt time.Time `yaml:"created_at"`
LastUsedAt *time.Time `yaml:"last_used_at"`
Username string `yaml:"username"`
Issuer string `yaml:"issuer"`
Algorithm string `yaml:"algorithm"`
Digits uint `yaml:"digits"`
Period uint `yaml:"period"`
Secret string `yaml:"secret"`
}
// TOTPConfigurationDataExport represents a TOTPConfiguration export file.
type TOTPConfigurationDataExport struct {
TOTPConfigurations []TOTPConfigurationData `yaml:"totp_configurations"`
}
// TOTPConfigurationExport represents a TOTPConfiguration export file.
type TOTPConfigurationExport struct {
TOTPConfigurations []TOTPConfiguration `yaml:"totp_configurations"`
}
// ToData converts this TOTPConfigurationExport into a TOTPConfigurationDataExport.
func (export TOTPConfigurationExport) ToData() TOTPConfigurationDataExport {
data := TOTPConfigurationDataExport{
TOTPConfigurations: make([]TOTPConfigurationData, len(export.TOTPConfigurations)),
}
for i, config := range export.TOTPConfigurations {
data.TOTPConfigurations[i] = config.ToData()
}
return data
}
// MarshalYAML marshals this model into YAML.
func (export TOTPConfigurationExport) MarshalYAML() (any, error) {
return export.ToData(), nil
}