Files
i2pkeys/NewI2PKeys.go

174 lines
4.2 KiB
Go

package i2pkeys
import (
"bufio"
"context"
"fmt"
"net"
"strings"
"time"
)
var DefaultSAMAddress = "127.0.0.1:7656"
const (
defaultTimeout = 30 * time.Second
maxResponseSize = 4096
cmdHello = "HELLO VERSION MIN=3.1 MAX=3.1\n"
cmdGenerate = "DEST GENERATE SIGNATURE_TYPE=%s\n"
responseOK = "RESULT=OK"
pubKeyPrefix = "PUB="
privKeyPrefix = "PRIV="
)
// samClient handles communication with the SAM bridge
type samClient struct {
addr string
timeout time.Duration
}
// newSAMClient creates a new SAM client with optional configuration
func newSAMClient(options ...func(*samClient)) *samClient {
client := &samClient{
addr: DefaultSAMAddress,
timeout: defaultTimeout,
}
for _, opt := range options {
opt(client)
}
return client
}
// NewDestination generates a new I2P destination using the SAM bridge.
// This is the only public function that external code should use.
func NewDestination(keyType ...string) (*I2PKeys, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
if keyType == nil {
keyType = []string{"7"}
}
client := newSAMClient()
return client.generateDestination(ctx, keyType[0])
}
// generateDestination handles the key generation process
func (c *samClient) generateDestination(ctx context.Context, keyType string) (*I2PKeys, error) {
conn, err := c.dial(ctx)
if err != nil {
return nil, fmt.Errorf("connecting to SAM bridge: %w", err)
}
// Ensure connection is always closed, even on error paths
defer func() {
if closeErr := conn.Close(); closeErr != nil {
log.WithError(closeErr).Debug("Error closing SAM connection")
}
}()
if err := c.handshake(ctx, conn); err != nil {
return nil, fmt.Errorf("SAM handshake failed: %w", err)
}
keys, err := c.generateKeys(ctx, conn, keyType)
if err != nil {
return nil, fmt.Errorf("generating keys: %w", err)
}
return keys, nil
}
func (c *samClient) dial(ctx context.Context) (net.Conn, error) {
dialer := &net.Dialer{Timeout: c.timeout}
conn, err := dialer.DialContext(ctx, "tcp", c.addr)
if err != nil {
return nil, fmt.Errorf("dialing SAM bridge: %w", err)
}
return conn, nil
}
func (c *samClient) handshake(ctx context.Context, conn net.Conn) error {
if err := c.writeCommand(conn, cmdHello); err != nil {
return err
}
response, err := c.readResponse(conn)
if err != nil {
return err
}
if !strings.Contains(response, responseOK) {
return fmt.Errorf("unexpected SAM response: %s", response)
}
return nil
}
func (c *samClient) generateKeys(ctx context.Context, conn net.Conn, keyType string) (*I2PKeys, error) {
cmdGenerate := fmt.Sprintf(cmdGenerate, keyType)
if err := c.writeCommand(conn, cmdGenerate); err != nil {
return nil, err
}
response, err := c.readResponse(conn)
if err != nil {
return nil, err
}
pub, priv, err := parseKeyResponse(response)
if err != nil {
return nil, err
}
log.Println("Generated keys:", pub, priv)
if len(pub) == 0 || len(priv) == 0 {
return nil, fmt.Errorf("invalid key response: %s", response)
}
if len(pub) > maxResponseSize || len(priv) > maxResponseSize {
return nil, fmt.Errorf("key response too large: %s", response)
}
if len(pub) < 128 || len(priv) < 128 {
return nil, fmt.Errorf("key response too small: %s", response)
}
return &I2PKeys{
Address: I2PAddr(pub),
Both: pub + priv,
}, nil
}
func (c *samClient) writeCommand(conn net.Conn, cmd string) error {
_, err := conn.Write([]byte(cmd))
if err != nil {
return fmt.Errorf("writing command: %w", err)
}
return nil
}
func (c *samClient) readResponse(conn net.Conn) (string, error) {
reader := bufio.NewReader(conn)
response, err := reader.ReadString('\n')
if err != nil {
return "", fmt.Errorf("reading response: %w", err)
}
return strings.TrimSpace(response), nil
}
func parseKeyResponse(response string) (pub, priv string, err error) {
parts := strings.Split(response, privKeyPrefix)
if len(parts) != 2 {
return "", "", fmt.Errorf("invalid key response format")
}
pubParts := strings.Split(parts[0], pubKeyPrefix)
if len(pubParts) != 2 {
return "", "", fmt.Errorf("invalid public key format")
}
pub = strings.TrimSpace(pubParts[1])
priv = strings.TrimSpace(parts[1])
return pub, priv, nil
}