go/pkg/crypt/openpgp/key.go

225 lines
6.7 KiB
Go

package openpgp
import (
"bytes"
"crypto"
"fmt"
"path/filepath"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/Snider/Core/pkg/crypt/lthn"
)
// CreateKeyPair generates a new OpenPGP key pair.
// The password parameter is optional. If not provided, the private key will not be encrypted.
func CreateKeyPair(username string, passwords ...string) (*KeyPair, error) {
var password string
if len(passwords) > 0 {
password = passwords[0]
}
entity, err := openpgp.NewEntity(username, "Lethean Desktop", "", &packet.Config{
RSABits: 4096,
DefaultHash: crypto.SHA256,
})
if err != nil {
return nil, fmt.Errorf("failed to create new entity: %w", err)
}
// The private key is initially unencrypted after NewEntity.
// Generate revocation certificate while the private key is unencrypted.
revocationCert, err := createRevocationCertificate(entity)
if err != nil {
revocationCert = "" // Non-critical, proceed without it if it fails
}
// Encrypt the private key only if a password is provided, after revocation cert generation.
if password != "" {
if err := entity.PrivateKey.Encrypt([]byte(password)); err != nil {
return nil, fmt.Errorf("failed to encrypt private key: %w", err)
}
}
publicKey, err := serializeEntity(entity, openpgp.PublicKeyType, "") // Public key doesn't need password
if err != nil {
return nil, err
}
// Private key serialization. The key is already in its final encrypted/unencrypted state.
privateKey, err := serializeEntity(entity, openpgp.PrivateKeyType, "") // No password needed here for serialization
if err != nil {
return nil, err
}
return &KeyPair{
PublicKey: publicKey,
PrivateKey: privateKey,
RevocationCertificate: revocationCert,
}, nil
}
// CreateServerKeyPair creates and stores a key pair for the server in a specific directory.
func CreateServerKeyPair(keysDir string) error {
serverKeyPath := filepath.Join(keysDir, "server.lthn.pub")
// Passphrase is derived from the path itself, consistent with original logic.
passphrase := lthn.Hash(serverKeyPath)
return createAndStoreKeyPair("server", passphrase, keysDir)
}
// GetPublicKey retrieves an armored public key for a given ID.
func GetPublicKey(path string) (*openpgp.Entity, error) {
return readEntity(path)
}
// GetPrivateKey retrieves and decrypts an armored private key.
func GetPrivateKey(path, passphrase string) (*openpgp.Entity, error) {
entity, err := readEntity(path)
if err != nil {
return nil, err
}
if entity.PrivateKey == nil {
return nil, fmt.Errorf("no private key found for path %s", path)
}
if entity.PrivateKey.Encrypted {
if err := entity.PrivateKey.Decrypt([]byte(passphrase)); err != nil {
return nil, fmt.Errorf("failed to decrypt private key for path %s: %w", path, err)
}
}
var primaryIdentity *openpgp.Identity
for _, identity := range entity.Identities {
if identity.SelfSignature.IsPrimaryId != nil && *identity.SelfSignature.IsPrimaryId {
primaryIdentity = identity
break
}
}
if primaryIdentity == nil {
for _, identity := range entity.Identities {
primaryIdentity = identity
break
}
}
if primaryIdentity == nil {
return nil, fmt.Errorf("key for %s has no identity", path)
}
if primaryIdentity.SelfSignature.KeyLifetimeSecs != nil {
if primaryIdentity.SelfSignature.CreationTime.Add(time.Duration(*primaryIdentity.SelfSignature.KeyLifetimeSecs) * time.Second).Before(time.Now()) {
return nil, fmt.Errorf("key for %s has expired", path)
}
}
return entity, nil
}
// --- Helper Functions ---
func createAndStoreKeyPair(id, password, dir string) error {
//var keyPair *KeyPair
var err error
//if password != "" {
// keyPair, err = CreateKeyPair(id, password)
//} else {
// keyPair, err = CreateKeyPair(id)
//}
if err != nil {
return fmt.Errorf("failed to create key pair for id %s: %w", id, err)
}
//if err := io.Local.EnsureDir(dir); err != nil {
// return fmt.Errorf("failed to ensure key directory exists: %w", err)
//}
//
//files := map[string]string{
// filepath.Join(dir, fmt.Sprintf("%s.lthn.pub", id)): keyPair.PublicKey,
// filepath.Join(dir, fmt.Sprintf("%s.lthn.key", id)): keyPair.PrivateKey,
// filepath.Join(dir, fmt.Sprintf("%s.lthn.rev", id)): keyPair.RevocationCertificate, // Re-enabled
//}
//
//for path, content := range files {
// if content == "" {
// continue
// }
// if err := io.Local.Write(path, content); err != nil {
// return fmt.Errorf("failed to write key file %s: %w", path, err)
// }
//}
return nil
}
func readEntity(path string) (*openpgp.Entity, error) {
//keyArmored, err := m.Read(path)
//if err != nil {
// return nil, fmt.Errorf("failed to read key file %s: %w", path, err)
//}
//entityList, err := openpgp.ReadArmoredKeyRing(strings.NewReader(keyArmored))
//if err != nil {
// return nil, fmt.Errorf("failed to parse key file %s: %w", path, err)
//}
//if len(entityList) == 0 {
// return nil, fmt.Errorf("no entity found in key file %s", path)
//}
//return entityList[0], nil
return nil, nil
}
func serializeEntity(entity *openpgp.Entity, keyType string, password string) (string, error) {
buf := new(bytes.Buffer)
writer, err := armor.Encode(buf, keyType, nil)
if err != nil {
return "", fmt.Errorf("failed to create armor encoder: %w", err)
}
if keyType == openpgp.PrivateKeyType {
// Serialize the private key in its current in-memory state.
// Encryption is handled by CreateKeyPair before this function is called.
err = entity.SerializePrivateWithoutSigning(writer, nil)
} else {
err = entity.Serialize(writer)
}
if err != nil {
return "", fmt.Errorf("failed to serialize entity: %w", err)
}
if err := writer.Close(); err != nil {
return "", fmt.Errorf("failed to close armor writer: %w", err)
}
return buf.String(), nil
}
func createRevocationCertificate(entity *openpgp.Entity) (string, error) {
buf := new(bytes.Buffer)
writer, err := armor.Encode(buf, openpgp.SignatureType, nil)
if err != nil {
return "", fmt.Errorf("failed to create armor encoder for revocation: %w", err)
}
sig := &packet.Signature{
SigType: packet.SigTypeKeyRevocation,
PubKeyAlgo: entity.PrimaryKey.PubKeyAlgo,
Hash: crypto.SHA256,
CreationTime: time.Now(),
IssuerKeyId: &entity.PrimaryKey.KeyId,
}
// SignKey requires an unencrypted private key.
if err := sig.SignKey(entity.PrimaryKey, entity.PrivateKey, nil); err != nil {
return "", fmt.Errorf("failed to sign revocation: %w", err)
}
if err := sig.Serialize(writer); err != nil {
return "", fmt.Errorf("failed to serialize revocation signature: %w", err)
}
if err := writer.Close(); err != nil {
return "", fmt.Errorf("failed to close revocation writer: %w", err)
}
return buf.String(), nil
}