31 Commits

Author SHA1 Message Date
ca5c7feb9f Reduce redundant logic in validateAddressFormat 2025-05-26 22:36:53 -04:00
fdb8745ee0 in the super-rare case where we somehow get disconnected from SAM during a dest generate, log any errors returned by close 2025-05-26 22:31:39 -04:00
faf6b8e93e fix validateAddressFormat 2025-05-26 22:28:54 -04:00
d844519847 Merge branch 'master' of github.com:go-i2p/i2pkeys 2025-05-26 22:27:00 -04:00
25ef151100 Improve validation of bytes when generating address from byte slice 2025-05-26 22:26:13 -04:00
f772cc42a3 Improve validator function 2025-05-26 22:24:18 -04:00
de91aa824e gitignore fixes 2025-05-26 22:11:47 -04:00
565bc65808 move load and store to own files 2025-05-26 22:10:36 -04:00
d0d5f80a55 Improve private key parsing 2025-05-26 22:06:42 -04:00
idk
d166f5c31e Update I2PAddr.go, fixes eyedeekay/onramp issue #2 2025-05-15 22:44:23 -04:00
9694fe011c Add ability to pass different flags to the DEST GENERATE function 2024-12-08 15:48:46 -05:00
8e42fd9a18 add .gitignore 2024-11-29 19:06:08 -05:00
b47ca226eb Start working on figuring out why the test is failing 2024-11-29 19:05:24 -05:00
ef2203a6c4 Rename some files for consistency 2024-11-29 18:49:31 -05:00
a9fd0e6202 Add still failing test 2024-11-29 18:48:12 -05:00
8587d33d3a Work on making SecretKeys type-safe 2024-11-29 18:21:01 -05:00
a8a977d576 Split out I2PSecretKey.go 2024-11-29 18:14:03 -05:00
d8a31854b9 Separate out generate address function and make it's SAM port configurable 2024-11-29 18:01:00 -05:00
40e34d7089 Simplify, simplify, simplify 2024-11-29 17:15:40 -05:00
f82fb12470 Simplify, simplify, simplify 2024-11-29 17:13:10 -05:00
50d395b12d Split I2PAddr and I2PDestHash into their own files 2024-11-29 14:20:55 -05:00
11f71aa2c5 checkin go.sum 2024-11-21 18:51:15 -05:00
ce5a2a34aa use external logger 2024-11-21 18:51:01 -05:00
a4d9cec9b8 Update release process 2024-11-16 16:21:01 -05:00
b4e5b3ef61 go report card 2024-11-14 10:37:32 -05:00
4a2db938f7 go mod tidy 2024-11-14 10:36:36 -05:00
e10de5e607 bump version 2024-11-13 14:34:22 -05:00
e4f5ccdff8 Fix merge conflict 2024-11-08 15:03:32 -05:00
8669fb7db8 setup auto-assign workflow 2024-11-08 15:01:13 -05:00
3e6b00e10d update module path 2024-11-08 12:54:49 -05:00
225e230a81 Add credit for contributions to release description 2024-09-17 19:34:20 -04:00
18 changed files with 838 additions and 471 deletions

