2022-03-06 05:47:40 +00:00
|
|
|
package model
|
2022-03-03 11:20:43 +00:00
|
|
|
|
|
|
|
import (
|
2022-10-20 02:16:36 +00:00
|
|
|
"database/sql"
|
2022-12-23 04:00:23 +00:00
|
|
|
"encoding/base64"
|
2022-03-03 11:20:43 +00:00
|
|
|
"encoding/hex"
|
2023-04-14 16:54:24 +00:00
|
|
|
"encoding/json"
|
2022-03-03 11:20:43 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2022-03-03 23:46:38 +00:00
|
|
|
"github.com/go-webauthn/webauthn/protocol"
|
|
|
|
"github.com/go-webauthn/webauthn/webauthn"
|
2022-03-03 11:20:43 +00:00
|
|
|
"github.com/google/uuid"
|
2022-12-23 04:00:23 +00:00
|
|
|
"gopkg.in/yaml.v3"
|
2022-03-03 11:20:43 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
attestationTypeFIDOU2F = "fido-u2f"
|
|
|
|
)
|
|
|
|
|
2023-04-10 07:01:23 +00:00
|
|
|
// WebAuthnUser is an object to represent a user for the WebAuthn lib.
|
|
|
|
type WebAuthnUser struct {
|
2023-04-14 16:54:24 +00:00
|
|
|
ID int `db:"id"`
|
|
|
|
RPID string `db:"rpid"`
|
|
|
|
Username string `db:"username"`
|
|
|
|
UserID string `db:"userid"`
|
|
|
|
DisplayName string `db:"-"`
|
|
|
|
|
|
|
|
Devices []WebAuthnDevice `db:"-"`
|
2022-03-03 11:20:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// HasFIDOU2F returns true if the user has any attestation type `fido-u2f` devices.
|
2023-04-10 07:01:23 +00:00
|
|
|
func (w WebAuthnUser) HasFIDOU2F() bool {
|
2022-03-03 11:20:43 +00:00
|
|
|
for _, c := range w.Devices {
|
|
|
|
if c.AttestationType == attestationTypeFIDOU2F {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// WebAuthnID implements the webauthn.User interface.
|
2023-04-10 07:01:23 +00:00
|
|
|
func (w WebAuthnUser) WebAuthnID() []byte {
|
2023-04-14 16:54:24 +00:00
|
|
|
return []byte(w.UserID)
|
2022-03-03 11:20:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// WebAuthnName implements the webauthn.User interface.
|
2023-04-10 07:01:23 +00:00
|
|
|
func (w WebAuthnUser) WebAuthnName() string {
|
2022-03-03 11:20:43 +00:00
|
|
|
return w.Username
|
|
|
|
}
|
|
|
|
|
|
|
|
// WebAuthnDisplayName implements the webauthn.User interface.
|
2023-04-10 07:01:23 +00:00
|
|
|
func (w WebAuthnUser) WebAuthnDisplayName() string {
|
2022-03-03 11:20:43 +00:00
|
|
|
return w.DisplayName
|
|
|
|
}
|
|
|
|
|
|
|
|
// WebAuthnIcon implements the webauthn.User interface.
|
2023-04-10 07:01:23 +00:00
|
|
|
func (w WebAuthnUser) WebAuthnIcon() string {
|
2022-03-03 11:20:43 +00:00
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// WebAuthnCredentials implements the webauthn.User interface.
|
2023-04-10 07:01:23 +00:00
|
|
|
func (w WebAuthnUser) WebAuthnCredentials() (credentials []webauthn.Credential) {
|
2022-03-03 11:20:43 +00:00
|
|
|
credentials = make([]webauthn.Credential, len(w.Devices))
|
|
|
|
|
|
|
|
var credential webauthn.Credential
|
|
|
|
|
|
|
|
for i, device := range w.Devices {
|
|
|
|
aaguid, err := device.AAGUID.MarshalBinary()
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
credential = webauthn.Credential{
|
|
|
|
ID: device.KID.Bytes(),
|
|
|
|
PublicKey: device.PublicKey,
|
|
|
|
AttestationType: device.AttestationType,
|
|
|
|
Authenticator: webauthn.Authenticator{
|
|
|
|
AAGUID: aaguid,
|
|
|
|
SignCount: device.SignCount,
|
|
|
|
CloneWarning: device.CloneWarning,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
transports := strings.Split(device.Transport, ",")
|
|
|
|
credential.Transport = []protocol.AuthenticatorTransport{}
|
|
|
|
|
|
|
|
for _, t := range transports {
|
|
|
|
if t == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
credential.Transport = append(credential.Transport, protocol.AuthenticatorTransport(t))
|
|
|
|
}
|
|
|
|
|
|
|
|
credentials[i] = credential
|
|
|
|
}
|
|
|
|
|
|
|
|
return credentials
|
|
|
|
}
|
|
|
|
|
|
|
|
// WebAuthnCredentialDescriptors decodes the users credentials into protocol.CredentialDescriptor's.
|
2023-04-10 07:01:23 +00:00
|
|
|
func (w WebAuthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.CredentialDescriptor) {
|
2022-03-03 11:20:43 +00:00
|
|
|
credentials := w.WebAuthnCredentials()
|
|
|
|
|
|
|
|
descriptors = make([]protocol.CredentialDescriptor, len(credentials))
|
|
|
|
|
|
|
|
for i, credential := range credentials {
|
|
|
|
descriptors[i] = credential.Descriptor()
|
|
|
|
}
|
|
|
|
|
|
|
|
return descriptors
|
|
|
|
}
|
|
|
|
|
2023-04-10 07:01:23 +00:00
|
|
|
// NewWebAuthnDeviceFromCredential creates a WebAuthnDevice from a webauthn.Credential.
|
|
|
|
func NewWebAuthnDeviceFromCredential(rpid, username, description string, credential *webauthn.Credential) (device WebAuthnDevice) {
|
2022-03-03 11:20:43 +00:00
|
|
|
transport := make([]string, len(credential.Transport))
|
|
|
|
|
|
|
|
for i, t := range credential.Transport {
|
|
|
|
transport[i] = string(t)
|
|
|
|
}
|
|
|
|
|
2023-04-10 07:01:23 +00:00
|
|
|
device = WebAuthnDevice{
|
2022-03-03 11:20:43 +00:00
|
|
|
RPID: rpid,
|
|
|
|
Username: username,
|
|
|
|
CreatedAt: time.Now(),
|
|
|
|
Description: description,
|
|
|
|
KID: NewBase64(credential.ID),
|
|
|
|
AttestationType: credential.AttestationType,
|
2023-04-14 16:54:24 +00:00
|
|
|
Transport: strings.Join(transport, ","),
|
2022-03-03 11:20:43 +00:00
|
|
|
SignCount: credential.Authenticator.SignCount,
|
|
|
|
CloneWarning: credential.Authenticator.CloneWarning,
|
2023-04-14 16:54:24 +00:00
|
|
|
PublicKey: credential.PublicKey,
|
2022-03-03 11:20:43 +00:00
|
|
|
}
|
|
|
|
|
2022-11-19 05:47:09 +00:00
|
|
|
aaguid, err := uuid.Parse(hex.EncodeToString(credential.Authenticator.AAGUID))
|
|
|
|
if err == nil && aaguid.ID() != 0 {
|
|
|
|
device.AAGUID = uuid.NullUUID{Valid: true, UUID: aaguid}
|
|
|
|
}
|
2022-03-03 11:20:43 +00:00
|
|
|
|
|
|
|
return device
|
|
|
|
}
|
|
|
|
|
2023-04-10 07:01:23 +00:00
|
|
|
// WebAuthnDevice represents a WebAuthn Device in the database storage.
|
|
|
|
type WebAuthnDevice struct {
|
2022-11-19 05:47:09 +00:00
|
|
|
ID int `db:"id"`
|
|
|
|
CreatedAt time.Time `db:"created_at"`
|
|
|
|
LastUsedAt sql.NullTime `db:"last_used_at"`
|
|
|
|
RPID string `db:"rpid"`
|
|
|
|
Username string `db:"username"`
|
|
|
|
Description string `db:"description"`
|
|
|
|
KID Base64 `db:"kid"`
|
2023-04-14 16:54:24 +00:00
|
|
|
AAGUID uuid.NullUUID `db:"aaguid"`
|
2022-11-19 05:47:09 +00:00
|
|
|
AttestationType string `db:"attestation_type"`
|
|
|
|
Transport string `db:"transport"`
|
|
|
|
SignCount uint32 `db:"sign_count"`
|
|
|
|
CloneWarning bool `db:"clone_warning"`
|
2023-04-14 16:54:24 +00:00
|
|
|
PublicKey []byte `db:"public_key"`
|
2022-03-03 11:20:43 +00:00
|
|
|
}
|
|
|
|
|
2023-04-10 07:01:23 +00:00
|
|
|
// UpdateSignInInfo adjusts the values of the WebAuthnDevice after a sign in.
|
|
|
|
func (d *WebAuthnDevice) UpdateSignInInfo(config *webauthn.Config, now time.Time, signCount uint32) {
|
2022-12-23 04:00:23 +00:00
|
|
|
d.LastUsedAt = sql.NullTime{Time: now, Valid: true}
|
2022-03-03 11:20:43 +00:00
|
|
|
|
2022-12-23 04:00:23 +00:00
|
|
|
d.SignCount = signCount
|
2022-03-03 11:20:43 +00:00
|
|
|
|
2022-12-23 04:00:23 +00:00
|
|
|
if d.RPID != "" {
|
2022-03-03 11:20:43 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-12-23 04:00:23 +00:00
|
|
|
switch d.AttestationType {
|
2022-03-03 11:20:43 +00:00
|
|
|
case attestationTypeFIDOU2F:
|
2023-04-02 06:09:18 +00:00
|
|
|
d.RPID = config.RPOrigin
|
2022-03-03 11:20:43 +00:00
|
|
|
default:
|
2022-12-23 04:00:23 +00:00
|
|
|
d.RPID = config.RPID
|
2022-03-03 11:20:43 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-23 04:00:23 +00:00
|
|
|
|
2023-04-14 16:54:24 +00:00
|
|
|
// DataValueLastUsedAt provides LastUsedAt as a *time.Time instead of sql.NullTime.
|
|
|
|
func (d *WebAuthnDevice) DataValueLastUsedAt() *time.Time {
|
2022-12-23 04:00:23 +00:00
|
|
|
if d.LastUsedAt.Valid {
|
2023-04-11 11:11:11 +00:00
|
|
|
value := time.Unix(d.LastUsedAt.Time.Unix(), int64(d.LastUsedAt.Time.Nanosecond()))
|
|
|
|
|
|
|
|
return &value
|
2022-12-23 04:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-14 16:54:24 +00:00
|
|
|
// DataValueAAGUID provides AAGUID as a *string instead of uuid.NullUUID.
|
|
|
|
func (d *WebAuthnDevice) DataValueAAGUID() *string {
|
|
|
|
if d.AAGUID.Valid {
|
|
|
|
value := d.AAGUID.UUID.String()
|
|
|
|
|
|
|
|
return &value
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-11 11:11:11 +00:00
|
|
|
func (d *WebAuthnDevice) ToData() WebAuthnDeviceData {
|
2023-04-14 16:54:24 +00:00
|
|
|
o := WebAuthnDeviceData{
|
|
|
|
ID: d.ID,
|
2022-12-23 04:00:23 +00:00
|
|
|
CreatedAt: d.CreatedAt,
|
2023-04-14 16:54:24 +00:00
|
|
|
LastUsedAt: d.DataValueLastUsedAt(),
|
2022-12-23 04:00:23 +00:00
|
|
|
RPID: d.RPID,
|
|
|
|
Username: d.Username,
|
|
|
|
Description: d.Description,
|
|
|
|
KID: d.KID.String(),
|
2023-04-14 16:54:24 +00:00
|
|
|
AAGUID: d.DataValueAAGUID(),
|
2022-12-23 04:00:23 +00:00
|
|
|
AttestationType: d.AttestationType,
|
|
|
|
SignCount: d.SignCount,
|
|
|
|
CloneWarning: d.CloneWarning,
|
2023-04-14 16:54:24 +00:00
|
|
|
PublicKey: base64.StdEncoding.EncodeToString(d.PublicKey),
|
2022-12-23 04:00:23 +00:00
|
|
|
}
|
2023-04-14 16:54:24 +00:00
|
|
|
|
|
|
|
if d.Transport != "" {
|
|
|
|
o.Transports = strings.Split(d.Transport, ",")
|
|
|
|
}
|
|
|
|
|
|
|
|
return o
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalJSON returns the WebAuthnDevice in a JSON friendly manner.
|
|
|
|
func (d *WebAuthnDevice) MarshalJSON() (data []byte, err error) {
|
|
|
|
return json.Marshal(d.ToData())
|
2023-04-11 11:11:11 +00:00
|
|
|
}
|
2022-12-23 04:00:23 +00:00
|
|
|
|
2023-04-11 11:11:11 +00:00
|
|
|
// MarshalYAML marshals this model into YAML.
|
|
|
|
func (d *WebAuthnDevice) MarshalYAML() (any, error) {
|
|
|
|
return d.ToData(), nil
|
2022-12-23 04:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// UnmarshalYAML unmarshalls YAML into this model.
|
2023-04-10 07:01:23 +00:00
|
|
|
func (d *WebAuthnDevice) UnmarshalYAML(value *yaml.Node) (err error) {
|
|
|
|
o := &WebAuthnDeviceData{}
|
2022-12-23 04:00:23 +00:00
|
|
|
|
|
|
|
if err = value.Decode(o); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if d.PublicKey, err = base64.StdEncoding.DecodeString(o.PublicKey); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var aaguid uuid.UUID
|
|
|
|
|
2023-04-14 16:54:24 +00:00
|
|
|
if o.AAGUID != nil {
|
|
|
|
if aaguid, err = uuid.Parse(*o.AAGUID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-12-23 04:00:23 +00:00
|
|
|
|
2023-04-14 16:54:24 +00:00
|
|
|
if aaguid.ID() != 0 {
|
|
|
|
d.AAGUID = uuid.NullUUID{Valid: true, UUID: aaguid}
|
|
|
|
}
|
2022-12-23 04:00:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var kid []byte
|
|
|
|
|
|
|
|
if kid, err = base64.StdEncoding.DecodeString(o.KID); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
d.KID = NewBase64(kid)
|
|
|
|
|
|
|
|
d.CreatedAt = o.CreatedAt
|
|
|
|
d.RPID = o.RPID
|
|
|
|
d.Username = o.Username
|
|
|
|
d.Description = o.Description
|
|
|
|
d.AttestationType = o.AttestationType
|
2023-04-14 16:54:24 +00:00
|
|
|
d.Transport = strings.Join(o.Transports, ",")
|
2022-12-23 04:00:23 +00:00
|
|
|
d.SignCount = o.SignCount
|
|
|
|
d.CloneWarning = o.CloneWarning
|
|
|
|
|
|
|
|
if o.LastUsedAt != nil {
|
|
|
|
d.LastUsedAt = sql.NullTime{Valid: true, Time: *o.LastUsedAt}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-10 07:01:23 +00:00
|
|
|
// WebAuthnDeviceData represents a WebAuthn Device in the database storage.
|
|
|
|
type WebAuthnDeviceData struct {
|
2023-04-14 16:54:24 +00:00
|
|
|
ID int `json:"id" yaml:"-"`
|
|
|
|
CreatedAt time.Time `json:"created_at" yaml:"created_at"`
|
|
|
|
LastUsedAt *time.Time `json:"last_used_at,omitempty" yaml:"last_used_at,omitempty"`
|
|
|
|
RPID string `json:"rpid" yaml:"rpid"`
|
|
|
|
Username string `json:"-" yaml:"username"`
|
|
|
|
Description string `json:"description" yaml:"description"`
|
|
|
|
KID string `json:"kid" yaml:"kid"`
|
|
|
|
AAGUID *string `json:"aaguid,omitempty" yaml:"aaguid,omitempty"`
|
|
|
|
AttestationType string `json:"attestation_type" yaml:"attestation_type"`
|
|
|
|
Transports []string `json:"transports" yaml:"transports"`
|
|
|
|
SignCount uint32 `json:"sign_count" yaml:"sign_count"`
|
|
|
|
CloneWarning bool `json:"clone_warning" yaml:"clone_warning"`
|
|
|
|
PublicKey string `json:"public_key" yaml:"public_key"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *WebAuthnDeviceData) ToDevice() (device *WebAuthnDevice, err error) {
|
|
|
|
device = &WebAuthnDevice{
|
|
|
|
CreatedAt: d.CreatedAt,
|
|
|
|
RPID: d.RPID,
|
|
|
|
Username: d.Username,
|
|
|
|
Description: d.Description,
|
|
|
|
AttestationType: d.AttestationType,
|
|
|
|
Transport: strings.Join(d.Transports, ","),
|
|
|
|
SignCount: d.SignCount,
|
|
|
|
CloneWarning: d.CloneWarning,
|
|
|
|
}
|
|
|
|
|
|
|
|
if device.PublicKey, err = base64.StdEncoding.DecodeString(d.PublicKey); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var aaguid uuid.UUID
|
|
|
|
|
|
|
|
if d.AAGUID != nil {
|
|
|
|
if aaguid, err = uuid.Parse(*d.AAGUID); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if aaguid.ID() != 0 {
|
|
|
|
device.AAGUID = uuid.NullUUID{Valid: true, UUID: aaguid}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var kid []byte
|
|
|
|
|
|
|
|
if kid, err = base64.StdEncoding.DecodeString(d.KID); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
device.KID = NewBase64(kid)
|
|
|
|
|
|
|
|
if d.LastUsedAt != nil {
|
|
|
|
device.LastUsedAt = sql.NullTime{Valid: true, Time: *d.LastUsedAt}
|
|
|
|
}
|
|
|
|
|
|
|
|
return device, nil
|
2022-12-23 04:00:23 +00:00
|
|
|
}
|
|
|
|
|
2023-04-10 07:01:23 +00:00
|
|
|
// WebAuthnDeviceExport represents a WebAuthnDevice export file.
|
|
|
|
type WebAuthnDeviceExport struct {
|
|
|
|
WebAuthnDevices []WebAuthnDevice `yaml:"webauthn_devices"`
|
2022-12-23 04:00:23 +00:00
|
|
|
}
|
2023-04-11 11:11:11 +00:00
|
|
|
|
|
|
|
// WebAuthnDeviceDataExport represents a WebAuthnDevice export file.
|
|
|
|
type WebAuthnDeviceDataExport struct {
|
|
|
|
WebAuthnDevices []WebAuthnDeviceData `yaml:"webauthn_devices"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// ToData converts this WebAuthnDeviceExport into a WebAuthnDeviceDataExport.
|
|
|
|
func (export WebAuthnDeviceExport) ToData() WebAuthnDeviceDataExport {
|
|
|
|
data := WebAuthnDeviceDataExport{
|
|
|
|
WebAuthnDevices: make([]WebAuthnDeviceData, len(export.WebAuthnDevices)),
|
|
|
|
}
|
|
|
|
|
|
|
|
for i, device := range export.WebAuthnDevices {
|
|
|
|
data.WebAuthnDevices[i] = device.ToData()
|
|
|
|
}
|
|
|
|
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
// MarshalYAML marshals this model into YAML.
|
|
|
|
func (export WebAuthnDeviceExport) MarshalYAML() (any, error) {
|
|
|
|
return export.ToData(), nil
|
|
|
|
}
|