Ctrl K
ComunidadGuías…

internal/app/wallet/keys.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package wallet

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"
)

// ErrWalletKeyFile marks a missing or malformed wallet master key file. The
// error never carries key material.
var ErrWalletKeyFile = errors.New("wallet master key file is unusable")

// ErrWalletCreationDisabled reports that telesrv was started without a wallet
// master key, so it cannot custody a generated wallet. Callers surface it as an
// unavailable capability, never as a generic failure.
var ErrWalletCreationDisabled = errors.New("server-held wallet creation is disabled")

// KeyCipher seals wallet private keys with a process-wide AES-256-GCM master
// key. The key lives in its own 0600 file next to the wallet state, so backing
// up data/ keeps stored wallets usable; losing it makes them unrecoverable.
type KeyCipher struct {
	aead cipher.AEAD
}

// LoadOrCreateKeyCipher reads the raw 32-byte master key from path, generating
// it on first use. The file must not be group or world accessible.
func LoadOrCreateKeyCipher(path string) (*KeyCipher, error) {
	if strings.TrimSpace(path) == "" {
		return nil, ErrWalletKeyFile
	}
	raw, err := os.ReadFile(path)
	if errors.Is(err, os.ErrNotExist) {
		raw = make([]byte, 32)
		if _, err = rand.Read(raw); err != nil {
			return nil, err
		}
		if err = writeKeyFile(path, raw); err != nil {
			return nil, err
		}
		// The decoder accepts the persisted hex format on both first boot and reload.
		raw = []byte(hex.EncodeToString(raw) + "\n")
	} else if err != nil {
		return nil, err
	}
	if info, statErr := os.Stat(path); statErr != nil {
		return nil, statErr
	} else if info.Mode().Perm()&0077 != 0 {
		return nil, fmt.Errorf("%w: %s must have mode 0600", ErrWalletKeyFile, path)
	}
	return newKeyCipher(decodeKeyFile(path, raw))
}

// decodeKeyFile reads the hex format written by writeKeyFile.
func decodeKeyFile(path string, raw []byte) []byte {
	trimmed := bytes.TrimRight(raw, "\n")
	if len(trimmed) == hex.EncodedLen(32) {
		if decoded, err := hex.DecodeString(string(trimmed)); err == nil {
			return decoded
		}
	}
	return nil
}

func writeKeyFile(path string, raw []byte) error {
	if dir := filepath.Dir(path); dir != "" {
		if err := os.MkdirAll(dir, 0700); err != nil {
			return err
		}
	}
	f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
	if err != nil {
		return err
	}
	defer f.Close()
	if _, err = f.WriteString(hex.EncodeToString(raw) + "\n"); err != nil {
		return err
	}
	if err := f.Sync(); err != nil {
		return err
	}
	if err := f.Close(); err != nil {
		return err
	}
	dir, err := os.Open(filepath.Dir(path))
	if err != nil {
		return err
	}
	defer dir.Close()
	return dir.Sync()
}

func newKeyCipher(raw []byte) (*KeyCipher, error) {
	if len(raw) != 32 {
		return nil, ErrWalletKeyFile
	}
	block, err := aes.NewCipher(raw)
	if err != nil {
		return nil, err
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	return &KeyCipher{aead: aead}, nil
}

// Seal encrypts wallet private key material. The nonce is stored next to the
// ciphertext; both are base64/hex encoded by the caller's record format.
func (k *KeyCipher) Seal(plaintext []byte) (ciphertext, nonce []byte, err error) {
	if k == nil || k.aead == nil || len(plaintext) == 0 {
		return nil, nil, ErrWalletKeyFile
	}
	nonce = make([]byte, k.aead.NonceSize())
	if _, err = rand.Read(nonce); err != nil {
		return nil, nil, err
	}
	return k.aead.Seal(nil, nonce, plaintext, nil), nonce, nil
}

// Open reverses Seal. A wrong master key fails as an authentication error, not
// as garbage plaintext.
func (k *KeyCipher) Open(ciphertext, nonce []byte) ([]byte, error) {
	if k == nil || k.aead == nil || len(nonce) != k.aead.NonceSize() || len(ciphertext) == 0 {
		return nil, ErrWalletKeyFile
	}
	return k.aead.Open(nil, nonce, ciphertext, nil)
}