feat: production-ready domain control dashboard
Container-Image veröffentlichen / container (push) Has been cancelled
Container-Image veröffentlichen / container (push) Has been cancelled
This commit is contained in:
1 parent
9c60eaf3fe
commit
df7516e7ab
45 files changed
+7932
-1
No files matched your search
@@ -0,0 +1,122 @@
|
||||
package secure
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
const (
|
||||
argonMemory = 64 * 1024
|
||||
argonTime = 3
|
||||
argonThreads = 2
|
||||
argonKeyLen = 32
|
||||
)
|
||||
|
||||
type Cipher struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
func LoadCipher(path string) (*Cipher, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read master key: %w", err)
|
||||
}
|
||||
raw = []byte(strings.TrimSpace(string(raw)))
|
||||
key, err := base64.RawStdEncoding.DecodeString(string(raw))
|
||||
if err != nil {
|
||||
key, err = base64.StdEncoding.DecodeString(string(raw))
|
||||
}
|
||||
if err != nil || len(key) != chacha20poly1305.KeySize {
|
||||
return nil, errors.New("master key must be a base64-encoded 32-byte value")
|
||||
}
|
||||
return &Cipher{key: key}, nil
|
||||
}
|
||||
|
||||
func (c *Cipher) Encrypt(plaintext string) (ciphertext, nonce []byte, err error) {
|
||||
aead, err := chacha20poly1305.NewX(c.key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
nonce = make([]byte, chacha20poly1305.NonceSizeX)
|
||||
if _, err = rand.Read(nonce); err != nil {
|
||||
return nil, nil, fmt.Errorf("create encryption nonce: %w", err)
|
||||
}
|
||||
return aead.Seal(nil, nonce, []byte(plaintext), nil), nonce, nil
|
||||
}
|
||||
|
||||
func (c *Cipher) Decrypt(ciphertext, nonce []byte) (string, error) {
|
||||
aead, err := chacha20poly1305.NewX(c.key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
plaintext, err := aead.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return "", errors.New("could not decrypt stored credential")
|
||||
}
|
||||
return string(plaintext), nil
|
||||
}
|
||||
|
||||
func (c *Cipher) Fingerprint(value string) string {
|
||||
mac := hmac.New(sha256.New, c.key)
|
||||
_, _ = mac.Write([]byte(value))
|
||||
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func RandomToken(bytes int) (string, error) {
|
||||
buf := make([]byte, bytes)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(buf), nil
|
||||
}
|
||||
|
||||
func HashToken(token string) string {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return base64.RawURLEncoding.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
if len(password) < 12 {
|
||||
return "", errors.New("password must contain at least 12 characters")
|
||||
}
|
||||
salt := make([]byte, 16)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
hash := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
|
||||
return fmt.Sprintf("argon2id$v=19$m=%d,t=%d,p=%d$%s$%s", argonMemory, argonTime, argonThreads,
|
||||
base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(hash)), nil
|
||||
}
|
||||
|
||||
func VerifyPassword(encoded, password string) bool {
|
||||
parts := strings.Split(encoded, "$")
|
||||
if len(parts) != 5 || parts[0] != "argon2id" || parts[1] != "v=19" {
|
||||
return false
|
||||
}
|
||||
var memory uint32
|
||||
var iterations uint32
|
||||
var threads uint8
|
||||
if _, err := fmt.Sscanf(parts[2], "m=%d,t=%d,p=%d", &memory, &iterations, &threads); err != nil {
|
||||
return false
|
||||
}
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[3])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
want, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
got := argon2.IDKey([]byte(password), salt, iterations, memory, threads, uint32(len(want)))
|
||||
return subtle.ConstantTimeCompare(got, want) == 1
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package secure
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCipherRoundTripAndFingerprint(t *testing.T) {
|
||||
t.Parallel()
|
||||
dir := t.TempDir()
|
||||
key := make([]byte, 32)
|
||||
for index := range key {
|
||||
key[index] = byte(index + 1)
|
||||
}
|
||||
path := filepath.Join(dir, "master-key")
|
||||
if err := os.WriteFile(path, []byte(base64.RawStdEncoding.EncodeToString(key)), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cipher, err := LoadCipher(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ciphertext, nonce, err := cipher.Encrypt("hetzner-token-value")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(ciphertext) == "hetzner-token-value" {
|
||||
t.Fatal("token was not encrypted")
|
||||
}
|
||||
plaintext, err := cipher.Decrypt(ciphertext, nonce)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if plaintext != "hetzner-token-value" {
|
||||
t.Fatalf("got %q", plaintext)
|
||||
}
|
||||
if cipher.Fingerprint("same") != cipher.Fingerprint("same") || cipher.Fingerprint("same") == cipher.Fingerprint("different") {
|
||||
t.Fatal("fingerprint must be deterministic and token-specific")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordHash(t *testing.T) {
|
||||
t.Parallel()
|
||||
hash, err := HashPassword("a secure password")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !VerifyPassword(hash, "a secure password") {
|
||||
t.Fatal("expected password verification to succeed")
|
||||
}
|
||||
if VerifyPassword(hash, "wrong password") {
|
||||
t.Fatal("wrong password verified")
|
||||
}
|
||||
if _, err := HashPassword("short"); err == nil {
|
||||
t.Fatal("short password was accepted")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user