Separate out generate address function and make it's SAM port configurable

This commit is contained in:
eyedeekay
2024-11-29 18:01:00 -05:00
parent 40e34d7089
commit d8a31854b9
6 changed files with 212 additions and 215 deletions

View File

@ -7,13 +7,13 @@ import (
) )
const ( const (
// Address length constraints // Address length constraints
MinAddressLength = 516 MinAddressLength = 516
MaxAddressLength = 4096 MaxAddressLength = 4096
// Domain suffixes // Domain suffixes
I2PDomainSuffix = ".i2p" I2PDomainSuffix = ".i2p"
B32DomainSuffix = ".b32.i2p" B32DomainSuffix = ".b32.i2p"
) )
// I2PAddr represents an I2P destination, equivalent to an IP address. // I2PAddr represents an I2P destination, equivalent to an IP address.
@ -22,98 +22,98 @@ type I2PAddr string
// Base64 returns the raw base64 representation of the I2P address. // Base64 returns the raw base64 representation of the I2P address.
func (a I2PAddr) Base64() string { func (a I2PAddr) Base64() string {
return string(a) return string(a)
} }
// String returns either the base64 or base32 representation based on configuration. // String returns either the base64 or base32 representation based on configuration.
func (a I2PAddr) String() string { func (a I2PAddr) String() string {
if StringIsBase64 { if StringIsBase64 {
return a.Base64() return a.Base64()
} }
return a.Base32() return a.Base32()
} }
// Network returns the network type, always "I2P". // Network returns the network type, always "I2P".
func (a I2PAddr) Network() string { func (a I2PAddr) Network() string {
return "I2P" return "I2P"
} }
// NewI2PAddrFromString creates a new I2P address from a base64-encoded string. // NewI2PAddrFromString creates a new I2P address from a base64-encoded string.
// It validates the format and returns an error if the address is invalid. // It validates the format and returns an error if the address is invalid.
func NewI2PAddrFromString(addr string) (I2PAddr, error) { func NewI2PAddrFromString(addr string) (I2PAddr, error) {
addr = sanitizeAddress(addr) addr = sanitizeAddress(addr)
if err := validateAddressFormat(addr); err != nil { if err := validateAddressFormat(addr); err != nil {
return I2PAddr(""), err return I2PAddr(""), err
} }
if err := validateBase64Encoding(addr); err != nil { if err := validateBase64Encoding(addr); err != nil {
return I2PAddr(""), err return I2PAddr(""), err
} }
return I2PAddr(addr), nil return I2PAddr(addr), nil
} }
func sanitizeAddress(addr string) string { func sanitizeAddress(addr string) string {
// Remove domain suffix if present // Remove domain suffix if present
addr = strings.TrimSuffix(addr, I2PDomainSuffix) addr = strings.TrimSuffix(addr, I2PDomainSuffix)
return strings.Trim(addr, "\t\n\r\f ") return strings.Trim(addr, "\t\n\r\f ")
} }
func validateAddressFormat(addr string) error { func validateAddressFormat(addr string) error {
if len(addr) > MaxAddressLength || len(addr) < MinAddressLength { if len(addr) > MaxAddressLength || len(addr) < MinAddressLength {
return fmt.Errorf("invalid address length: got %d, want between %d and %d", return fmt.Errorf("invalid address length: got %d, want between %d and %d",
len(addr), MinAddressLength, MaxAddressLength) len(addr), MinAddressLength, MaxAddressLength)
} }
if strings.HasSuffix(addr, B32DomainSuffix) { if strings.HasSuffix(addr, B32DomainSuffix) {
return fmt.Errorf("cannot convert %s to full destination", B32DomainSuffix) return fmt.Errorf("cannot convert %s to full destination", B32DomainSuffix)
} }
return nil return nil
} }
func validateBase64Encoding(addr string) error { func validateBase64Encoding(addr string) error {
buf := make([]byte, i2pB64enc.DecodedLen(len(addr))) buf := make([]byte, i2pB64enc.DecodedLen(len(addr)))
if _, err := i2pB64enc.Decode(buf, []byte(addr)); err != nil { if _, err := i2pB64enc.Decode(buf, []byte(addr)); err != nil {
return fmt.Errorf("invalid base64 encoding: %w", err) return fmt.Errorf("invalid base64 encoding: %w", err)
} }
return nil return nil
} }
// NewI2PAddrFromBytes creates a new I2P address from a byte array. // NewI2PAddrFromBytes creates a new I2P address from a byte array.
func NewI2PAddrFromBytes(addr []byte) (I2PAddr, error) { func NewI2PAddrFromBytes(addr []byte) (I2PAddr, error) {
if len(addr) > MaxAddressLength || len(addr) < MinAddressLength { if len(addr) > MaxAddressLength || len(addr) < MinAddressLength {
return I2PAddr(""), fmt.Errorf("invalid address length: got %d, want between %d and %d", return I2PAddr(""), fmt.Errorf("invalid address length: got %d, want between %d and %d",
len(addr), MinAddressLength, MaxAddressLength) len(addr), MinAddressLength, MaxAddressLength)
} }
encoded := make([]byte, i2pB64enc.EncodedLen(len(addr))) encoded := make([]byte, i2pB64enc.EncodedLen(len(addr)))
i2pB64enc.Encode(encoded, addr) i2pB64enc.Encode(encoded, addr)
return I2PAddr(encoded), nil return I2PAddr(encoded), nil
} }
// ToBytes converts the I2P address to its raw byte representation. // ToBytes converts the I2P address to its raw byte representation.
func (addr I2PAddr) ToBytes() ([]byte, error) { func (addr I2PAddr) ToBytes() ([]byte, error) {
decoded, err := i2pB64enc.DecodeString(string(addr)) decoded, err := i2pB64enc.DecodeString(string(addr))
if err != nil { if err != nil {
return nil, fmt.Errorf("decoding address: %w", err) return nil, fmt.Errorf("decoding address: %w", err)
} }
return decoded, nil return decoded, nil
} }
// Base32 returns the *.b32.i2p representation of the address. // Base32 returns the *.b32.i2p representation of the address.
func (addr I2PAddr) Base32() string { func (addr I2PAddr) Base32() string {
return addr.DestHash().String() return addr.DestHash().String()
} }
// DestHash computes the SHA-256 hash of the address. // DestHash computes the SHA-256 hash of the address.
func (addr I2PAddr) DestHash() I2PDestHash { func (addr I2PAddr) DestHash() I2PDestHash {
var hash I2PDestHash var hash I2PDestHash
h := sha256.New() h := sha256.New()
if bytes, err := addr.ToBytes(); err == nil { if bytes, err := addr.ToBytes(); err == nil {
h.Write(bytes) h.Write(bytes)
copy(hash[:], h.Sum(nil)) copy(hash[:], h.Sum(nil))
} }
return hash return hash
} }