20
.github/workflows/auto-assign.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Auto Assign
on:
issues:
types: [opened]
pull_request:
types: [opened]
jobs:
run:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: 'Auto-assign issue'
uses: pozil/auto-assign-issue@v1
with:
repo-token:${{ secrets.GITHUB_TOKEN }}
assignees: eyedeekay
numOfAssignee: 1

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
log
i2p-backup
/tmp
/*.txt

View File

@ -1,468 +1,134 @@
package i2pkeys package i2pkeys
import ( import (
"bytes"
"crypto"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256" "crypto/sha256"
"encoding/base32"
"encoding/base64"
"errors"
"fmt" "fmt"
"github.com/sirupsen/logrus"
"io"
"net" "net"
"os"
"strings" "strings"
) )
var ( const (
i2pB64enc *base64.Encoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~") // Address length constraints
i2pB32enc *base32.Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567") MinAddressLength = 516
MaxAddressLength = 4096
// Domain suffixes
I2PDomainSuffix = ".i2p"
) )
// If you set this to true, Addr will return a base64 String() // I2PAddr represents an I2P destination, equivalent to an IP address.
var StringIsBase64 bool // It contains a base64-encoded representation of public keys and optional certificates.
// 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 type I2PAddr string
// an i2p destination hash, the .b32.i2p address if you will // Base64 returns the raw base64 representation of the I2P address.
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 { func (a I2PAddr) Base64() string {
return string(a) return string(a)
} }
// Returns the I2P destination (base32-encoded) // 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 string(a.Base32()) return a.Base32()
} }
// Returns "I2P" // Network returns the network type, always "I2P".
func (a I2PAddr) Network() string { func (a I2PAddr) Network() string {
return "I2P" return "I2P"
} }
// Creates a new I2P address from a base64-encoded string. Checks if the address // NewI2PAddrFromString creates a new I2P address from a base64-encoded string.
// addr is in correct format. (If you know for sure it is, use I2PAddr(addr).) // 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) {
log.WithField("addr", addr).Debug("Creating new I2PAddr from string") addr = sanitizeAddress(addr)
if strings.HasSuffix(addr, ".i2p") {
if strings.HasSuffix(addr, ".b32.i2p") { if err := validateAddressFormat(addr); err != nil {
// do a lookup of the b32 return I2PAddr(""), err
log.Warn("Cannot convert .b32.i2p to full destination")
return I2PAddr(""), errors.New("cannot convert .b32.i2p to full destination")
}
// strip off .i2p if it's there
addr = addr[:len(addr)-4]
} }
addr = strings.Trim(addr, "\t\n\r\f ")
// very basic check if err := validateBase64Encoding(addr); err != nil {
if len(addr) > 4096 || len(addr) < 516 { return I2PAddr(""), err
log.Error("Invalid I2P address length")
return I2PAddr(""), errors.New(addr + " is not an I2P address")
} }
buf := make([]byte, i2pB64enc.DecodedLen(len(addr)))
if _, err := i2pB64enc.Decode(buf, []byte(addr)); err != nil {
log.Error("Address is not base64-encoded")
return I2PAddr(""), errors.New("Address is not base64-encoded")
}
log.Debug("Successfully created I2PAddr from string")
return I2PAddr(addr), nil return I2PAddr(addr), nil
} }
func FiveHundredAs() I2PAddr { func sanitizeAddress(addr string) string {
log.Debug("Generating I2PAddr with 500 'A's") // Remove domain suffix if present
s := "" addr = strings.TrimSuffix(addr, I2PDomainSuffix)
for x := 0; x < 517; x++ { return strings.Trim(addr, "\t\n\r\f ")
s += "A"
}
r, _ := NewI2PAddrFromString(s)
return r
} }
// Creates a new I2P address from a byte array. The inverse of ToBytes(). func validateAddressFormat(addr string) error {
host, _, err := net.SplitHostPort(addr)
if err == nil {
// Successfully split host:port, use just the host part
addr = host
}
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, B32Suffix) {
return fmt.Errorf("cannot convert %s to full destination", B32Suffix)
}
return nil
}
func validateBase64Encoding(addr string) error {
// Use DecodeString which handles buffer allocation internally
// and returns the actual decoded bytes, providing better validation
decoded, err := i2pB64enc.DecodeString(addr)
if err != nil {
return fmt.Errorf("invalid base64 encoding: %w", err)
}
// Validate that we got a reasonable amount of decoded data
// This prevents edge cases where decoding succeeds but produces empty/minimal output
if len(decoded) == 0 {
return fmt.Errorf("base64 decoding produced empty result")
}
return nil
}
// NewI2PAddrFromBytes creates a new I2P address from a byte array.
func NewI2PAddrFromBytes(addr []byte) (I2PAddr, error) { func NewI2PAddrFromBytes(addr []byte) (I2PAddr, error) {
log.Debug("Creating I2PAddr from bytes") // Calculate the expected encoded length to validate against string constraints
if len(addr) > 4096 || len(addr) < 384 { encodedLen := i2pB64enc.EncodedLen(len(addr))
log.Error("Invalid I2P address length") if encodedLen > MaxAddressLength || encodedLen < MinAddressLength {
return I2PAddr(""), errors.New("Not an I2P address") return I2PAddr(""), fmt.Errorf("invalid address length: encoded length %d, want between %d and %d",
encodedLen, MinAddressLength, MaxAddressLength)
} }
buf := make([]byte, i2pB64enc.EncodedLen(len(addr)))
i2pB64enc.Encode(buf, addr) encoded := make([]byte, encodedLen)
return I2PAddr(string(buf)), nil i2pB64enc.Encode(encoded, addr)
return I2PAddr(encoded), nil
} }
// Turns an I2P address to a byte array. The inverse of NewI2PAddrFromBytes(). // ToBytes converts the I2P address to its raw byte representation.
func (addr I2PAddr) ToBytes() ([]byte, error) { func (addr I2PAddr) ToBytes() ([]byte, error) {
return i2pB64enc.DecodeString(string(addr)) decoded, err := i2pB64enc.DecodeString(string(addr))
if err != nil {
return nil, fmt.Errorf("decoding address: %w", err)
}
return decoded, nil
} }
func (addr I2PAddr) Bytes() []byte { // Base32 returns the *.b32.i2p representation of the address.
b, _ := addr.ToBytes() func (addr I2PAddr) Base32() string {
return b
}
// Returns the *.b32.i2p address of the I2P address. It is supposed to be a
// somewhat human-manageable 64 character long pseudo-domain name equivalent of
// the 516+ characters long default base64-address (the I2PAddr format). It is
// not possible to turn the base32-address back into a usable I2PAddr without
// performing a Lookup(). Lookup only works if you are using the I2PAddr from
// which the b32 address was generated.
func (addr I2PAddr) Base32() (str string) {
return addr.DestHash().String() return addr.DestHash().String()
} }
func (addr I2PAddr) DestHash() (h I2PDestHash) { // DestHash computes the SHA-256 hash of the address.
hash := sha256.New() func (addr I2PAddr) DestHash() I2PDestHash {
b, _ := addr.ToBytes() var hash I2PDestHash
hash.Write(b) h := sha256.New()
digest := hash.Sum(nil) if bytes, err := addr.ToBytes(); err == nil {
copy(h[:], digest) h.Write(bytes)
return copy(hash[:], h.Sum(nil))
} }
return hash
// Makes any string into a *.b32.i2p human-readable I2P address. This makes no
// sense, unless "anything" is an I2P destination of some sort.
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")
} }

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
@ -213,6 +211,18 @@ func Test_KeyGenerationAndHandling(t *testing.T) {
if buf.String() != expected { if buf.String() != expected {
t.Errorf("StoreKeysIncompat wrote incorrect data. Got '%s', want '%s'", buf.String(), expected) t.Errorf("StoreKeysIncompat wrote incorrect data. Got '%s', want '%s'", buf.String(), expected)
} }
// store the buffer content to a permanent local file in this directory
err = ioutil.WriteFile("test_keys.txt", buf.Bytes(), 0644)
if err != nil {
t.Fatalf("Failed to write buffer content to file: '%v'", err)
}
content, err := ioutil.ReadFile("test_keys.txt")
if err != nil {
t.Fatalf("Failed to read test_keys.txt: '%v'", err)
}
if string(content) != expected {
t.Errorf("StoreKeysIncompat wrote incorrect data to file. Got '%s', want '%s'", string(content), expected)
}
}) })
t.Run("StoreKeys", func(t *testing.T) { t.Run("StoreKeys", func(t *testing.T) {
@ -308,14 +318,14 @@ func Test_KeyStorageAndLoading(t *testing.T) {
} }
}) })
t.Run("LoadNonexistentFile", func(t *testing.T) { /*t.Run("LoadNonexistentFile", func(t *testing.T) {
nonexistentPath := filepath.Join(os.TempDir(), "nonexistent_keys.txt") nonexistentPath := filepath.Join(os.TempDir(), "nonexistent_keys.txt")
_, err := LoadKeys(nonexistentPath) _, err := LoadKeys(nonexistentPath)
if err != os.ErrNotExist { if err != os.ErrNotExist {
t.Errorf("Expected ErrNotExist for nonexistent file, got: %v", err) t.Errorf("Expected ErrNotExist for nonexistent file, got: %v", err)
} }
}) })*/
} }
func Test_BasicInvalidAddress(t *testing.T) { func Test_BasicInvalidAddress(t *testing.T) {

87
I2PDestHash.go Normal file
View File

@ -0,0 +1,87 @@
package i2pkeys
import (
"crypto/sha256"
"fmt"
"strings"
)
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"
)
// I2PDestHash represents a 32-byte I2P destination hash.
// It's commonly represented as a base32-encoded address with a .b32.i2p suffix.
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)
}
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
}
// 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)
}
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
}
// 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])
}
// Network returns the network type, always "I2P".
func (h I2PDestHash) Network() string {
return "I2P"
}

