diff --git a/I2PAddr.go b/I2PAddr.go index 2da57b2..7cd05c5 100644 --- a/I2PAddr.go +++ b/I2PAddr.go @@ -7,13 +7,13 @@ import ( ) const ( - // Address length constraints - MinAddressLength = 516 - MaxAddressLength = 4096 - - // Domain suffixes - I2PDomainSuffix = ".i2p" - B32DomainSuffix = ".b32.i2p" + // Address length constraints + MinAddressLength = 516 + MaxAddressLength = 4096 + + // Domain suffixes + I2PDomainSuffix = ".i2p" + B32DomainSuffix = ".b32.i2p" ) // 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. func (a I2PAddr) Base64() string { - return string(a) + return string(a) } // String returns either the base64 or base32 representation based on configuration. func (a I2PAddr) String() string { - if StringIsBase64 { - return a.Base64() - } - return a.Base32() + if StringIsBase64 { + return a.Base64() + } + return a.Base32() } // Network returns the network type, always "I2P". func (a I2PAddr) Network() string { - return "I2P" + return "I2P" } // NewI2PAddrFromString creates a new I2P address from a base64-encoded string. // It validates the format and returns an error if the address is invalid. func NewI2PAddrFromString(addr string) (I2PAddr, error) { - addr = sanitizeAddress(addr) - - if err := validateAddressFormat(addr); err != nil { - return I2PAddr(""), err - } - - if err := validateBase64Encoding(addr); err != nil { - return I2PAddr(""), err - } - - return I2PAddr(addr), nil + addr = sanitizeAddress(addr) + + if err := validateAddressFormat(addr); err != nil { + return I2PAddr(""), err + } + + if err := validateBase64Encoding(addr); err != nil { + return I2PAddr(""), err + } + + return I2PAddr(addr), nil } func sanitizeAddress(addr string) string { - // Remove domain suffix if present - addr = strings.TrimSuffix(addr, I2PDomainSuffix) - return strings.Trim(addr, "\t\n\r\f ") + // Remove domain suffix if present + addr = strings.TrimSuffix(addr, I2PDomainSuffix) + return strings.Trim(addr, "\t\n\r\f ") } func validateAddressFormat(addr string) error { - if len(addr) > MaxAddressLength || len(addr) < MinAddressLength { - return fmt.Errorf("invalid address length: got %d, want between %d and %d", - len(addr), MinAddressLength, MaxAddressLength) - } - - if strings.HasSuffix(addr, B32DomainSuffix) { - return fmt.Errorf("cannot convert %s to full destination", B32DomainSuffix) - } - - return nil + if len(addr) > MaxAddressLength || len(addr) < MinAddressLength { + return fmt.Errorf("invalid address length: got %d, want between %d and %d", + len(addr), MinAddressLength, MaxAddressLength) + } + + if strings.HasSuffix(addr, B32DomainSuffix) { + return fmt.Errorf("cannot convert %s to full destination", B32DomainSuffix) + } + + return nil } func validateBase64Encoding(addr string) error { - buf := make([]byte, i2pB64enc.DecodedLen(len(addr))) - if _, err := i2pB64enc.Decode(buf, []byte(addr)); err != nil { - return fmt.Errorf("invalid base64 encoding: %w", err) - } - return nil + buf := make([]byte, i2pB64enc.DecodedLen(len(addr))) + if _, err := i2pB64enc.Decode(buf, []byte(addr)); err != nil { + return fmt.Errorf("invalid base64 encoding: %w", err) + } + return nil } // NewI2PAddrFromBytes creates a new I2P address from a byte array. func NewI2PAddrFromBytes(addr []byte) (I2PAddr, error) { - if len(addr) > MaxAddressLength || len(addr) < MinAddressLength { - return I2PAddr(""), fmt.Errorf("invalid address length: got %d, want between %d and %d", - len(addr), MinAddressLength, MaxAddressLength) - } - - encoded := make([]byte, i2pB64enc.EncodedLen(len(addr))) - i2pB64enc.Encode(encoded, addr) - return I2PAddr(encoded), nil + if len(addr) > MaxAddressLength || len(addr) < MinAddressLength { + return I2PAddr(""), fmt.Errorf("invalid address length: got %d, want between %d and %d", + len(addr), MinAddressLength, MaxAddressLength) + } + + encoded := make([]byte, i2pB64enc.EncodedLen(len(addr))) + i2pB64enc.Encode(encoded, addr) + return I2PAddr(encoded), nil } // ToBytes converts the I2P address to its raw byte representation. func (addr I2PAddr) ToBytes() ([]byte, error) { - decoded, err := i2pB64enc.DecodeString(string(addr)) - if err != nil { - return nil, fmt.Errorf("decoding address: %w", err) - } - return decoded, nil + decoded, err := i2pB64enc.DecodeString(string(addr)) + if err != nil { + return nil, fmt.Errorf("decoding address: %w", err) + } + return decoded, nil } // Base32 returns the *.b32.i2p representation of the address. func (addr I2PAddr) Base32() string { - return addr.DestHash().String() + return addr.DestHash().String() } // DestHash computes the SHA-256 hash of the address. func (addr I2PAddr) DestHash() I2PDestHash { - var hash I2PDestHash - h := sha256.New() - if bytes, err := addr.ToBytes(); err == nil { - h.Write(bytes) - copy(hash[:], h.Sum(nil)) - } - return hash -} \ No newline at end of file + var hash I2PDestHash + h := sha256.New() + if bytes, err := addr.ToBytes(); err == nil { + h.Write(bytes) + copy(hash[:], h.Sum(nil)) + } + return hash +} diff --git a/I2PAddr_test.go b/I2PAddr_test.go index 92bf45e..e0a3d14 100644 --- a/I2PAddr_test.go +++ b/I2PAddr_test.go @@ -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") if err != nil { 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.Run("LoadKeysIncompat", func(t *testing.T) { - //extract keys + // extract keys addr := keys.Address fmt.Println(addr) - //both := removeNewlines(keys.Both) + // both := removeNewlines(keys.Both) both := keys.Both fmt.Println(both) - //FORMAT TO LOAD: (Address, Both) + // FORMAT TO LOAD: (Address, Both) addrload := addr.Base64() + "\n" + both r := strings.NewReader(addrload) @@ -187,9 +187,8 @@ func Test_KeyGenerationAndHandling(t *testing.T) { } 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) - } if 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 diff --git a/I2PDestHash.go b/I2PDestHash.go index 722bcc8..eebca1b 100644 --- a/I2PDestHash.go +++ b/I2PDestHash.go @@ -7,20 +7,20 @@ import ( ) const ( - // HashSize is the size of an I2P destination hash in bytes - HashSize = 32 - - // B32AddressLength is the length of a base32 address without suffix - B32AddressLength = 52 - - // FullB32Length is the total length of a .b32.i2p address - FullB32Length = 60 - - // B32Padding is the padding used for base32 encoding - B32Padding = "====" - - // B32Suffix is the standard suffix for base32 I2P addresses - B32Suffix = ".b32.i2p" + // HashSize is the size of an I2P destination hash in bytes + HashSize = 32 + + // B32AddressLength is the length of a base32 address without suffix + B32AddressLength = 52 + + // FullB32Length is the total length of a .b32.i2p address + FullB32Length = 60 + + // B32Padding is the padding used for base32 encoding + B32Padding = "====" + + // B32Suffix is the standard suffix for base32 I2P addresses + B32Suffix = ".b32.i2p" ) // 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. // The input should be in the format "base32address.b32.i2p". func DestHashFromString(addr string) (I2PDestHash, error) { - if !isValidB32Address(addr) { - return I2PDestHash{}, fmt.Errorf("invalid address format: %s", addr) - } + if !isValidB32Address(addr) { + return I2PDestHash{}, fmt.Errorf("invalid address format: %s", addr) + } - var hash I2PDestHash - b32Input := addr[:B32AddressLength] + B32Padding - - n, err := i2pB32enc.Decode(hash[:], []byte(b32Input)) - if err != nil { - return I2PDestHash{}, fmt.Errorf("decoding base32 address: %w", err) - } - - if n != HashSize { - return I2PDestHash{}, fmt.Errorf("decoded hash has invalid length: got %d, want %d", n, HashSize) - } - - return hash, nil + var hash I2PDestHash + b32Input := addr[:B32AddressLength] + B32Padding + + n, err := i2pB32enc.Decode(hash[:], []byte(b32Input)) + if err != nil { + return I2PDestHash{}, fmt.Errorf("decoding base32 address: %w", err) + } + + if n != HashSize { + return I2PDestHash{}, fmt.Errorf("decoded hash has invalid length: got %d, want %d", n, HashSize) + } + + return hash, nil } // isValidB32Address checks if the address has the correct format and length 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. // The input must be exactly 32 bytes long. func DestHashFromBytes(data []byte) (I2PDestHash, error) { - if len(data) != HashSize { - return I2PDestHash{}, fmt.Errorf("invalid hash length: got %d, want %d", len(data), HashSize) - } + if len(data) != HashSize { + return I2PDestHash{}, fmt.Errorf("invalid hash length: got %d, want %d", len(data), HashSize) + } - var hash I2PDestHash - copy(hash[:], data) - return hash, nil + var hash I2PDestHash + copy(hash[:], data) + return hash, nil } // String returns the base32-encoded representation with the .b32.i2p suffix. func (h I2PDestHash) String() string { - encoded := make([]byte, i2pB32enc.EncodedLen(HashSize)) - i2pB32enc.Encode(encoded, h[:]) - return string(encoded[:B32AddressLength]) + B32Suffix + encoded := make([]byte, i2pB32enc.EncodedLen(HashSize)) + i2pB32enc.Encode(encoded, h[:]) + return string(encoded[:B32AddressLength]) + B32Suffix } // Hash returns the base64-encoded SHA-256 hash of the destination hash. func (h I2PDestHash) Hash() string { - digest := sha256.Sum256(h[:]) - encoded := make([]byte, i2pB64enc.EncodedLen(len(digest))) - i2pB64enc.Encode(encoded, digest[:]) - return string(encoded[:44]) + digest := sha256.Sum256(h[:]) + encoded := make([]byte, i2pB64enc.EncodedLen(len(digest))) + i2pB64enc.Encode(encoded, digest[:]) + return string(encoded[:44]) } // Network returns the network type, always "I2P". func (h I2PDestHash) Network() string { - return "I2P" -} \ No newline at end of file + return "I2P" +} diff --git a/I2PKeys.go b/I2PKeys.go index 9352bab..ea16428 100644 --- a/I2PKeys.go +++ b/I2PKeys.go @@ -128,6 +128,7 @@ func StoreKeysIncompat(k I2PKeys, w io.Writer) error { log.WithField("keys", k).Debug("Keys stored successfully") return nil } + func StoreKeys(k I2PKeys, r string) error { log.WithField("filename", r).Debug("Storing keys to file") 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)) if err != nil { 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 } @@ -225,4 +226,3 @@ func (k I2PKeys) HostnameEntry(hostname string, opts crypto.SignerOpts) (string, } return string(sig), nil } - diff --git a/log.go b/log.go index b277907..7472895 100644 --- a/log.go +++ b/log.go @@ -4,9 +4,7 @@ import ( "github.com/go-i2p/logger" ) -var ( - log *logger.Logger -) +var log *logger.Logger func InitializeI2PKeysLogger() { logger.InitializeGoI2PLogger() diff --git a/new.go b/new.go index 9f0de89..76f966c 100644 --- a/new.go +++ b/new.go @@ -9,145 +9,146 @@ import ( "time" ) +var DefaultSAMAddress = "127.0.0.1:7656" + const ( - defaultSAMAddress = "127.0.0.1:7656" - defaultTimeout = 30 * time.Second - maxResponseSize = 4096 - - cmdHello = "HELLO VERSION MIN=3.1 MAX=3.1\n" - cmdGenerate = "DEST GENERATE SIGNATURE_TYPE=7\n" - responseOK = "RESULT=OK" - pubKeyPrefix = "PUB=" - privKeyPrefix = "PRIV=" + defaultTimeout = 30 * time.Second + maxResponseSize = 4096 + + cmdHello = "HELLO VERSION MIN=3.1 MAX=3.1\n" + cmdGenerate = "DEST GENERATE SIGNATURE_TYPE=7\n" + responseOK = "RESULT=OK" + pubKeyPrefix = "PUB=" + privKeyPrefix = "PRIV=" ) // samClient handles communication with the SAM bridge type samClient struct { - addr string - timeout time.Duration + 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 + 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() (*I2PKeys, error) { - ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) - defer cancel() + ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) + defer cancel() - client := newSAMClient() - return client.generateDestination(ctx) + client := newSAMClient() + return client.generateDestination(ctx) } // generateDestination handles the key generation process func (c *samClient) generateDestination(ctx context.Context) (*I2PKeys, error) { - conn, err := c.dial(ctx) - if err != nil { - return nil, fmt.Errorf("connecting to SAM bridge: %w", err) - } - defer conn.Close() + conn, err := c.dial(ctx) + if err != nil { + return nil, fmt.Errorf("connecting to SAM bridge: %w", err) + } + defer conn.Close() - if err := c.handshake(ctx, conn); err != nil { - return nil, fmt.Errorf("SAM handshake failed: %w", err) - } + if err := c.handshake(ctx, conn); err != nil { + return nil, fmt.Errorf("SAM handshake failed: %w", err) + } - keys, err := c.generateKeys(ctx, conn) - if err != nil { - return nil, fmt.Errorf("generating keys: %w", err) - } + keys, err := c.generateKeys(ctx, conn) + if err != nil { + return nil, fmt.Errorf("generating keys: %w", err) + } - return keys, nil + 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 + 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 - } + if err := c.writeCommand(conn, cmdHello); err != nil { + return err + } - response, err := c.readResponse(conn) - if 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) - } + if !strings.Contains(response, responseOK) { + return fmt.Errorf("unexpected SAM response: %s", response) + } - return nil + return nil } func (c *samClient) generateKeys(ctx context.Context, conn net.Conn) (*I2PKeys, error) { - if err := c.writeCommand(conn, cmdGenerate); err != nil { - return nil, err - } + if err := c.writeCommand(conn, cmdGenerate); err != nil { + return nil, err + } - response, err := c.readResponse(conn) - if 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 - } + pub, priv, err := parseKeyResponse(response) + if err != nil { + return nil, err + } - return &I2PKeys{ - Address: I2PAddr(pub), - Both: pub + priv, - }, nil + 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 + _, 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 + 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") - } + 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") - } + 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]) + pub = strings.TrimSpace(pubParts[1]) + priv = strings.TrimSpace(parts[1]) - return pub, priv, nil -} \ No newline at end of file + return pub, priv, nil +}