mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/11909 Fixes #9629. New pull mirrors have credentials stored encrypted in the database, the same as push mirrors, rather than in the repository's `config` file. `git fetch` on the pull mirror is updated to use the credential store. Pull mirrors will have their credentials migrated to the encrypted storage in the database as they're synced or otherwise accessed via the web UI. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11984 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org> Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org> Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
160 lines
5.8 KiB
Go
160 lines
5.8 KiB
Go
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Keying is a module that allows for subkeys to be deterministically generated
|
|
// from the same master key. It allows for domain separation to take place by
|
|
// using new keys for new subsystems/domains. These subkeys are provided with
|
|
// an API to encrypt and decrypt data. The module panics if a bad interaction
|
|
// happened, the panic should be seen as an non-recoverable error.
|
|
//
|
|
// HKDF (per RFC 5869) is used to derive new subkeys in a safe manner. It
|
|
// provides a KDF security property, which is required for Forgejo, as the
|
|
// secret key would be an ASCII string and isn't a random uniform bit string.
|
|
// XChaCha-Poly1305 (per draft-irtf-cfrg-xchacha-01) is used as AEAD to encrypt
|
|
// and decrypt messages. A new fresh random nonce is generated for every
|
|
// encryption. The nonce gets prepended to the ciphertext.
|
|
package keying
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/cipher"
|
|
"crypto/hkdf"
|
|
crand "crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"errors"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
)
|
|
|
|
// Specifies the context for which a subkey should be derived for.
|
|
var (
|
|
// Used for the `push_mirror` table.
|
|
PushMirror = deriveKey("pushmirror")
|
|
// Used for the `two_factor` table.
|
|
TOTP = deriveKey("totp")
|
|
// Used for the `secret` table.
|
|
ActionSecret = deriveKey("action_secret")
|
|
// Used for the `task` table where type == TaskTypeMigrateRepo.
|
|
MigrateTask = deriveKey("migrate_repo_task")
|
|
// Used for the `webhook` table.
|
|
Webhook = deriveKey("webhook")
|
|
// Used for the `mirror` table.
|
|
PullMirror = deriveKey("pullmirror")
|
|
)
|
|
|
|
var (
|
|
// The hash used for HKDF.
|
|
hash = sha256.New
|
|
// The AEAD used for encryption/decryption.
|
|
aead = chacha20poly1305.NewX
|
|
// The pseudorandom key generated by HKDF-Extract.
|
|
prk atomic.Value
|
|
)
|
|
|
|
// Set the main IKM for this module.
|
|
func Init(ikm []byte) {
|
|
// Salt is intentionally left empty, it's not useful to Forgejo's use case.
|
|
buf, err := hkdf.Extract(hash, ikm, nil)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if ok := prk.CompareAndSwap(nil, buf); ok {
|
|
return
|
|
}
|
|
// prk was already set
|
|
old := prk.Load().([]byte)
|
|
if bytes.Equal(old, buf) {
|
|
return
|
|
}
|
|
panic("main IKM cannot be updated at runtime")
|
|
}
|
|
|
|
const (
|
|
aeadKeySize = chacha20poly1305.KeySize
|
|
aeadNonceSize = chacha20poly1305.NonceSizeX
|
|
)
|
|
|
|
// Derive *the* key for a given context, this is a deterministic function.
|
|
// The same key will be provided for the same context.
|
|
func deriveKey(context string) Context {
|
|
// wrap another sync.Once to prevent panic on initialization (prk would be nil)
|
|
return Context{sync.OnceValue(func() cipher.AEAD {
|
|
return expandPRK(prk.Load().([]byte), context)
|
|
})}
|
|
}
|
|
|
|
func expandPRK(prk []byte, context string) cipher.AEAD {
|
|
if len(prk) != sha256.Size {
|
|
panic("keying: not initialized")
|
|
}
|
|
|
|
key, err := hkdf.Expand(hash, prk, context, aeadKeySize)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
e, err := aead(key)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return e
|
|
}
|
|
|
|
type Context struct {
|
|
aead func() cipher.AEAD
|
|
}
|
|
|
|
// Encrypts the specified plaintext with some additional data that is tied to
|
|
// this plaintext. The additional data can be seen as the context in which the
|
|
// data is being encrypted for, this is different than the context for which the
|
|
// key was derived; this allows for more granularity without deriving new keys.
|
|
// Avoid any user-generated data to be passed into the additional data. The most
|
|
// common usage of this would be to encrypt a database field, in that case use
|
|
// the ID and database column name as additional data. The additional data isn't
|
|
// appended to the ciphertext and may be publicly known, it must be available
|
|
// when decryping the ciphertext.
|
|
func (k Context) Encrypt(plaintext, additionalData []byte) []byte {
|
|
nonce := make([]byte, aeadNonceSize)
|
|
_, _ = crand.Read(nonce) // never returns an error
|
|
|
|
// Returns the ciphertext of this plaintext.
|
|
return k.aead().Seal(nonce, nonce, plaintext, additionalData)
|
|
}
|
|
|
|
// Decrypts the ciphertext and authenticates it against the given additional
|
|
// data that was given when it was encrypted. It returns an error if the
|
|
// authentication failed.
|
|
func (k Context) Decrypt(ciphertext, additionalData []byte) ([]byte, error) {
|
|
if len(ciphertext) <= aeadNonceSize {
|
|
return nil, errors.New("keying: ciphertext is too short")
|
|
}
|
|
|
|
nonce, ciphertext := ciphertext[:aeadNonceSize], ciphertext[aeadNonceSize:]
|
|
|
|
return k.aead().Open(nil, nonce, ciphertext, additionalData)
|
|
}
|
|
|
|
// ColumnAndID generates a context that can be used as additional context for
|
|
// encrypting and decrypting data. It requires the column name and the row ID
|
|
// (this requires to be known beforehand). Be careful when using this, as the
|
|
// table name isn't part of this context. This means it's not bound to a
|
|
// particular table. The table should be part of the context that the key was
|
|
// derived for, in which case it binds through that.
|
|
func ColumnAndID(column string, id int64) []byte {
|
|
return binary.BigEndian.AppendUint64(append([]byte(column), ':'), uint64(id))
|
|
}
|
|
|
|
// ColumnAndJSONSelectorAndID generates a context that can be used as additional context
|
|
// for encrypting and decrypting data. It requires the column name, JSON
|
|
// selector and the row ID (this requires to be known beforehand). Be careful
|
|
// when using this, as the table name isn't part of this context. This means
|
|
// it's not bound to a particular table. The table should be part of the context
|
|
// that the key was derived for, in which case it binds through that. Use this
|
|
// over `ColumnAndID` if you're encrypting data that's stored inside JSON.
|
|
// jsonSelector must be a unambiguous selector to the JSON field that stores the
|
|
// encrypted data.
|
|
func ColumnAndJSONSelectorAndID(column, jsonSelector string, id int64) []byte {
|
|
return binary.BigEndian.AppendUint64(append(append([]byte(column), ':'), append([]byte(jsonSelector), ':')...), uint64(id))
|
|
}
|