diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b75e82a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/go-i2p/i2pkeys-converter + +go 1.24.2 diff --git a/i2pkeys/extractor.go b/i2pkeys/extractor.go new file mode 100644 index 0000000..bcc1dd8 --- /dev/null +++ b/i2pkeys/extractor.go @@ -0,0 +1,222 @@ +// Package i2pkeys provides utilities for working with I2P key formats +package i2pkeys + +import ( + "encoding/base64" + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +// I2P uses a custom Base64 encoding with '-' and '~' instead of '+' and '/' +var i2pB64Encoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~") + +// KeyPair represents an I2P key pair with both public and private components +type KeyPair struct { + PublicKey []byte // The destination (public key) + PrivateKey []byte // The private key + FullData []byte // The complete key data +} + +// ConvertKeyFile converts an I2P binary key file to the two-line format required by Go I2P +func ConvertKeyFile(inputPath, outputPath string) error { + // Read the key file as binary data + data, err := os.ReadFile(inputPath) + if err != nil { + return fmt.Errorf("failed to read key file: %w", err) + } + + // Check if input is already in the expected format + if IsCorrectFormat(string(data)) { + // Create output directory if needed + outputDir := filepath.Dir(outputPath) + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Just copy the file as is + if err := os.WriteFile(outputPath, data, 0600); err != nil { + return fmt.Errorf("failed to write output file: %w", err) + } + + return nil + } + + // Try to extract public key information if it's in I2P Base64 format + keyData := string(data) + var formattedOutput string + + // If data is in I2P Base64 format, try to extract the public key portion + if isI2PBase64Format(keyData) { + // Split by newlines in case there are multiple keys + lines := strings.Split(keyData, "\n") + completeKey := lines[0] + + // For I2P tunnel keys, the public key is the first 516 characters + // This is a heuristic based on the standard format of I2P keys + if len(completeKey) >= 516 { + publicPart := completeKey[:516] + formattedOutput = publicPart + "\n" + completeKey + } else { + // If we can't extract, convert the entire binary file + completeKey = toI2PBase64(data) + + // Public key is typically the first 516 characters + if len(completeKey) >= 516 { + publicPart := completeKey[:516] + formattedOutput = publicPart + "\n" + completeKey + } else { + return errors.New("key data too short to extract public key portion") + } + } + } else { + // Not in Base64 format, treat as binary and convert + completeKey := toI2PBase64(data) + + // Public key is typically the first 516 characters + if len(completeKey) >= 516 { + publicPart := completeKey[:516] + formattedOutput = publicPart + "\n" + completeKey + } else { + return errors.New("key data too short to extract public key portion") + } + } + + // Create output directory if needed + outputDir := filepath.Dir(outputPath) + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Write formatted output to file + if err := os.WriteFile(outputPath, []byte(formattedOutput), 0600); err != nil { + return fmt.Errorf("failed to write output file: %w", err) + } + + return nil +} + +// IsCorrectFormat checks if the data is already in the correct two-line format +func IsCorrectFormat(data string) bool { + lines := strings.Split(strings.TrimSpace(data), "\n") + if len(lines) != 2 { + return false + } + + // Check if both lines appear to be valid I2P Base64 + return isI2PBase64Format(lines[0]) && isI2PBase64Format(lines[1]) +} + +// isI2PBase64Format checks if a string appears to be in I2P Base64 format +func isI2PBase64Format(data string) bool { + // Remove whitespace + data = strings.TrimSpace(data) + if data == "" { + return false + } + + // Check for I2P Base64 character set + for _, r := range data { + if !((r >= 'A' && r <= 'Z') || + (r >= 'a' && r <= 'z') || + (r >= '0' && r <= '9') || + r == '-' || r == '~' || r == '=') { + return false + } + } + + // Try to decode + _, err := fromI2PBase64(data) + return err == nil +} + +// toI2PBase64 converts binary data to I2P's Base64 variant +func toI2PBase64(data []byte) string { + return i2pB64Encoding.EncodeToString(data) +} + +// fromI2PBase64 converts I2P Base64 format back to binary +func fromI2PBase64(i2pBase64 string) ([]byte, error) { + return i2pB64Encoding.DecodeString(i2pBase64) +} + +// FormatKeysFile formats an existing I2P Base64 key into the proper two-line format +func FormatKeysFile(inputPath, outputPath string) error { + // Read the key file + data, err := os.ReadFile(inputPath) + if err != nil { + return fmt.Errorf("failed to read key file: %w", err) + } + + // Check if it's already in the correct format + if IsCorrectFormat(string(data)) { + // Already in the correct format, just copy + if inputPath != outputPath { + if err := os.WriteFile(outputPath, data, 0600); err != nil { + return fmt.Errorf("failed to write output file: %w", err) + } + } + return nil + } + + // Clean the input + cleanedInput := cleanI2PBase64(string(data)) + + // Split by lines (there might be multiple keys) + lines := strings.Split(cleanedInput, "\n") + + // Process the first non-empty line + var completeKey string + for _, line := range lines { + if strings.TrimSpace(line) != "" { + completeKey = line + break + } + } + + // Ensure we have enough data + if len(completeKey) < 516 { + return errors.New("key data too short to format correctly") + } + + // Extract public key (first 516 characters) + publicPart := completeKey[:516] + + // Create the proper two-line format + formattedOutput := publicPart + "\n" + completeKey + + // Create output directory if needed + outputDir := filepath.Dir(outputPath) + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Write to output file + if err := os.WriteFile(outputPath, []byte(formattedOutput), 0600); err != nil { + return fmt.Errorf("failed to write output file: %w", err) + } + + return nil +} + +// cleanI2PBase64 cleans a string to ensure it only contains valid I2P Base64 characters +func cleanI2PBase64(data string) string { + // Remove whitespace + data = strings.TrimSpace(data) + + // Clean the line of any invalid characters + var cleaned strings.Builder + for _, r := range data { + if (r >= 'A' && r <= 'Z') || + (r >= 'a' && r <= 'z') || + (r >= '0' && r <= '9') || + r == '-' || r == '~' || r == '=' || + r == '\n' { + cleaned.WriteRune(r) + } + } + + return cleaned.String() +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..069f107 --- /dev/null +++ b/main.go @@ -0,0 +1,120 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/go-i2p/i2pkeys-converter/i2pkeys" +) + +func main() { + // Command line arguments + inputFile := flag.String("in", "", "Path to the I2P key file (required)") + outputFile := flag.String("out", "", "Path to save the formatted key (optional)") + verbose := flag.Bool("v", false, "Verbose output with key details") + checkFormat := flag.Bool("check", false, "Check if a file is already in the correct format") + + // Custom usage message + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "I2P Keys Converter - Format I2P keys for Go I2P libraries\n\n") + fmt.Fprintf(os.Stderr, "Usage: %s -in keyfile [-out outputfile] [-v] [-check]\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Options:\n") + flag.PrintDefaults() + fmt.Fprintf(os.Stderr, "\nExamples:\n") + fmt.Fprintf(os.Stderr, " Convert binary key file: %s -in keys.dat -out keys.dat.formatted\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " Check key file format: %s -in keys.dat -check\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " Format with verbose info: %s -in keys.dat -v\n", os.Args[0]) + } + + flag.Parse() + + // Validate input file parameter + if *inputFile == "" { + fmt.Println("Error: Input file (-in) is required") + flag.Usage() + os.Exit(1) + } + + // Check if input file exists + if _, err := os.Stat(*inputFile); os.IsNotExist(err) { + fmt.Printf("Error: Input file '%s' does not exist\n", *inputFile) + os.Exit(1) + } + + // If check mode is enabled, just check the format + if *checkFormat { + data, err := os.ReadFile(*inputFile) + if err != nil { + fmt.Printf("Error reading file: %s\n", err) + os.Exit(1) + } + + if i2pkeys.IsCorrectFormat(string(data)) { + fmt.Println("File IS in the correct two-line format") + os.Exit(0) + } else { + fmt.Println("File is NOT in the correct two-line format") + os.Exit(1) + } + } + + // Set default output file if not specified + if *outputFile == "" { + baseName := filepath.Base(*inputFile) + dir := filepath.Dir(*inputFile) + *outputFile = filepath.Join(dir, baseName+".formatted") + } + + // Print operation info + fmt.Printf("Formatting I2P key file: %s\n", *inputFile) + fmt.Printf("Output file: %s\n", *outputFile) + + // Convert the key file + err := i2pkeys.ConvertKeyFile(*inputFile, *outputFile) + if err != nil { + fmt.Printf("Error: %s\n", err) + os.Exit(1) + } + + // Verify the result + resultData, err := os.ReadFile(*outputFile) + if err != nil { + fmt.Printf("Error reading result file: %s\n", err) + os.Exit(1) + } + + if i2pkeys.IsCorrectFormat(string(resultData)) { + fmt.Println("Conversion successful - key is now in the correct format") + + // Display additional information if verbose mode is enabled + if *verbose { + lines := strings.Split(string(resultData), "\n") + if len(lines) >= 2 { + publicKeyPreview := truncateString(lines[0], 40) + fullKeyPreview := truncateString(lines[1], 40) + + fmt.Println("\nKey Information:") + fmt.Printf("- Destination (public key): %s...\n", publicKeyPreview) + fmt.Printf("- Full key length: %d characters\n", len(lines[1])) + fmt.Printf("- Full key preview: %s...\n", fullKeyPreview) + fmt.Println("\nFormat: Two lines") + fmt.Println("- Line 1: Base64-encoded destination (public key)") + fmt.Println("- Line 2: Base64-encoded full keypair (public + private)") + } + } + } else { + fmt.Println("Warning: Output file is not in the correct format") + os.Exit(1) + } +} + +// truncateString truncates a string and adds ellipsis if needed +func truncateString(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +}