// Copyright 2025 The Gogs Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package database import ( "context" "encoding/json" "time" "github.com/pkg/errors" "gorm.io/gorm" ) // GPGKey represents a GPG key for commit signature verification. type GPGKey struct { ID int64 `gorm:"primaryKey"` OwnerID int64 `gorm:"index;not null"` KeyID string `gorm:"type:VARCHAR(16);unique;not null"` // Short key ID (8 or 16 chars) Fingerprint string `gorm:"type:VARCHAR(40);not null"` // Full 40-character fingerprint Content string `gorm:"type:TEXT;not null"` // ASCII-armored public key CanSign bool `gorm:"not null;default:false"` CanEncrypt bool `gorm:"not null;default:false"` Emails string `gorm:"type:TEXT"` // JSON array of email addresses Created time.Time `gorm:"-"` CreatedUnix int64 Updated time.Time `gorm:"-"` UpdatedUnix int64 Expired time.Time `gorm:"-"` ExpiredUnix int64 HasRecentActivity bool `gorm:"-" json:"-"` } // BeforeCreate implements the GORM create hook. func (k *GPGKey) BeforeCreate(tx *gorm.DB) error { if k.CreatedUnix == 0 { k.CreatedUnix = tx.NowFunc().Unix() } if k.UpdatedUnix == 0 { k.UpdatedUnix = k.CreatedUnix } return nil } // BeforeUpdate implements the GORM update hook. func (k *GPGKey) BeforeUpdate(tx *gorm.DB) error { k.UpdatedUnix = tx.NowFunc().Unix() return nil } // AfterFind implements the GORM query hook. func (k *GPGKey) AfterFind(tx *gorm.DB) error { k.Created = time.Unix(k.CreatedUnix, 0).Local() k.Updated = time.Unix(k.UpdatedUnix, 0).Local() if k.ExpiredUnix > 0 { k.Expired = time.Unix(k.ExpiredUnix, 0).Local() } return nil } // GetEmails returns the list of email addresses associated with this GPG key. func (k *GPGKey) GetEmails() ([]string, error) { if k.Emails == "" { return []string{}, nil } var emails []string err := json.Unmarshal([]byte(k.Emails), &emails) if err != nil { return nil, errors.Wrap(err, "unmarshal emails") } return emails, nil } // SetEmails sets the list of email addresses for this GPG key. func (k *GPGKey) SetEmails(emails []string) error { data, err := json.Marshal(emails) if err != nil { return errors.Wrap(err, "marshal emails") } k.Emails = string(data) return nil } // IsExpired returns true if the key has expired. func (k *GPGKey) IsExpired() bool { return k.ExpiredUnix > 0 && time.Now().Unix() > k.ExpiredUnix } // GPGKeysStore is the storage layer for GPG keys. type GPGKeysStore struct { db *gorm.DB } func newGPGKeysStore(db *gorm.DB) *GPGKeysStore { return &GPGKeysStore{db: db} } // Create creates a new GPG key for the given user. func (s *GPGKeysStore) Create(ctx context.Context, ownerID int64, keyID, fingerprint, content string, emails []string, canSign, canEncrypt bool, expiredUnix int64) (*GPGKey, error) { key := &GPGKey{ OwnerID: ownerID, KeyID: keyID, Fingerprint: fingerprint, Content: content, CanSign: canSign, CanEncrypt: canEncrypt, ExpiredUnix: expiredUnix, } if err := key.SetEmails(emails); err != nil { return nil, errors.Wrap(err, "set emails") } err := s.db.WithContext(ctx).Create(key).Error if err != nil { return nil, errors.Wrap(err, "create") } return key, nil } // GetByID returns the GPG key with the given ID. func (s *GPGKeysStore) GetByID(ctx context.Context, id int64) (*GPGKey, error) { var key GPGKey err := s.db.WithContext(ctx).Where("id = ?", id).First(&key).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, ErrGPGKeyNotExist{args: map[string]any{"keyID": id}} } return nil, errors.Wrap(err, "get") } return &key, nil } // GetByKeyID returns the GPG key with the given key ID. func (s *GPGKeysStore) GetByKeyID(ctx context.Context, keyID string) (*GPGKey, error) { var key GPGKey err := s.db.WithContext(ctx).Where("key_id = ?", keyID).First(&key).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, ErrGPGKeyNotExist{args: map[string]any{"keyID": keyID}} } return nil, errors.Wrap(err, "get") } return &key, nil } // GetByFingerprint returns the GPG key with the given fingerprint. func (s *GPGKeysStore) GetByFingerprint(ctx context.Context, fingerprint string) (*GPGKey, error) { var key GPGKey err := s.db.WithContext(ctx).Where("fingerprint = ?", fingerprint).First(&key).Error if err != nil { if err == gorm.ErrRecordNotFound { return nil, ErrGPGKeyNotExist{args: map[string]any{"fingerprint": fingerprint}} } return nil, errors.Wrap(err, "get") } return &key, nil } // List returns all GPG keys for the given user. func (s *GPGKeysStore) List(ctx context.Context, ownerID int64) ([]*GPGKey, error) { var keys []*GPGKey err := s.db.WithContext(ctx).Where("owner_id = ?", ownerID).Order("created_unix DESC").Find(&keys).Error if err != nil { return nil, errors.Wrap(err, "list") } return keys, nil } // Delete deletes the GPG key with the given ID. func (s *GPGKeysStore) Delete(ctx context.Context, id int64) error { result := s.db.WithContext(ctx).Where("id = ?", id).Delete(&GPGKey{}) if result.Error != nil { return errors.Wrap(result.Error, "delete") } if result.RowsAffected == 0 { return ErrGPGKeyNotExist{args: map[string]any{"keyID": id}} } return nil } // DeleteByOwnerID deletes all GPG keys for the given user. func (s *GPGKeysStore) DeleteByOwnerID(ctx context.Context, ownerID int64) error { return s.db.WithContext(ctx).Where("owner_id = ?", ownerID).Delete(&GPGKey{}).Error }