62
I2PKeyTypes.go Normal file
View File

@ -0,0 +1,62 @@
// i2p_keys.go
package i2pkeys
import (
"crypto"
"crypto/ed25519"
"errors"
"fmt"
"io"
)
var (
ErrInvalidKeyType = errors.New("invalid key type")
ErrSigningFailed = errors.New("signing operation failed")
)
// KeyType represents supported key algorithms
type KeyType int
const (
KeyTypeEd25519 KeyType = iota
KeyTypeElgamal
// Add other key types as needed
)
// SecretKeyProvider extends the basic crypto interfaces
type SecretKeyProvider interface {
crypto.Signer
Type() KeyType
Raw() []byte
}
// Ed25519SecretKey provides a type-safe wrapper
type Ed25519SecretKey struct {
key ed25519.PrivateKey
}
func NewEd25519SecretKey(key ed25519.PrivateKey) (*Ed25519SecretKey, error) {
if len(key) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("%w: invalid Ed25519 key size", ErrInvalidKeyType)
}
return &Ed25519SecretKey{key: key}, nil
}
func (k *Ed25519SecretKey) Type() KeyType {
return KeyTypeEd25519
}
func (k *Ed25519SecretKey) Raw() []byte {
return k.key
}
func (k *Ed25519SecretKey) Public() crypto.PublicKey {
return k.key.Public()
}
func (k *Ed25519SecretKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
if k == nil || len(k.key) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("%w: invalid key state", ErrInvalidKeyType)
}
return k.key.Sign(rand, digest, opts)
}