View File

@ -70,7 +70,7 @@ func Test_NewI2PAddrFromString(t *testing.T) {
} }
}) })
t.Run("Address with .i2p suffix", func(t *testing.T) { //CHECK t.Run("Address with .i2p suffix", func(t *testing.T) { // CHECK
addr, err := NewI2PAddrFromString(validI2PAddrB64 + ".i2p") addr, err := NewI2PAddrFromString(validI2PAddrB64 + ".i2p")
if err != nil { if err != nil {
t.Fatalf("NewI2PAddrFromString failed for address with .i2p suffix: '%v'", err) t.Fatalf("NewI2PAddrFromString failed for address with .i2p suffix: '%v'", err)
@ -169,15 +169,15 @@ func Test_KeyGenerationAndHandling(t *testing.T) {
t.Fatalf("Failed to generate new I2P keys: %v", err) t.Fatalf("Failed to generate new I2P keys: %v", err)
} }
t.Run("LoadKeysIncompat", func(t *testing.T) { t.Run("LoadKeysIncompat", func(t *testing.T) {
//extract keys // extract keys
addr := keys.Address addr := keys.Address
fmt.Println(addr) fmt.Println(addr)
//both := removeNewlines(keys.Both) // both := removeNewlines(keys.Both)
both := keys.Both both := keys.Both
fmt.Println(both) fmt.Println(both)
//FORMAT TO LOAD: (Address, Both) // FORMAT TO LOAD: (Address, Both)
addrload := addr.Base64() + "\n" + both addrload := addr.Base64() + "\n" + both
r := strings.NewReader(addrload) r := strings.NewReader(addrload)
@ -187,9 +187,8 @@ func Test_KeyGenerationAndHandling(t *testing.T) {
} }
if loadedKeys.Address != keys.Address { if loadedKeys.Address != keys.Address {
//fmt.Printf("loadedKeys.Address md5hash: '%s'\n keys.Address md5hash: '%s'\n", getMD5Hash(string(loadedKeys.Address)), getMD5Hash(string(keys.Address))) // fmt.Printf("loadedKeys.Address md5hash: '%s'\n keys.Address md5hash: '%s'\n", getMD5Hash(string(loadedKeys.Address)), getMD5Hash(string(keys.Address)))
t.Errorf("LoadKeysIncompat returned incorrect address. Got '%s', want '%s'", loadedKeys.Address, keys.Address) t.Errorf("LoadKeysIncompat returned incorrect address. Got '%s', want '%s'", loadedKeys.Address, keys.Address)
} }
if loadedKeys.Both != keys.Both { if loadedKeys.Both != keys.Both {
t.Errorf("LoadKeysIncompat returned incorrect pair. Got '%s'\nwant '%s'\n", loadedKeys.Both, keys.Both) t.Errorf("LoadKeysIncompat returned incorrect pair. Got '%s'\nwant '%s'\n", loadedKeys.Both, keys.Both)
@ -199,7 +198,6 @@ func Test_KeyGenerationAndHandling(t *testing.T) {
} }
*/ */
} }
}) })
expected := keys.Address.Base64() + "\n" + keys.Both expected := keys.Address.Base64() + "\n" + keys.Both

View File

@ -7,20 +7,20 @@ import (
) )
const ( const (
// HashSize is the size of an I2P destination hash in bytes // HashSize is the size of an I2P destination hash in bytes
HashSize = 32 HashSize = 32
// B32AddressLength is the length of a base32 address without suffix // B32AddressLength is the length of a base32 address without suffix
B32AddressLength = 52 B32AddressLength = 52
// FullB32Length is the total length of a .b32.i2p address // FullB32Length is the total length of a .b32.i2p address
FullB32Length = 60 FullB32Length = 60
// B32Padding is the padding used for base32 encoding // B32Padding is the padding used for base32 encoding
B32Padding = "====" B32Padding = "===="
// B32Suffix is the standard suffix for base32 I2P addresses // B32Suffix is the standard suffix for base32 I2P addresses
B32Suffix = ".b32.i2p" B32Suffix = ".b32.i2p"
) )
// I2PDestHash represents a 32-byte I2P destination hash. // I2PDestHash represents a 32-byte I2P destination hash.
@ -30,58 +30,58 @@ type I2PDestHash [HashSize]byte
// DestHashFromString creates a destination hash from a base32-encoded string. // DestHashFromString creates a destination hash from a base32-encoded string.
// The input should be in the format "base32address.b32.i2p". // The input should be in the format "base32address.b32.i2p".
func DestHashFromString(addr string) (I2PDestHash, error) { func DestHashFromString(addr string) (I2PDestHash, error) {
if !isValidB32Address(addr) { if !isValidB32Address(addr) {
return I2PDestHash{}, fmt.Errorf("invalid address format: %s", addr) return I2PDestHash{}, fmt.Errorf("invalid address format: %s", addr)
} }
var hash I2PDestHash var hash I2PDestHash
b32Input := addr[:B32AddressLength] + B32Padding b32Input := addr[:B32AddressLength] + B32Padding
n, err := i2pB32enc.Decode(hash[:], []byte(b32Input)) n, err := i2pB32enc.Decode(hash[:], []byte(b32Input))
if err != nil { if err != nil {
return I2PDestHash{}, fmt.Errorf("decoding base32 address: %w", err) return I2PDestHash{}, fmt.Errorf("decoding base32 address: %w", err)
} }
if n != HashSize { if n != HashSize {
return I2PDestHash{}, fmt.Errorf("decoded hash has invalid length: got %d, want %d", n, HashSize) return I2PDestHash{}, fmt.Errorf("decoded hash has invalid length: got %d, want %d", n, HashSize)
} }
return hash, nil return hash, nil
} }
// isValidB32Address checks if the address has the correct format and length // isValidB32Address checks if the address has the correct format and length
func isValidB32Address(addr string) bool { func isValidB32Address(addr string) bool {
return strings.HasSuffix(addr, B32Suffix) && len(addr) == FullB32Length return strings.HasSuffix(addr, B32Suffix) && len(addr) == FullB32Length
} }
// DestHashFromBytes creates a destination hash from a byte slice. // DestHashFromBytes creates a destination hash from a byte slice.
// The input must be exactly 32 bytes long. // The input must be exactly 32 bytes long.
func DestHashFromBytes(data []byte) (I2PDestHash, error) { func DestHashFromBytes(data []byte) (I2PDestHash, error) {
if len(data) != HashSize { if len(data) != HashSize {
return I2PDestHash{}, fmt.Errorf("invalid hash length: got %d, want %d", len(data), HashSize) return I2PDestHash{}, fmt.Errorf("invalid hash length: got %d, want %d", len(data), HashSize)
} }
var hash I2PDestHash var hash I2PDestHash
copy(hash[:], data) copy(hash[:], data)
return hash, nil return hash, nil
} }
// String returns the base32-encoded representation with the .b32.i2p suffix. // String returns the base32-encoded representation with the .b32.i2p suffix.
func (h I2PDestHash) String() string { func (h I2PDestHash) String() string {
encoded := make([]byte, i2pB32enc.EncodedLen(HashSize)) encoded := make([]byte, i2pB32enc.EncodedLen(HashSize))
i2pB32enc.Encode(encoded, h[:]) i2pB32enc.Encode(encoded, h[:])
return string(encoded[:B32AddressLength]) + B32Suffix return string(encoded[:B32AddressLength]) + B32Suffix
} }
// Hash returns the base64-encoded SHA-256 hash of the destination hash. // Hash returns the base64-encoded SHA-256 hash of the destination hash.
func (h I2PDestHash) Hash() string { func (h I2PDestHash) Hash() string {
digest := sha256.Sum256(h[:]) digest := sha256.Sum256(h[:])
encoded := make([]byte, i2pB64enc.EncodedLen(len(digest))) encoded := make([]byte, i2pB64enc.EncodedLen(len(digest)))
i2pB64enc.Encode(encoded, digest[:]) i2pB64enc.Encode(encoded, digest[:])
return string(encoded[:44]) return string(encoded[:44])
} }
// Network returns the network type, always "I2P". // Network returns the network type, always "I2P".
func (h I2PDestHash) Network() string { func (h I2PDestHash) Network() string {
return "I2P" return "I2P"
} }

