129 lines
3.7 KiB
Go
129 lines
3.7 KiB
Go
package client
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.nevets.tech/Steven/certman/common"
|
|
)
|
|
|
|
// DecryptAndWriteCertificates walks the workspace's root directory, decrypts
|
|
// every *.crpt file using the domain's crypto key, and writes the cleartext
|
|
// output into certsDir.
|
|
//
|
|
// On a fully successful pass it records the current HEAD commit SHA via
|
|
// WriteCommitHash, so the next tick can short-circuit when nothing changed.
|
|
// Per-file failures are collected and returned together; the commit-hash
|
|
// marker is only written when every file decrypted cleanly, so a partial
|
|
// sync never masquerades as up-to-date on the next tick.
|
|
func DecryptAndWriteCertificates(certsDir string, domainConfig *common.DomainConfig, ws *common.GitWorkspace) error {
|
|
entries, err := ws.FS.ReadDir("/")
|
|
if err != nil {
|
|
return fmt.Errorf("read workspace root: %w", err)
|
|
}
|
|
|
|
var errs []error
|
|
var wrote int
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
if !strings.HasSuffix(name, ".crpt") {
|
|
continue
|
|
}
|
|
plainName, _ := strings.CutSuffix(name, ".crpt")
|
|
|
|
data, err := readWorkspaceFile(ws, name)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("%s: %w", name, err))
|
|
continue
|
|
}
|
|
if err := common.DecryptFileFromBytes(domainConfig.Certificates.CryptoKey, data, filepath.Join(certsDir, plainName), nil); err != nil {
|
|
errs = append(errs, fmt.Errorf("%s: decrypt: %w", name, err))
|
|
continue
|
|
}
|
|
wrote++
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return errors.Join(errs...)
|
|
}
|
|
if wrote == 0 {
|
|
return nil
|
|
}
|
|
|
|
head, err := ws.Repo.Head()
|
|
if err != nil {
|
|
return fmt.Errorf("get repo head: %w", err)
|
|
}
|
|
return WriteCommitHash(certsDir, head.Hash().String())
|
|
}
|
|
|
|
// DecryptCertificates is a standalone utility that decrypts every *.crpt
|
|
// file in an on-disk directory. It is used by the `cert decrypt` CLI command
|
|
// and does not touch git state.
|
|
func DecryptCertificates(certPath, cryptoKey string) error {
|
|
entries, err := os.ReadDir(certPath)
|
|
if err != nil {
|
|
return fmt.Errorf("read %s: %w", certPath, err)
|
|
}
|
|
var errs []error
|
|
for _, entry := range entries {
|
|
name := entry.Name()
|
|
if !strings.HasSuffix(name, ".crpt") {
|
|
continue
|
|
}
|
|
plainName, _ := strings.CutSuffix(name, ".crpt")
|
|
|
|
data, err := os.ReadFile(filepath.Join(certPath, name))
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("%s: %w", name, err))
|
|
continue
|
|
}
|
|
if err := common.DecryptFileFromBytes(cryptoKey, data, filepath.Join(certPath, plainName), nil); err != nil {
|
|
errs = append(errs, fmt.Errorf("%s: decrypt: %w", name, err))
|
|
continue
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
return errors.Join(errs...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateSymlinks refreshes every configured cert and key symlink so it
|
|
// points at the domain's current cert/key files under certsDir. It reports
|
|
// all link failures together rather than stopping at the first one.
|
|
func UpdateSymlinks(domain string, domainConfig *common.DomainConfig, certsDir string) error {
|
|
var errs []error
|
|
for _, link := range domainConfig.Certificates.CertSymlinks {
|
|
if err := common.LinkFile(filepath.Join(certsDir, domain+".crt"), link, domain, ".crt"); err != nil {
|
|
errs = append(errs, fmt.Errorf("cert link %s: %w", link, err))
|
|
}
|
|
}
|
|
for _, link := range domainConfig.Certificates.KeySymlinks {
|
|
if err := common.LinkFile(filepath.Join(certsDir, domain+".key"), link, domain, ".key"); err != nil {
|
|
errs = append(errs, fmt.Errorf("key link %s: %w", link, err))
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
return errors.Join(errs...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readWorkspaceFile(ws *common.GitWorkspace, name string) ([]byte, error) {
|
|
f, err := ws.FS.Open(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open: %w", err)
|
|
}
|
|
defer f.Close()
|
|
data, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read: %w", err)
|
|
}
|
|
return data, nil
|
|
}
|