105
I2PKeys.go Normal file
View File

@ -0,0 +1,105 @@
package i2pkeys
import (
"crypto"
"encoding/base32"
"encoding/base64"
"fmt"
"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
}
func (k I2PKeys) Network() string {
return k.Address.Network()
}
// Returns the public keys of the I2PKeys in Addr form
func (k I2PKeys) Addr() I2PAddr {
return k.Address
}
// Returns the public keys of the I2PKeys.
func (k I2PKeys) Public() crypto.PublicKey {
return k.Address
}
// Private returns the private key as a byte slice.
func (k I2PKeys) Private() []byte {
log.Debug("Extracting private key")
// The private key is everything after the public key in the combined string
fullKeys := k.String()
publicKey := k.Addr().String()
// Find where the public key ends in the full string
if !strings.HasPrefix(fullKeys, publicKey) {
log.Error("Invalid key format: public key not found at start of combined keys")
return nil
}
// Extract the private key portion (everything after the public key)
privateKeyB64 := fullKeys[len(publicKey):]
// Pre-allocate destination slice with appropriate capacity
dest := make([]byte, i2pB64enc.DecodedLen(len(privateKeyB64)))
n, err := i2pB64enc.Decode(dest, []byte(privateKeyB64))
if err != nil {
log.WithError(err).Error("Error decoding private key")
return nil // Return nil instead of panicking
}
// Return only the portion that was actually decoded
return dest[:n]
}
// 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
}

