diff --git a/I2PAddr.go b/I2PAddr.go index bb382b7..0568170 100644 --- a/I2PAddr.go +++ b/I2PAddr.go @@ -1,298 +1,17 @@ package i2pkeys import ( - "bytes" - "crypto" - "crypto/ed25519" - "crypto/rand" "crypto/sha256" - "encoding/base32" - "encoding/base64" "errors" - "fmt" - "github.com/sirupsen/logrus" - "io" - "net" - "os" "strings" ) -var ( - i2pB64enc *base64.Encoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~") - i2pB32enc *base32.Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567") -) - -// If you set this to true, Addr will return a base64 String() -var StringIsBase64 bool - -// The public and private keys associated with an I2P destination. I2P hides the -// details of exactly what this is, so treat them as blobs, but generally: One -// pair of DSA keys, one pair of ElGamal keys, and sometimes (almost never) also -// a certificate. String() returns you the full content of I2PKeys and Addr() -// returns the public keys. -type I2PKeys struct { - Address I2PAddr // only the public key - Both string // both public and private keys -} - -// Creates I2PKeys from an I2PAddr and a public/private keypair string (as -// generated by String().) -func NewKeys(addr I2PAddr, both string) I2PKeys { - log.WithField("addr", addr).Debug("Creating new I2PKeys") - return I2PKeys{addr, both} -} - -// fileExists checks if a file exists and is not a directory before we -// try using it to prevent further errors. -func fileExists(filename string) (bool, error) { - info, err := os.Stat(filename) - if os.IsNotExist(err) { - log.WithField("filename", filename).Debug("File does not exist") - return false, nil - } else if err != nil { - log.WithError(err).WithField("filename", filename).Error("Error checking file existence") - return false, fmt.Errorf("error checking file existence: %w", err) - } - exists := !info.IsDir() - if exists { - log.WithField("filename", filename).Debug("File exists") - } else { - log.WithField("filename", filename).Debug("File is a directory") - } - return !info.IsDir(), nil -} - -// LoadKeysIncompat loads keys from a non-standard format -func LoadKeysIncompat(r io.Reader) (I2PKeys, error) { - log.Debug("Loading keys from reader") - var buff bytes.Buffer - _, err := io.Copy(&buff, r) - if err != nil { - log.WithError(err).Error("Error copying from reader, did not load keys") - return I2PKeys{}, fmt.Errorf("error copying from reader: %w", err) - } - - parts := strings.Split(buff.String(), "\n") - if len(parts) < 2 { - err := errors.New("invalid key format: not enough data") - log.WithError(err).Error("Error parsing keys") - return I2PKeys{}, err - } - - k := I2PKeys{I2PAddr(parts[0]), parts[1]} - log.WithField("keys", k).Debug("Loaded keys") - return k, nil -} - -// load keys from non-standard format by specifying a text file. -// If the file does not exist, generate keys, otherwise, fail -// closed. -func LoadKeys(r string) (I2PKeys, error) { - log.WithField("filename", r).Debug("Loading keys from file") - exists, err := fileExists(r) - if err != nil { - log.WithError(err).Error("Error checking if file exists") - return I2PKeys{}, err - } - if !exists { - // File doesn't exist so we'll generate new keys - log.WithError(err).Debug("File does not exist, attempting to generate new keys") - k, err := NewDestination() - if err != nil { - log.WithError(err).Error("Error generating new keys") - return I2PKeys{}, err - } - // Save the new keys to the file - err = StoreKeys(*k, r) - if err != nil { - log.WithError(err).Error("Error saving new keys to file") - return I2PKeys{}, err - } - return *k, nil - } - fi, err := os.Open(r) - if err != nil { - log.WithError(err).WithField("filename", r).Error("Error opening file") - return I2PKeys{}, fmt.Errorf("error opening file: %w", err) - } - defer fi.Close() - log.WithField("filename", r).Debug("File opened successfully") - return LoadKeysIncompat(fi) -} - -// store keys in non standard format -func StoreKeysIncompat(k I2PKeys, w io.Writer) error { - log.Debug("Storing keys") - _, err := io.WriteString(w, k.Address.Base64()+"\n"+k.Both) - if err != nil { - log.WithError(err).Error("Error writing keys") - return fmt.Errorf("error writing keys: %w", err) - } - 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 { - if os.IsNotExist(err) { - log.WithField("filename", r).Debug("File does not exist, creating new file") - fi, err := os.Create(r) - if err != nil { - log.WithError(err).Error("Error creating file") - return err - } - defer fi.Close() - return StoreKeysIncompat(k, fi) - } - } - fi, err := os.Open(r) - if err != nil { - log.WithError(err).Error("Error opening file") - return err - } - defer fi.Close() - return StoreKeysIncompat(k, fi) -} - -func (k I2PKeys) Network() string { - return k.Address.Network() -} - -// Returns the public keys of the I2PKeys. -func (k I2PKeys) Addr() I2PAddr { - return k.Address -} - -func (k I2PKeys) Public() crypto.PublicKey { - return k.Address -} - -func (k I2PKeys) Private() []byte { - log.Debug("Extracting private key") - src := strings.Split(k.String(), k.Addr().String())[0] - var dest []byte - _, err := i2pB64enc.Decode(dest, []byte(src)) - if err != nil { - log.WithError(err).Error("Error decoding private key") - panic(err) - } - return dest -} - -type SecretKey interface { - Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) -} - -func (k I2PKeys) SecretKey() SecretKey { - var pk ed25519.PrivateKey = k.Private() - return pk -} - -func (k I2PKeys) PrivateKey() crypto.PrivateKey { - var pk ed25519.PrivateKey = k.Private() - _, 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? - } - return pk -} - -func (k I2PKeys) Ed25519PrivateKey() *ed25519.PrivateKey { - return k.SecretKey().(*ed25519.PrivateKey) -} - -/*func (k I2PKeys) ElgamalPrivateKey() *ed25519.PrivateKey { - return k.SecretKey().(*ed25519.PrivateKey) -}*/ - -//func (k I2PKeys) Decrypt(rand io.Reader, msg []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) { -//return k.SecretKey().(*ed25519.PrivateKey).Decrypt(rand, msg, opts) -//} - -func (k I2PKeys) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { - return k.SecretKey().(*ed25519.PrivateKey).Sign(rand, digest, opts) -} - -// Returns the keys (both public and private), in I2Ps base64 format. Use this -// when you create sessions. -func (k I2PKeys) String() string { - return k.Both -} - -func (k I2PKeys) HostnameEntry(hostname string, opts crypto.SignerOpts) (string, error) { - sig, err := k.Sign(rand.Reader, []byte(hostname), opts) - if err != nil { - log.WithError(err).Error("Error signing hostname") - return "", fmt.Errorf("error signing hostname: %w", err) - } - return string(sig), nil -} - // I2PAddr represents an I2P destination, almost equivalent to an IP address. // This is the humongously huge base64 representation of such an address, which // really is just a pair of public keys and also maybe a certificate. (I2P hides // the details of exactly what it is. Read the I2P specifications for more info.) type I2PAddr string -// an i2p destination hash, the .b32.i2p address if you will -type I2PDestHash [32]byte - -// create a desthash from a string b32.i2p address -func DestHashFromString(str string) (dhash I2PDestHash, err error) { - log.WithField("address", str).Debug("Creating desthash from string") - if strings.HasSuffix(str, ".b32.i2p") && len(str) == 60 { - // valid - _, err = i2pB32enc.Decode(dhash[:], []byte(str[:52]+"====")) - if err != nil { - log.WithError(err).Error("Error decoding base32 address") - } - } else { - // invalid - err = errors.New("invalid desthash format") - log.WithError(err).Error("Invalid desthash format") - } - return -} - -// create a desthash from a []byte array -func DestHashFromBytes(str []byte) (dhash I2PDestHash, err error) { - log.Debug("Creating DestHash from bytes") - if len(str) == 32 { - // valid - //_, err = i2pB32enc.Decode(dhash[:], []byte(str[:52]+"====")) - log.WithField("str", str).Debug("Copying str to desthash") - copy(dhash[:], str) - } else { - // invalid - err = errors.New("invalid desthash format") - log.WithField("str", str).Error("Invalid desthash format") - } - return -} - -// get string representation of i2p dest hash(base32 version) -func (h I2PDestHash) String() string { - b32addr := make([]byte, 56) - i2pB32enc.Encode(b32addr, h[:]) - return string(b32addr[:52]) + ".b32.i2p" -} - -// get base64 representation of i2p dest sha256 hash(the 44-character one) -func (h I2PDestHash) Hash() string { - hash := sha256.New() - hash.Write(h[:]) - digest := hash.Sum(nil) - buf := make([]byte, 44) - i2pB64enc.Encode(buf, digest) - return string(buf) -} - -// Returns "I2P" -func (h I2PDestHash) Network() string { - return "I2P" -} - // Returns the base64 representation of the I2PAddr func (a I2PAddr) Base64() string { return string(a) @@ -395,74 +114,3 @@ func (addr I2PAddr) DestHash() (h I2PDestHash) { func Base32(anything string) string { return I2PAddr(anything).Base32() } - -/* -HELLO VERSION MIN=3.1 MAX=3.1 -DEST GENERATE SIGNATURE_TYPE=7 -*/ -func NewDestination() (*I2PKeys, error) { - removeNewlines := func(s string) string { - return strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", ""), "\n", "") - } - // - log.Debug("Creating new destination via SAM") - conn, err := net.Dial("tcp", "127.0.0.1:7656") - if err != nil { - return nil, err - } - defer conn.Close() - _, err = conn.Write([]byte("HELLO VERSION MIN=3.1 MAX=3.1\n")) - if err != nil { - log.WithError(err).Error("Error writing to SAM bridge") - return nil, err - } - buf := make([]byte, 4096) - n, err := conn.Read(buf) - if err != nil { - log.WithError(err).Error("Error reading from SAM bridge") - return nil, err - } - if n < 1 { - log.Error("No data received from SAM bridge") - return nil, fmt.Errorf("no data received") - } - - response := string(buf[:n]) - log.WithField("response", response).Debug("Received response from SAM bridge") - - if strings.Contains(string(buf[:n]), "RESULT=OK") { - _, err = conn.Write([]byte("DEST GENERATE SIGNATURE_TYPE=7\n")) - if err != nil { - log.WithError(err).Error("Error writing DEST GENERATE to SAM bridge") - return nil, err - } - n, err = conn.Read(buf) - if err != nil { - log.WithError(err).Error("Error reading destination from SAM bridge") - return nil, err - } - if n < 1 { - log.Error("No destination data received from SAM bridge") - return nil, fmt.Errorf("no destination data received") - } - pub := strings.Split(strings.Split(string(buf[:n]), "PRIV=")[0], "PUB=")[1] - _priv := strings.Split(string(buf[:n]), "PRIV=")[1] - - priv := removeNewlines(_priv) //There is an extraneous newline in the private key, so we'll remove it. - - log.WithFields(logrus.Fields{ - "_priv(pre-newline removal)": _priv, - "priv": priv, - }).Debug("Removed newline") - - log.Debug("Successfully created new destination") - - return &I2PKeys{ - Address: I2PAddr(pub), - Both: pub + priv, - }, nil - - } - log.Error("No RESULT=OK received from SAM bridge") - return nil, fmt.Errorf("no result received") -} diff --git a/I2PDestHash.go b/I2PDestHash.go new file mode 100644 index 0000000..a81a183 --- /dev/null +++ b/I2PDestHash.go @@ -0,0 +1,65 @@ +package i2pkeys + +import ( + "crypto/sha256" + "errors" + "strings" +) + +// an i2p destination hash, the .b32.i2p address if you will +type I2PDestHash [32]byte + +// create a desthash from a string b32.i2p address +func DestHashFromString(str string) (dhash I2PDestHash, err error) { + log.WithField("address", str).Debug("Creating desthash from string") + if strings.HasSuffix(str, ".b32.i2p") && len(str) == 60 { + // valid + _, err = i2pB32enc.Decode(dhash[:], []byte(str[:52]+"====")) + if err != nil { + log.WithError(err).Error("Error decoding base32 address") + } + } else { + // invalid + err = errors.New("invalid desthash format") + log.WithError(err).Error("Invalid desthash format") + } + return +} + +// create a desthash from a []byte array +func DestHashFromBytes(str []byte) (dhash I2PDestHash, err error) { + log.Debug("Creating DestHash from bytes") + if len(str) == 32 { + // valid + //_, err = i2pB32enc.Decode(dhash[:], []byte(str[:52]+"====")) + log.WithField("str", str).Debug("Copying str to desthash") + copy(dhash[:], str) + } else { + // invalid + err = errors.New("invalid desthash format") + log.WithField("str", str).Error("Invalid desthash format") + } + return +} + +// get string representation of i2p dest hash(base32 version) +func (h I2PDestHash) String() string { + b32addr := make([]byte, 56) + i2pB32enc.Encode(b32addr, h[:]) + return string(b32addr[:52]) + ".b32.i2p" +} + +// get base64 representation of i2p dest sha256 hash(the 44-character one) +func (h I2PDestHash) Hash() string { + hash := sha256.New() + hash.Write(h[:]) + digest := hash.Sum(nil) + buf := make([]byte, 44) + i2pB64enc.Encode(buf, digest) + return string(buf) +} + +// Returns "I2P" +func (h I2PDestHash) Network() string { + return "I2P" +} diff --git a/I2PKeys.go b/I2PKeys.go new file mode 100644 index 0000000..f073cd6 --- /dev/null +++ b/I2PKeys.go @@ -0,0 +1,301 @@ +package i2pkeys + +import ( + "bytes" + "crypto" + "crypto/ed25519" + "crypto/rand" + "encoding/base32" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "os" + "strings" + + "github.com/sirupsen/logrus" +) + +var ( + i2pB64enc *base64.Encoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~") + i2pB32enc *base32.Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567") +) + +// If you set this to true, Addr will return a base64 String() +var StringIsBase64 bool + +// The public and private keys associated with an I2P destination. I2P hides the +// details of exactly what this is, so treat them as blobs, but generally: One +// pair of DSA keys, one pair of ElGamal keys, and sometimes (almost never) also +// a certificate. String() returns you the full content of I2PKeys and Addr() +// returns the public keys. +type I2PKeys struct { + Address I2PAddr // only the public key + Both string // both public and private keys +} + +// Creates I2PKeys from an I2PAddr and a public/private keypair string (as +// generated by String().) +func NewKeys(addr I2PAddr, both string) I2PKeys { + log.WithField("addr", addr).Debug("Creating new I2PKeys") + return I2PKeys{addr, both} +} + +// fileExists checks if a file exists and is not a directory before we +// try using it to prevent further errors. +func fileExists(filename string) (bool, error) { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + log.WithField("filename", filename).Debug("File does not exist") + return false, nil + } else if err != nil { + log.WithError(err).WithField("filename", filename).Error("Error checking file existence") + return false, fmt.Errorf("error checking file existence: %w", err) + } + exists := !info.IsDir() + if exists { + log.WithField("filename", filename).Debug("File exists") + } else { + log.WithField("filename", filename).Debug("File is a directory") + } + return !info.IsDir(), nil +} + +// LoadKeysIncompat loads keys from a non-standard format +func LoadKeysIncompat(r io.Reader) (I2PKeys, error) { + log.Debug("Loading keys from reader") + var buff bytes.Buffer + _, err := io.Copy(&buff, r) + if err != nil { + log.WithError(err).Error("Error copying from reader, did not load keys") + return I2PKeys{}, fmt.Errorf("error copying from reader: %w", err) + } + + parts := strings.Split(buff.String(), "\n") + if len(parts) < 2 { + err := errors.New("invalid key format: not enough data") + log.WithError(err).Error("Error parsing keys") + return I2PKeys{}, err + } + + k := I2PKeys{I2PAddr(parts[0]), parts[1]} + log.WithField("keys", k).Debug("Loaded keys") + return k, nil +} + +// load keys from non-standard format by specifying a text file. +// If the file does not exist, generate keys, otherwise, fail +// closed. +func LoadKeys(r string) (I2PKeys, error) { + log.WithField("filename", r).Debug("Loading keys from file") + exists, err := fileExists(r) + if err != nil { + log.WithError(err).Error("Error checking if file exists") + return I2PKeys{}, err + } + if !exists { + // File doesn't exist so we'll generate new keys + log.WithError(err).Debug("File does not exist, attempting to generate new keys") + k, err := NewDestination() + if err != nil { + log.WithError(err).Error("Error generating new keys") + return I2PKeys{}, err + } + // Save the new keys to the file + err = StoreKeys(*k, r) + if err != nil { + log.WithError(err).Error("Error saving new keys to file") + return I2PKeys{}, err + } + return *k, nil + } + fi, err := os.Open(r) + if err != nil { + log.WithError(err).WithField("filename", r).Error("Error opening file") + return I2PKeys{}, fmt.Errorf("error opening file: %w", err) + } + defer fi.Close() + log.WithField("filename", r).Debug("File opened successfully") + return LoadKeysIncompat(fi) +} + +// store keys in non standard format +func StoreKeysIncompat(k I2PKeys, w io.Writer) error { + log.Debug("Storing keys") + _, err := io.WriteString(w, k.Address.Base64()+"\n"+k.Both) + if err != nil { + log.WithError(err).Error("Error writing keys") + return fmt.Errorf("error writing keys: %w", err) + } + 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 { + if os.IsNotExist(err) { + log.WithField("filename", r).Debug("File does not exist, creating new file") + fi, err := os.Create(r) + if err != nil { + log.WithError(err).Error("Error creating file") + return err + } + defer fi.Close() + return StoreKeysIncompat(k, fi) + } + } + fi, err := os.Open(r) + if err != nil { + log.WithError(err).Error("Error opening file") + return err + } + defer fi.Close() + return StoreKeysIncompat(k, fi) +} + +func (k I2PKeys) Network() string { + return k.Address.Network() +} + +// Returns the public keys of the I2PKeys. +func (k I2PKeys) Addr() I2PAddr { + return k.Address +} + +func (k I2PKeys) Public() crypto.PublicKey { + return k.Address +} + +func (k I2PKeys) Private() []byte { + log.Debug("Extracting private key") + src := strings.Split(k.String(), k.Addr().String())[0] + var dest []byte + _, err := i2pB64enc.Decode(dest, []byte(src)) + if err != nil { + log.WithError(err).Error("Error decoding private key") + panic(err) + } + return dest +} + +type SecretKey interface { + Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) +} + +func (k I2PKeys) SecretKey() SecretKey { + var pk ed25519.PrivateKey = k.Private() + return pk +} + +func (k I2PKeys) PrivateKey() crypto.PrivateKey { + var pk ed25519.PrivateKey = k.Private() + _, 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? + } + return pk +} + +func (k I2PKeys) Ed25519PrivateKey() *ed25519.PrivateKey { + return k.SecretKey().(*ed25519.PrivateKey) +} + +/*func (k I2PKeys) ElgamalPrivateKey() *ed25519.PrivateKey { + return k.SecretKey().(*ed25519.PrivateKey) +}*/ + +//func (k I2PKeys) Decrypt(rand io.Reader, msg []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) { +//return k.SecretKey().(*ed25519.PrivateKey).Decrypt(rand, msg, opts) +//} + +func (k I2PKeys) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { + return k.SecretKey().(*ed25519.PrivateKey).Sign(rand, digest, opts) +} + +// Returns the keys (both public and private), in I2Ps base64 format. Use this +// when you create sessions. +func (k I2PKeys) String() string { + return k.Both +} + +func (k I2PKeys) HostnameEntry(hostname string, opts crypto.SignerOpts) (string, error) { + sig, err := k.Sign(rand.Reader, []byte(hostname), opts) + if err != nil { + log.WithError(err).Error("Error signing hostname") + return "", fmt.Errorf("error signing hostname: %w", err) + } + return string(sig), nil +} + +/* +HELLO VERSION MIN=3.1 MAX=3.1 +DEST GENERATE SIGNATURE_TYPE=7 +*/ +func NewDestination() (*I2PKeys, error) { + removeNewlines := func(s string) string { + return strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", ""), "\n", "") + } + // + log.Debug("Creating new destination via SAM") + conn, err := net.Dial("tcp", "127.0.0.1:7656") + if err != nil { + return nil, err + } + defer conn.Close() + _, err = conn.Write([]byte("HELLO VERSION MIN=3.1 MAX=3.1\n")) + if err != nil { + log.WithError(err).Error("Error writing to SAM bridge") + return nil, err + } + buf := make([]byte, 4096) + n, err := conn.Read(buf) + if err != nil { + log.WithError(err).Error("Error reading from SAM bridge") + return nil, err + } + if n < 1 { + log.Error("No data received from SAM bridge") + return nil, fmt.Errorf("no data received") + } + + response := string(buf[:n]) + log.WithField("response", response).Debug("Received response from SAM bridge") + + if strings.Contains(string(buf[:n]), "RESULT=OK") { + _, err = conn.Write([]byte("DEST GENERATE SIGNATURE_TYPE=7\n")) + if err != nil { + log.WithError(err).Error("Error writing DEST GENERATE to SAM bridge") + return nil, err + } + n, err = conn.Read(buf) + if err != nil { + log.WithError(err).Error("Error reading destination from SAM bridge") + return nil, err + } + if n < 1 { + log.Error("No destination data received from SAM bridge") + return nil, fmt.Errorf("no destination data received") + } + pub := strings.Split(strings.Split(string(buf[:n]), "PRIV=")[0], "PUB=")[1] + _priv := strings.Split(string(buf[:n]), "PRIV=")[1] + + priv := removeNewlines(_priv) //There is an extraneous newline in the private key, so we'll remove it. + + log.WithFields(logrus.Fields{ + "_priv(pre-newline removal)": _priv, + "priv": priv, + }).Debug("Removed newline") + + log.Debug("Successfully created new destination") + + return &I2PKeys{ + Address: I2PAddr(pub), + Both: pub + priv, + }, nil + + } + log.Error("No RESULT=OK received from SAM bridge") + return nil, fmt.Errorf("no result received") +}