diff --git a/internal/cert/storage.go b/internal/cert/storage.go new file mode 100644 index 0000000..e41d070 --- /dev/null +++ b/internal/cert/storage.go @@ -0,0 +1,112 @@ +package cert + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "sync" +) + +type Storage struct { + basePath string + certs map[string]*Certificate + mu sync.RWMutex +} + +type Certificate struct { + ID string + OriginalPath string + StoredPath string + Password string +} + +func NewStorage(basePath string) *Storage { + if basePath == "" { + basePath = "./certs/" + } + return &Storage{ + basePath: basePath, + certs: make(map[string]*Certificate), + } +} + +func (s *Storage) Init() error { + if err := os.MkdirAll(s.basePath, 0700); err != nil { + return fmt.Errorf("creating cert storage directory: %w", err) + } + return nil +} + +func (s *Storage) Store(id, origPath, password string) (string, error) { + s.mu.Lock() + defer s.mu.Unlock() + + exists := false + for _, c := range s.certs { + if c.ID == id { + exists = true + break + } + } + if exists { + return "", fmt.Errorf("certificate already exists with this ID") + } + + ext := filepath.Ext(origPath) + storedFilename := fmt.Sprintf("%s%s", id, ext) + storedPath := filepath.Join(s.basePath, storedFilename) + + data, err := os.ReadFile(origPath) + if err != nil { + return "", fmt.Errorf("reading certificate file: %w", err) + } + + if err := os.WriteFile(storedPath, data, 0600); err != nil { + return "", fmt.Errorf("storing certificate: %w", err) + } + + cert := &Certificate{ + ID: id, + OriginalPath: origPath, + StoredPath: storedPath, + Password: password, + } + s.certs[id] = cert + + return storedPath, nil +} + +func (s *Storage) Get(id string) (*Certificate, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + cert, ok := s.certs[id] + if !ok { + return nil, fmt.Errorf("certificate not found") + } + return cert, nil +} + +func (s *Storage) Delete(id string) error { + s.mu.Lock() + defer s.mu.Unlock() + + cert, ok := s.certs[id] + if !ok { + return fmt.Errorf("certificate not found") + } + + if err := os.Remove(cert.StoredPath); err != nil { + return fmt.Errorf("removing certificate file: %w", err) + } + + delete(s.certs, id) + return nil +} + +func GenerateID() string { + hash := sha256.Sum256([]byte(fmt.Sprintf("%d", os.Getpid()))) + return hex.EncodeToString(hash[:])[:16] +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..01383de --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,58 @@ +package config + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Server ServerConfig `yaml:"server"` + VeriFactu VeriFactuConfig `yaml:"verifactu"` + Certificates CertificateConfig `yaml:"certificates"` + Crypto CryptoConfig `yaml:"crypto"` +} + +type ServerConfig struct { + Port int `yaml:"port"` +} + +type VeriFactuConfig struct { + Environment string `yaml:"environment"` +} + +type CertificateConfig struct { + StoragePath string `yaml:"storage_path"` +} + +type CryptoConfig struct { + KeysPath string `yaml:"keys_path"` +} + +func Load(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("reading config file: %w", err) + } + + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parsing config file: %w", err) + } + + if cfg.Server.Port == 0 { + cfg.Server.Port = 8080 + } + if cfg.VeriFactu.Environment == "" { + cfg.VeriFactu.Environment = "test" + } + if cfg.Certificates.StoragePath == "" { + cfg.Certificates.StoragePath = "./certs/" + } + if cfg.Crypto.KeysPath == "" { + cfg.Crypto.KeysPath = "./keys/" + } + + return &cfg, nil +} diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go new file mode 100644 index 0000000..0e0b218 --- /dev/null +++ b/internal/crypto/crypto.go @@ -0,0 +1,135 @@ +package crypto + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "os" + "path/filepath" +) + +const ( + DefaultKeyBits = 2048 + DefaultKeyDir = "./keys" +) + +type KeyPair struct { + PublicKey *rsa.PublicKey + PrivateKey *rsa.PrivateKey +} + +func GenerateKeyPair(bits int) (*KeyPair, error) { + priv, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, fmt.Errorf("generating RSA key: %w", err) + } + return &KeyPair{ + PublicKey: &priv.PublicKey, + PrivateKey: priv, + }, nil +} + +func (k *KeyPair) PublicKeyPEM() ([]byte, error) { + pubBytes, err := x509.MarshalPKIXPublicKey(k.PublicKey) + if err != nil { + return nil, fmt.Errorf("marshaling public key: %w", err) + } + block := &pem.Block{Type: "PUBLIC KEY", Bytes: pubBytes} + return pem.EncodeToMemory(block), nil +} + +func (k *KeyPair) PrivateKeyPEM() ([]byte, error) { + block := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k.PrivateKey)} + return pem.EncodeToMemory(block), nil +} + +func LoadKeyPair(pubPath, privPath string) (*KeyPair, error) { + pubData, err := os.ReadFile(pubPath) + if err != nil { + return nil, fmt.Errorf("reading public key: %w", err) + } + + privData, err := os.ReadFile(privPath) + if err != nil { + return nil, fmt.Errorf("reading private key: %w", err) + } + + block, _ := pem.Decode(pubData) + if block == nil { + return nil, fmt.Errorf("invalid public key PEM") + } + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing public key: %w", err) + } + rsaPub, ok := pub.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("not an RSA public key") + } + + block, _ = pem.Decode(privData) + if block == nil { + return nil, fmt.Errorf("invalid private key PEM") + } + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parsing private key: %w", err) + } + + return &KeyPair{ + PublicKey: rsaPub, + PrivateKey: priv, + }, nil +} + +func LoadOrCreateKeyPair(keyDir string) (*KeyPair, error) { + if keyDir == "" { + keyDir = DefaultKeyDir + } + + pubPath := filepath.Join(keyDir, "public.pem") + privPath := filepath.Join(keyDir, "private.pem") + + if _, err := os.Stat(pubPath); err == nil { + if _, err := os.Stat(privPath); err == nil { + return LoadKeyPair(pubPath, privPath) + } + } + + if err := os.MkdirAll(keyDir, 0700); err != nil { + return nil, fmt.Errorf("creating key directory: %w", err) + } + + keyPair, err := GenerateKeyPair(DefaultKeyBits) + if err != nil { + return nil, err + } + + pubPEM, err := keyPair.PublicKeyPEM() + if err != nil { + return nil, err + } + if err := os.WriteFile(pubPath, pubPEM, 0644); err != nil { + return nil, fmt.Errorf("saving public key: %w", err) + } + + privPEM, err := keyPair.PrivateKeyPEM() + if err != nil { + return nil, err + } + if err := os.WriteFile(privPath, privPEM, 0600); err != nil { + return nil, fmt.Errorf("saving private key: %w", err) + } + + return keyPair, nil +} + +func Encrypt(plain []byte, pub *rsa.PublicKey) ([]byte, error) { + return rsa.EncryptPKCS1v15(rand.Reader, pub, plain) +} + +func Decrypt(cipher []byte, priv *rsa.PrivateKey) ([]byte, error) { + return rsa.DecryptPKCS1v15(rand.Reader, priv, cipher) +}