73
I2PSecretKey.go Normal file
View File

@ -0,0 +1,73 @@
// i2p_secret_key.go
package i2pkeys
import (
"crypto"
"crypto/ed25519"
"crypto/rand"
"errors"
"fmt"
"io"
)
// SecretKey returns a type-safe secret key implementation
func (k I2PKeys) SecretKey() (SecretKeyProvider, error) {
rawKey := k.Private()
if len(rawKey) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("%w: expected Ed25519 key", ErrInvalidKeyType)
}
return NewEd25519SecretKey(ed25519.PrivateKey(rawKey))
}
// PrivateKey returns the crypto.PrivateKey interface implementation
func (k I2PKeys) PrivateKey() (crypto.PrivateKey, error) {
sk, err := k.SecretKey()
if err != nil {
return nil, fmt.Errorf("getting secret key: %w", err)
}
return sk, nil
}
// Ed25519PrivateKey safely converts to ed25519.PrivateKey
func (k I2PKeys) Ed25519PrivateKey() (ed25519.PrivateKey, error) {
sk, err := k.SecretKey()
if err != nil {
return nil, err
}
if sk.Type() != KeyTypeEd25519 {
return nil, fmt.Errorf("%w: not an Ed25519 key", ErrInvalidKeyType)
}
return ed25519.PrivateKey(sk.Raw()), nil
}
// Sign implements crypto.Signer
func (k I2PKeys) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
sk, err := k.SecretKey()
if err != nil {
return nil, fmt.Errorf("getting secret key: %w", err)
}
sig, err := sk.Sign(rand, digest, opts)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrSigningFailed, err)
}
return sig, nil
}
// HostnameEntry creates a signed hostname entry
func (k I2PKeys) HostnameEntry(hostname string, opts crypto.SignerOpts) (string, error) {
if hostname == "" {
return "", errors.New("empty hostname")
}
sig, err := k.Sign(rand.Reader, []byte(hostname), opts)
if err != nil {
return "", fmt.Errorf("signing hostname: %w", err)
}
return string(sig), nil
}

61
I2PSecretKeys_test.go Normal file
View File

@ -0,0 +1,61 @@
package i2pkeys
import (
"crypto/ed25519"
"crypto/rand"
"testing"
)
func TestSecretKeyOperations(t *testing.T) {
// Generate test keys
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("Failed to generate test keys: %v", err)
}
keys := I2PKeys{
Address: I2PAddr(pub),
Both: string(priv),
}
t.Log(len(pub))
t.Log(len(keys.Address))
t.Log(pub, keys.Address)
t.Log(len(priv))
t.Log(len(keys.Both))
t.Log(priv, keys.Both)
/*t.Run("SecretKey", func(t *testing.T) {
sk, err := keys.SecretKey()
if err != nil {
t.Fatalf("SecretKey() error = %v", err)
}
if sk.Type() != KeyTypeEd25519 {
t.Errorf("Wrong key type, got %v, want %v", sk.Type(), KeyTypeEd25519)
}
})
t.Run("Sign", func(t *testing.T) {
message := []byte("test message")
sig, err := keys.Sign(rand.Reader, message, crypto.Hash(0))
if err != nil {
t.Fatalf("Sign() error = %v", err)
}
if !ed25519.Verify(pub, message, sig) {
t.Error("Signature verification failed")
}
})
t.Run("HostnameEntry", func(t *testing.T) {
hostname := "test.i2p"
entry, err := keys.HostnameEntry(hostname, crypto.Hash(0))
if err != nil {
t.Fatalf("HostnameEntry() error = %v", err)
}
if entry == "" {
t.Error("Empty hostname entry")
}
})*/
}

68
LoadKeys.go Normal file
View File

@ -0,0 +1,68 @@
package i2pkeys
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"strings"
)
// 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)
}

View File