View File

@ -128,6 +128,7 @@ func StoreKeysIncompat(k I2PKeys, w io.Writer) error {
log.WithField("keys", k).Debug("Keys stored successfully") log.WithField("keys", k).Debug("Keys stored successfully")
return nil return nil
} }
func StoreKeys(k I2PKeys, r string) error { func StoreKeys(k I2PKeys, r string) error {
log.WithField("filename", r).Debug("Storing keys to file") log.WithField("filename", r).Debug("Storing keys to file")
if _, err := os.Stat(r); err != nil { if _, err := os.Stat(r); err != nil {
@ -190,7 +191,7 @@ func (k I2PKeys) PrivateKey() crypto.PrivateKey {
_, err := pk.Sign(rand.Reader, []byte("nonsense"), crypto.Hash(0)) _, err := pk.Sign(rand.Reader, []byte("nonsense"), crypto.Hash(0))
if err != nil { if err != nil {
log.WithError(err).Warn("Error in private key signature") log.WithError(err).Warn("Error in private key signature")
//TODO: Elgamal, P256, P384, P512, GOST? keys? // TODO: Elgamal, P256, P384, P512, GOST? keys?
} }
return pk return pk
} }
@ -225,4 +226,3 @@ func (k I2PKeys) HostnameEntry(hostname string, opts crypto.SignerOpts) (string,
} }
return string(sig), nil return string(sig), nil
} }

4
log.go
View File

@ -4,9 +4,7 @@ import (
"github.com/go-i2p/logger" "github.com/go-i2p/logger"
) )
var ( var log *logger.Logger
log *logger.Logger
)
func InitializeI2PKeysLogger() { func InitializeI2PKeysLogger() {
logger.InitializeGoI2PLogger() logger.InitializeGoI2PLogger()

187
new.go
View File

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