key.go raw

   1  package tunnel
   2  
   3  import (
   4  	"crypto/rand"
   5  	"encoding/hex"
   6  	"errors"
   7  	"fmt"
   8  	"os"
   9  	"path/filepath"
  10  	"strings"
  11  
  12  	"git.smesh.lol/nostrino/pkg/crypto/secp256k1"
  13  	"git.smesh.lol/nostrino/pkg/nostr"
  14  )
  15  
  16  // ParseKey reads a 64-hex-character secret key.
  17  func ParseKey(raw string) ([32]byte, error) {
  18  	var key [32]byte
  19  	if err := nostr.Decode32(strings.TrimSpace(raw), key[:]); err != nil {
  20  		return key, errors.New("tunnel: secret key must be 64 lowercase hex characters")
  21  	}
  22  	if _, ok := secp256k1.PubKey(key); !ok {
  23  		return key, errors.New("tunnel: secret key is out of range")
  24  	}
  25  	return key, nil
  26  }
  27  
  28  // LoadOrCreateKey reads a key file, creating it with fresh key material on
  29  // first use. The file accepts either 32 raw bytes or 64 hex characters, and
  30  // is written as hex with mode 0600.
  31  func LoadOrCreateKey(path string) ([32]byte, error) {
  32  	var key [32]byte
  33  	body, err := os.ReadFile(path)
  34  	switch {
  35  	case err == nil:
  36  		if len(body) == 32 {
  37  			copy(key[:], body)
  38  		} else if err := nostr.Decode32(strings.TrimSpace(string(body)), key[:]); err != nil {
  39  			return key, fmt.Errorf("tunnel: key file %s: %w", path, err)
  40  		}
  41  		if _, ok := secp256k1.PubKey(key); !ok {
  42  			return key, fmt.Errorf("tunnel: key file %s: invalid secret key", path)
  43  		}
  44  		return key, nil
  45  	case !os.IsNotExist(err):
  46  		return key, fmt.Errorf("tunnel: read key file: %w", err)
  47  	}
  48  	for {
  49  		if _, err := rand.Read(key[:]); err != nil {
  50  			return key, fmt.Errorf("tunnel: generate key: %w", err)
  51  		}
  52  		if _, ok := secp256k1.PubKey(key); ok {
  53  			break
  54  		}
  55  	}
  56  	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
  57  		return key, fmt.Errorf("tunnel: key directory: %w", err)
  58  	}
  59  	tmp := path + ".tmp"
  60  	if err := os.WriteFile(tmp, []byte(hex.EncodeToString(key[:])+"\n"), 0o600); err != nil {
  61  		return key, fmt.Errorf("tunnel: write key file: %w", err)
  62  	}
  63  	if err := os.Rename(tmp, path); err != nil {
  64  		return key, fmt.Errorf("tunnel: install key file: %w", err)
  65  	}
  66  	return key, nil
  67  }
  68