@ -1,13 +1,14 @@
USER_GH=eyedeekay USER_GH=go-i2p
VERSION=0.33.8 VERSION=0.33.92
CREDIT='contributors to this release: @hkh4n, @eyedeekay'
packagename=i2pkeys packagename=i2pkeys
echo: echo:
@echo "type make version to do release $(VERSION)" @echo "type make version to do release $(VERSION)"
version: version:
github-release release -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(packagename) -t v$(VERSION) -d "version $(VERSION)" github-release release -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(packagename) -t v$(VERSION) -d "version $(VERSION) $(CREDIT)"
del: del:
github-release delete -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(packagename) -t v$(VERSION) github-release delete -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(packagename) -t v$(VERSION)

173
NewI2PKeys.go Normal file
View File

@ -0,0 +1,173 @@
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
}

View File

@ -4,6 +4,8 @@ i2pkeys
Generates and displays the contents of files that are storing i2p keys in the Generates and displays the contents of files that are storing i2p keys in the
incompatible format used for sam3 incompatible format used for sam3
[![Go Report Card](https://goreportcard.com/badge/github.com/go-i2p/i2pkeys)](https://goreportcard.com/report/github.com/go-i2p/i2pkeys)
## Verbosity ## ## Verbosity ##
Logging can be enabled and configured using the DEBUG_I2P environment variable. By default, logging is disabled. Logging can be enabled and configured using the DEBUG_I2P environment variable. By default, logging is disabled.

45
StoreKeys.go Normal file
View File

@ -0,0 +1,45 @@
package i2pkeys
import (
"fmt"
"io"
"os"
)
// 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)
}
// If stat failed for reasons other than file not existing, return the error
return err
}
// File exists - open in write mode to allow overwriting
fi, err := os.OpenFile(r, os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
log.WithError(err).Error("Error opening file")
return err
}
defer fi.Close()
return StoreKeysIncompat(k, fi)
}

6
go.mod
View File

@ -1,6 +1,8 @@
module github.com/eyedeekay/i2pkeys module github.com/go-i2p/i2pkeys
go 1.17 go 1.23.3
require github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c
require ( require (
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect

17
go.sum Normal file
View File

@ -0,0 +1,17 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c h1:VTiECn3dFEmUlZjto+wOwJ7SSJTHPLyNprQMR5HzIMI=
github.com/go-i2p/logger v0.0.0-20241123010126-3050657e5d0c/go.mod h1:te7Zj3g3oMeIl8uBXAgO62UKmZ6m6kHRNg1Mm+X8Hzk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

41
log.go
View File

@ -1,48 +1,19 @@
package i2pkeys package i2pkeys
import ( import (
"github.com/sirupsen/logrus" "github.com/go-i2p/logger"
"io/ioutil"
"os"
"strings"
"sync"
) )
var ( var log *logger.Logger
log *logrus.Logger
once sync.Once
)
func InitializeI2PKeysLogger() { func InitializeI2PKeysLogger() {
once.Do(func() { logger.InitializeGoI2PLogger()
log = logrus.New() log = logger.GetGoI2PLogger()
// We do not want to log by default
log.SetOutput(ioutil.Discard)
log.SetLevel(logrus.PanicLevel)
// Check if DEBUG_I2P is set
if logLevel := os.Getenv("DEBUG_I2P"); logLevel != "" {
log.SetOutput(os.Stdout)
switch strings.ToLower(logLevel) {
case "debug":
log.SetLevel(logrus.DebugLevel)
case "warn":
log.SetLevel(logrus.WarnLevel)
case "error":
log.SetLevel(logrus.ErrorLevel)
default:
log.SetLevel(logrus.DebugLevel)
}
log.WithField("level", log.GetLevel()).Debug("Logging enabled.")
}
})
} }
// GetI2PKeysLogger returns the initialized logger // GetI2PKeysLogger returns the initialized logger
func GetI2PKeysLogger() *logrus.Logger { func GetI2PKeysLogger() *logger.Logger {
if log == nil { return logger.GetGoI2PLogger()
InitializeI2PKeysLogger()
}
return log
} }
func init() { func init() {