174 lines
4.2 KiB
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
|
|
}
|