86 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
idk
edace64685 Merge pull request #12 from hkh4n/refactor
return error instead of panic when VALUE= is not found
2024-11-07 17:06:48 +00:00
1712957252 add unit test 2024-11-07 11:39:04 -05:00
eb51810ddd minor grammar tweaks 2024-11-07 11:16:05 -05:00
c4cbd6e041 return error instead of panic when VALUE= is not found 2024-11-07 11:07:13 -05:00
idk
84d1ec1a12 Merge pull request #9 from hkh4n/tests
added Test_KeyStorageAndLoading
2024-11-04 05:01:36 +00:00
idk
43fb66564b Merge pull request #10 from hkh4n/refactor
Refactored LoadKeysIncompat & Finished up LoadKeys()
2024-11-03 22:19:33 +00:00
2e82fab112 update Makefile 2024-11-03 12:01:55 -05:00
daa08faa71 Minor typo fix 2024-11-02 19:42:22 -04:00
a05ca99118 Merge branch 'loadkeys' into refactor 2024-10-26 22:59:30 -04:00
b99e77153e Make LoadKeys() create new keys if the keyfile doesn't exist 2024-10-26 22:59:06 -04:00
b56afeb346 tweaks 2024-10-26 19:20:37 -04:00
890d71f974 comments 2024-10-26 19:20:05 -04:00
bfcde005b3 Refactored LoadKeysIncompat
-Previously: would log errors regardless at the end
-Better error handling
2024-10-26 19:18:08 -04:00
a9a4310a04 added Test_KeyStorageAndLoading 2024-10-26 18:37:33 -04:00
idk
4cba6e2edd Merge pull request #8 from hkh4n/refactor
return os.ErrNotExist instead of written error
2024-10-26 21:35:44 +00:00
25eedfeed8 return os.ErrNotExist instead of written error 2024-10-26 13:50:59 -04:00
idk
8632c8275d Merge pull request #7 from hkh4n/logging
logging naming convention hotfix
2024-10-23 17:34:31 +00:00
9e61a8e00f Merge branch 'eyedeekay:master' into logging 2024-10-23 00:10:22 -04:00
fb2ca1a92c fix logger naming collision with other libs 2024-10-23 00:09:36 -04:00
1bf0437e54 tweaks 2024-10-17 14:52:00 -04:00
idk
0af72cba75 Merge pull request #6 from hkh4n/logging
Added logging to Lookup.go and I2PAddr.go
2024-10-17 18:51:39 +00:00
a4a38460e3 tweaks 2024-10-17 14:42:38 -04:00
75a7b2aec9 Update README.md to reflect logging 2024-10-17 14:38:35 -04:00
43a6a0e07f adjusted log.go 2024-10-16 14:59:01 -04:00
936c39746c tweaks 2024-10-15 19:48:13 -04:00
8df0f31a4d Added logging to I2PAddr.go
-removed "trace" and "info"
-moved log to its own file
-shifted priv -> _priv (pre newline removal)
-added response string in Lookup()
-added response string in NewDestination()
2024-10-15 09:53:10 -04:00
c1b05d6ede !WIP! - added logging 2024-10-14 23:28:25 -04:00
idk
0ef26b9207 Merge pull request #5 from hkh4n/makefile
Updated Makefile
2024-09-24 13:10:37 -04:00
8eee571a7b -remove boilerplate artifact 2024-09-22 20:04:55 -04:00
95744f9498 -bump version
-added testing from make
2024-09-22 19:52:02 -04:00
225e230a81 Add credit for contributions to release description 2024-09-17 19:34:20 -04:00
idk
b4e9da89e9 Merge pull request #3 from hkh4n/tests
Added tests for addresses and keys. Changed NewDestination()
2024-09-17 18:18:41 -04:00
afae8e6f14 changed urls to fit with tests 2024-09-15 21:56:23 -04:00
6c95fc6ac7 -Critical change: trim newline from private key
-commented out newline investigation in Test_KeyGenerationAndHandling
-Test_KeyGnerationAndHandling works as expected now.
2024-09-15 21:48:51 -04:00
ada0d39af4 Investigating LoadKeysIncompat (amend instead) 2024-09-15 15:17:33 -04:00
59bffea3f3 Merge remote-tracking branch 'origin/tests-keys' into tests-keys
# Conflicts:
#	I2PAddr_test.go
2024-09-15 13:13:36 -04:00
316dc840d6 Investigating LoadKeysIncompat (amend instead) 2024-09-15 13:08:11 -04:00
90da121025 Investigating LoadKeysIncompat x4 2024-09-15 12:31:39 -04:00
d362997650 Investigating LoadKeysIncompat x3 2024-09-15 11:28:47 -04:00
4004d3050d Investigating LoadKeysIncompat x2 2024-09-15 11:16:46 -04:00
0d58ebfa78 Investigating LoadKeysIncompat 2024-09-15 11:15:18 -04:00
acbf68bc58 Added broken tests for keys (TODO) 2024-09-15 00:46:32 -04:00
dc8ef52d46 Revamped failures for Test_basic() and Test_Basic_Lookup().
Added:
-Test_NewI2PAddrFromString()
-Test_I2PAddr()
-Test_DestHashFromString()
-Test_I2PAddrToBytes()

with sub-tests.
2024-09-15 00:45:09 -04:00
aae46b4dec added TestKeyGenerationAndHandling 2024-09-14 21:31:52 -04:00
597e1da68d Merge remote-tracking branch 'origin/tests' into tests + Correct Func names as "Test_"
# Conflicts:
#	I2PAddr_test.go
2024-09-14 08:04:19 -04:00
4cf76aeec2 Added various tests 2024-09-14 07:59:56 -04:00
80bfc77145 Added various tests 2024-09-14 00:32:23 -04:00
idk
5ae94bc639 Merge pull request #1 from hkh4n/error-handling
More robust error handling in I2PAddr.go
2024-09-10 14:33:24 -04:00
c86c07c1df refactored error handling in HostnameEntry() 2024-09-09 20:20:43 -04:00
fde718e1d8 refactored error handling in StoreKeysIncompat() 2024-09-09 20:17:21 -04:00
3a99966c42 refactored error handling in LoadKeys() 2024-09-09 20:07:43 -04:00
d23cb52f2c refactored error handling in fileExists() 2024-09-09 20:02:56 -04:00
81f9b8a8cc update makefile 2024-01-09 14:43:01 -05:00
idk
28d6bf4d97 update go modules 2023-03-07 02:11:16 +00:00
idk
9307ae9cf4 StringIsBase64 is bittorrent mode 2022-09-22 23:14:43 -04:00
idk
e6cb984e8f add option to only return b64s 2022-09-21 00:36:57 -04:00
18 changed files with 1305 additions and 344 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,371 +1,134 @@
package i2pkeys
import (
"bytes"
"crypto"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/base32"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"os"
"strings"
)
var (
i2pB64enc *base64.Encoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~")
i2pB32enc *base32.Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567")
const (
// Address length constraints
MinAddressLength = 516
MaxAddressLength = 4096
// Domain suffixes
I2PDomainSuffix = ".i2p"
)
// 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 {
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) {
return false, nil
} else if err != nil {
return false, err
}
return !info.IsDir(), nil
}
// load keys from non standard format
func LoadKeysIncompat(r io.Reader) (k I2PKeys, err error) {
var buff bytes.Buffer
_, err = io.Copy(&buff, r)
if err == nil {
parts := strings.Split(buff.String(), "\n")
k = I2PKeys{I2PAddr(parts[0]), parts[1]}
}
return
}
// 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) {
exists, err := fileExists(r)
if err != nil {
return I2PKeys{}, err
}
if exists {
fi, err := os.Open(r)
if err != nil {
return I2PKeys{}, err
}
defer fi.Close()
return LoadKeysIncompat(fi)
}
return I2PKeys{}, err
}
// store keys in non standard format
func StoreKeysIncompat(k I2PKeys, w io.Writer) (err error) {
_, err = io.WriteString(w, k.Address.Base64()+"\n"+k.Both)
return
}
func StoreKeys(k I2PKeys, r string) error {
if _, err := os.Stat(r); err != nil {
if os.IsNotExist(err) {
fi, err := os.Create(r)
if err != nil {
return err
}
defer fi.Close()
return StoreKeysIncompat(k, fi)
}
}
fi, err := os.Open(r)
if err != nil {
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 {
src := strings.Split(k.String(), k.Addr().String())[0]
var dest []byte
_, err := i2pB64enc.Decode(dest, []byte(src))
if err != nil {
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 {
//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 {
return "", 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.)
// I2PAddr represents an I2P destination, equivalent to an IP address.
// It contains a base64-encoded representation of public keys and optional certificates.
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) {
if strings.HasSuffix(str, ".b32.i2p") && len(str) == 60 {
// valid
_, err = i2pB32enc.Decode(dhash[:], []byte(str[:52]+"===="))
} else {
// invalid
err = errors.New("invalid desthash format")
}
return
}
// create a desthash from a []byte array
func DestHashFromBytes(str []byte) (dhash I2PDestHash, err error) {
if len(str) == 32 {
// valid
//_, err = i2pB32enc.Decode(dhash[:], []byte(str[:52]+"===="))
copy(dhash[:], str)
} else {
// invalid
err = errors.New("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
// Base64 returns the raw base64 representation of the I2P address.
func (a I2PAddr) Base64() string {
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 {
return string(a.Base32())
if StringIsBase64 {
return a.Base64()
}
return a.Base32()
}
// Returns "I2P"
// Network returns the network type, always "I2P".
func (a I2PAddr) Network() string {
return "I2P"
}
// Creates a new I2P address from a base64-encoded string. Checks if the address
// addr is in correct format. (If you know for sure it is, use I2PAddr(addr).)
// 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) {
if strings.HasSuffix(addr, ".i2p") {
if strings.HasSuffix(addr, ".b32.i2p") {
// do a lookup of the b32
addr = sanitizeAddress(addr)
return I2PAddr(""), errors.New("cannot convert .b32.i2p to full destination")
if err := validateAddressFormat(addr); err != nil {
return I2PAddr(""), err
}
// strip off .i2p if it's there
addr = addr[:len(addr)-4]
}
addr = strings.Trim(addr, "\t\n\r\f ")
// very basic check
if len(addr) > 4096 || len(addr) < 516 {
return I2PAddr(""), errors.New("Not an I2P address")
}
buf := make([]byte, i2pB64enc.DecodedLen(len(addr)))
if _, err := i2pB64enc.Decode(buf, []byte(addr)); err != nil {
return I2PAddr(""), errors.New("Address is not base64-encoded")
if err := validateBase64Encoding(addr); err != nil {
return I2PAddr(""), err
}
return I2PAddr(addr), nil
}
func FiveHundredAs() I2PAddr {
s := ""
for x := 0; x < 517; x++ {
s += "A"
}
r, _ := NewI2PAddrFromString(s)
return r
func sanitizeAddress(addr string) string {
// Remove domain suffix if present
addr = strings.TrimSuffix(addr, I2PDomainSuffix)
return strings.Trim(addr, "\t\n\r\f ")
}
// 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) {
if len(addr) > 4096 || len(addr) < 384 {
return I2PAddr(""), errors.New("Not an I2P address")
// Calculate the expected encoded length to validate against string constraints
encodedLen := i2pB64enc.EncodedLen(len(addr))
if encodedLen > MaxAddressLength || encodedLen < MinAddressLength {
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)
return I2PAddr(string(buf)), nil
encoded := make([]byte, encodedLen)
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) {
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 {
b, _ := addr.ToBytes()
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) {
// Base32 returns the *.b32.i2p representation of the address.
func (addr I2PAddr) Base32() string {
return addr.DestHash().String()
}
func (addr I2PAddr) DestHash() (h I2PDestHash) {
hash := sha256.New()
b, _ := addr.ToBytes()
hash.Write(b)
digest := hash.Sum(nil)
copy(h[:], digest)
return
}
// 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) {
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 {
return nil, err
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
return nil, err
}
if n < 1 {
return nil, fmt.Errorf("no data received")
}
if strings.Contains(string(buf[:n]), "RESULT=OK") {
_, err = conn.Write([]byte("DEST GENERATE SIGNATURE_TYPE=7\n"))
if err != nil {
return nil, err
}
n, err = conn.Read(buf)
if err != nil {
return nil, err
}
if n < 1 {
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]
return &I2PKeys{
Address: I2PAddr(pub),
Both: pub + priv,
}, nil
}
return nil, fmt.Errorf("no result received")
// 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
}

View File

@ -1,21 +1,28 @@
package i2pkeys
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
// "time"
)
const yoursam = "127.0.0.1:7656"
const (
yoursam = "127.0.0.1:7656"
validShortenedI2PAddr = "idk.i2p"
validI2PAddrB32 = "b2o47zwxqjbn7jj37yqkmvbmci7kqubwgxu3umqid7cexmc7xudq.b32.i2p"
validI2PAddrB64 = "spHxea2xhPjKH9yyEeFJ96aqtvKidH-GiWxs8dH6RWS2FrDoWFhuEkfw77pF~Hv57lLhMaMB3qqWjCtYXOjL48Q1zYbr3MAcTO44wwVPjOU1hU77vbJcUuwBeRvaSr2dZx-FiTSOdQuhPD1EozYNRIMFwZ0fZwKf~3Gj4dEWccOLKs~NbiPsj-~tc5tmhAs8yBeoZEqEBe40X75SfSHY-EnstcZevVAwIXYk3zX3KF0mji3bo2QXuTFcMZHHLiLd2AHLRANzWyvQ9DC1rnCsHJM4xxV4dVp0pHkP1hwBo7E0NJvN4nFkQcj-FI2RJ~cFUCk7qc86PRHwvKCjzSlrgjtDsMUwd83Dz1PfpzCqHNLUFWI7uPKbKcJZhasFm4kEhUyupd85q75Ch2IZE9J2JXodSxmseO5ZKcHK6pFtfR-HbzKjIe92TWHsNkmvtoHiUaOVrWnk-cmo2I1W1VxfL08teDxQ13P80uFaMcameRzuFM2F8pSOpoyEJUDRGLEeBQAEAAcAAA=="
)
func Test_Basic(t *testing.T) {
fmt.Println("Test_Basic")
fmt.Println("\tAttaching to SAM at " + yoursam)
keys, err := NewDestination()
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
t.Fatal(err.Error())
}
fmt.Println(keys.String())
}
@ -23,11 +30,309 @@ func Test_Basic(t *testing.T) {
func Test_Basic_Lookup(t *testing.T) {
fmt.Println("Test_Basic")
fmt.Println("\tAttaching to SAM at " + yoursam)
keys, err := Lookup("idk.i2p")
keys, err := Lookup(validShortenedI2PAddr)
if err != nil {
fmt.Println(err.Error())
t.Fail()
return
t.Fatal(err.Error())
}
fmt.Println(keys.String())
}
func Test_NewI2PAddrFromString(t *testing.T) {
t.Run("Valid base64 address", func(t *testing.T) {
addr, err := NewI2PAddrFromString(validI2PAddrB64)
if err != nil {
t.Fatalf("NewI2PAddrFromString failed for valid address: '%v'", err)
}
if addr.Base64() != validI2PAddrB64 {
t.Errorf("NewI2PAddrFromString returned incorrect address. Got '%s', want '%s'", addr.Base64(), validI2PAddrB64)
}
})
t.Run("Invalid address", func(t *testing.T) {
invalidAddr := "not-a-valid-address"
_, err := NewI2PAddrFromString(invalidAddr)
if err == nil {
t.Error("NewI2PAddrFromString should have failed for invalid address")
}
})
t.Run("Base32 address", func(t *testing.T) {
_, err := NewI2PAddrFromString(validI2PAddrB32)
if err == nil {
t.Error("NewI2PAddrFromString should have failed for base32 address")
}
})
t.Run("Empty address", func(t *testing.T) {
_, err := NewI2PAddrFromString("")
if err == nil {
t.Error("NewI2PAddrFromString should have failed for empty address")
}
})
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)
}
if addr.Base64() != validI2PAddrB64 {
t.Errorf("NewI2PAddrFromString returned incorrect address. Got '%s', want '%s'", addr.Base64(), validI2PAddrB64)
}
})
}
func Test_I2PAddr(t *testing.T) {
addr := I2PAddr(validI2PAddrB64)
base32 := addr.Base32()
t.Run("Base32 suffix", func(t *testing.T) {
if !strings.HasSuffix(base32, ".b32.i2p") {
t.Errorf("Base32 address should end with .b32.i2p, got %s", base32)
}
})
t.Run("Base32 length", func(t *testing.T) {
if len(base32) != 60 {
t.Errorf("Base32 address should be 60 characters long, got %d", len(base32))
}
})
}
func Test_DestHashFromString(t *testing.T) {
t.Run("Valid hash", func(t *testing.T) {
hash, err := DestHashFromString(validI2PAddrB32)
if err != nil {
t.Fatalf("DestHashFromString failed for valid hash: '%v'", err)
}
if hash.String() != validI2PAddrB32 {
t.Errorf("DestHashFromString returned incorrect hash. Got '%s', want '%s'", hash.String(), validI2PAddrB32)
}
})
t.Run("Invalid hash", func(t *testing.T) {
invalidHash := "not-a-valid-hash"
_, err := DestHashFromString(invalidHash)
if err == nil {
t.Error("DestHashFromString should have failed for invalid hash")
}
})
t.Run("Empty hash", func(t *testing.T) {
_, err := DestHashFromString("")
if err == nil {
t.Error("DestHashFromString should have failed for empty hash")
}
})
}
func Test_I2PAddrToBytes(t *testing.T) {
addr := I2PAddr(validI2PAddrB64)
t.Run("ToBytes and back", func(t *testing.T) {
decodedBytes, err := addr.ToBytes()
if err != nil {
t.Fatalf("ToBytes failed: '%v'", err)
}
encodedString := i2pB64enc.EncodeToString(decodedBytes)
if encodedString != validI2PAddrB64 {
t.Errorf("Round-trip encoding/decoding failed. Got '%s', want '%s'", encodedString, validI2PAddrB64)
}
})
t.Run("Direct decoding comparison", func(t *testing.T) {
decodedBytes, err := addr.ToBytes()
if err != nil {
t.Fatalf("ToBytes failed: '%v'", err)
}
directlyDecoded, err := i2pB64enc.DecodeString(validI2PAddrB64)
if err != nil {
t.Fatalf("Failed to decode test string using i2pB64enc: '%v'", err)
}
if !bytes.Equal(decodedBytes, directlyDecoded) {
t.Errorf("Mismatch between ToBytes result and direct decoding. ToBytes len: '%d', Direct decoding len: '%d'", len(decodedBytes), len(directlyDecoded))
}
})
}
/*
func removeNewlines(s string) string {
return strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", ""), "\n", "")
}
*/
func Test_KeyGenerationAndHandling(t *testing.T) {
// Generate new keys
keys, err := NewDestination()
if err != nil {
t.Fatalf("Failed to generate new I2P keys: %v", err)
}
t.Run("LoadKeysIncompat", func(t *testing.T) {
// extract keys
addr := keys.Address
fmt.Println(addr)
// both := removeNewlines(keys.Both)
both := keys.Both
fmt.Println(both)
// FORMAT TO LOAD: (Address, Both)
addrload := addr.Base64() + "\n" + both
r := strings.NewReader(addrload)
loadedKeys, err := LoadKeysIncompat(r)
if err != nil {
t.Fatalf("LoadKeysIncompat failed: %v", err)
}
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)))
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)
/*
if loadedKeys.Both == removeNewlines(keys.Both) {
fmt.Println("However, both pairs are correct if newline is removed in generated keys.")
}
*/
}
})
expected := keys.Address.Base64() + "\n" + keys.Both
t.Run("StoreKeysIncompat", func(t *testing.T) {
var buf bytes.Buffer
err := StoreKeysIncompat(*keys, &buf)
if err != nil {
t.Fatalf("StoreKeysIncompat failed: '%v'", err)
}
if 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) {
tmpDir, err := ioutil.TempDir("", "test_keys_")
if err != nil {
t.Fatalf("Failed to create temp directory: '%v'", err)
}
defer os.RemoveAll(tmpDir)
tmpFilePath := filepath.Join(tmpDir, "test_keys.txt")
err = StoreKeys(*keys, tmpFilePath)
if err != nil {
t.Fatalf("StoreKeys failed: '%v'", err)
}
content, err := ioutil.ReadFile(tmpFilePath)
if err != nil {
t.Fatalf("Failed to read temp file: '%v'", err)
}
if string(content) != expected {
t.Errorf("StoreKeys wrote incorrect data. Got '%s', want '%s'", string(content), expected)
}
})
}
func Test_KeyStorageAndLoading(t *testing.T) {
// Generate initial keys
keys, err := NewDestination()
if err != nil {
t.Fatalf("Failed to generate new I2P keys: %v", err)
}
t.Run("StoreAndLoadFile", func(t *testing.T) {
// Create temporary directory for test
tmpDir, err := ioutil.TempDir("", "test_keys_")
if err != nil {
t.Fatalf("Failed to create temp directory: %v", err)
}
defer os.RemoveAll(tmpDir)
tmpFilePath := filepath.Join(tmpDir, "test_keys.txt")
// Store keys to file
err = StoreKeys(*keys, tmpFilePath)
if err != nil {
t.Fatalf("StoreKeys failed: %v", err)
}
// Load keys from file
loadedKeys, err := LoadKeys(tmpFilePath)
if err != nil {
t.Fatalf("LoadKeys failed: %v", err)
}
// Verify loaded keys match original
if loadedKeys.Address != keys.Address {
t.Errorf("Loaded address does not match original. Got %s, want %s",
loadedKeys.Address, keys.Address)
}
if loadedKeys.Both != keys.Both {
t.Errorf("Loaded keypair does not match original. Got %s, want %s",
loadedKeys.Both, keys.Both)
}
})
t.Run("StoreAndLoadIncompat", func(t *testing.T) {
var buf bytes.Buffer
// Store keys to buffer
err := StoreKeysIncompat(*keys, &buf)
if err != nil {
t.Fatalf("StoreKeysIncompat failed: %v", err)
}
// Create new reader from buffer content
reader := strings.NewReader(buf.String())
// Load keys from reader
loadedKeys, err := LoadKeysIncompat(reader)
if err != nil {
t.Fatalf("LoadKeysIncompat failed: %v", err)
}
// Verify loaded keys match original
if loadedKeys.Address != keys.Address {
t.Errorf("Loaded address does not match original. Got %s, want %s",
loadedKeys.Address, keys.Address)
}
if loadedKeys.Both != keys.Both {
t.Errorf("Loaded keypair does not match original. Got %s, want %s",
loadedKeys.Both, keys.Both)
}
})
/*t.Run("LoadNonexistentFile", func(t *testing.T) {
nonexistentPath := filepath.Join(os.TempDir(), "nonexistent_keys.txt")
_, err := LoadKeys(nonexistentPath)
if err != os.ErrNotExist {
t.Errorf("Expected ErrNotExist for nonexistent file, got: %v", err)
}
})*/
}
func Test_BasicInvalidAddress(t *testing.T) {
invalidAddr := strings.Repeat("x", 60)
invalidAddr += ".b32.i2p"
_, err := Lookup(invalidAddr)
if err == nil {
t.Fatal("Expected error for nonexistent address")
}
}

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
}

View File

@ -7,41 +7,60 @@ import (
)
func Lookup(addr string) (*I2PAddr, error) {
log.WithField("addr", addr).Debug("Starting Lookup")
conn, err := net.Dial("tcp", "127.0.0.1:7656")
if err != nil {
log.Error("Failed to connect to SAM bridge")
return nil, err
}
defer conn.Close()
_, err = conn.Write([]byte("HELLO VERSION MIN=3.1 MAX=3.1\n"))
if err != nil {
log.Error("Failed to write HELLO VERSION")
return nil, err
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
log.Error("Failed to read HELLO VERSION response")
return nil, err
}
if n < 1 {
log.Error("no data received")
return nil, fmt.Errorf("no data received")
}
response := string(buf[:n])
log.WithField("response", response).Debug("Received HELLO response")
if strings.Contains(string(buf[:n]), "RESULT=OK") {
_, err = conn.Write([]byte(fmt.Sprintf("NAMING LOOKUP NAME=%s\n", addr)))
if err != nil {
log.Error("Failed to write NAMING LOOKUP command")
return nil, err
}
n, err = conn.Read(buf)
if err != nil {
log.Error("Failed to read NAMING LOOKUP response")
return nil, err
}
if n < 1 {
return nil, fmt.Errorf("no destination data received")
}
value := strings.Split(string(buf[:n]), "VALUE=")[1]
parts := strings.Split(string(buf[:n]), "VALUE=")
if len(parts) < 2 {
log.Error("Could not find VALUE=, maybe we couldn't find the destination?")
return nil, fmt.Errorf("could not find VALUE=")
}
value := parts[1]
addr, err := NewI2PAddrFromString(value)
if err != nil {
log.Error("Failed to parse I2P address from lookup response")
return nil, err
}
log.WithField("addr", addr).Debug("Successfully resolved I2P address")
return &addr, err
}
log.Error("no RESULT=OK received in HELLO response")
return nil, fmt.Errorf("no result received")
}

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)
}

119
Makefile
View File

@ -1,13 +1,122 @@
build:
go build -a -tags netgo -ldflags '-w -extldflags "-static"'
USER_GH=go-i2p
VERSION=0.33.92
CREDIT='contributors to this release: @hkh4n, @eyedeekay'
packagename=i2pkeys
echo:
@echo "$(GOPATH)"
@echo "type make version to do release $(VERSION)"
version:
github-release release -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(packagename) -t v$(VERSION) -d "version $(VERSION) $(CREDIT)"
del:
github-release delete -s $(GITHUB_TOKEN) -u $(USER_GH) -r $(packagename) -t v$(VERSION)
tar:
tar --exclude .git \
--exclude .go \
--exclude bin \
-cJvf ../$(packagename)_$(VERSION).orig.tar.xz .
copier:
echo '#! /usr/bin/env sh' > deb/copy.sh
echo 'for f in $$(ls); do scp $$f/*.deb user@192.168.99.106:~/DEBIAN_PKGS/$$f/main/; done' >> deb/copy.sh
fmt:
find . -path ./.go -prune -o -name "*.go" -exec gofmt -w {} \;
find . -path ./.go -prune -o -name "*.i2pkeys" -exec rm {} \;
install:
install -m755 i2pkeys /usr/local/bin/i2pkeys
upload-linux:
github-release upload -R -u $(USER_GH) -r "$(packagename)" -t $(VERSION) -l `sha256sum ` -n "$(packagename)" -f "$(packagename)"
test-basic:
go test -v -run Test_Basic
test-basic-lookup:
go test -v -run Test_Basic_Lookup
test-newi2paddrfromstring:
go test -v -run Test_NewI2PAddrFromString
test-i2paddr:
go test -v -run Test_I2PAddr
test-desthashfromstring:
go test -v -run Test_DestHashFromString
test-i2paddr-to-bytes:
go test -v -run Test_I2PAddrToBytes
test-key-generation-and-handling:
go test -v -run Test_KeyGenerationAndHandling
# Subtest targets
test-newi2paddrfromstring-valid:
go test -v -run Test_NewI2PAddrFromString/Valid_base64_address
test-newi2paddrfromstring-invalid:
go test -v -run Test_NewI2PAddrFromString/Invalid_address
test-newi2paddrfromstring-base32:
go test -v -run Test_NewI2PAddrFromString/Base32_address
test-newi2paddrfromstring-empty:
go test -v -run Test_NewI2PAddrFromString/Empty_address
test-newi2paddrfromstring-i2p-suffix:
go test -v -run Test_NewI2PAddrFromString/Address_with_.i2p_suffix
test-i2paddr-base32-suffix:
go test -v -run Test_I2PAddr/Base32_suffix
test-i2paddr-base32-length:
go test -v -run Test_I2PAddr/Base32_length
test-desthashfromstring-valid:
go test -v -run Test_DestHashFromString/Valid_hash
test-desthashfromstring-invalid:
go test -v -run Test_DestHashFromString/Invalid_hash
test-desthashfromstring-empty:
go test -v -run Test_DestHashFromString/Empty_hash
test-i2paddr-to-bytes-roundtrip:
go test -v -run Test_I2PAddrToBytes/ToBytes_and_back
test-i2paddr-to-bytes-comparison:
go test -v -run Test_I2PAddrToBytes/Direct_decoding_comparison
test-key-generation-and-handling-loadkeys:
go test -v -run Test_KeyGenerationAndHandling/LoadKeysIncompat
test-key-generation-and-handling-storekeys-incompat:
go test -v -run Test_KeyGenerationAndHandling/StoreKeysIncompat
test-key-generation-and-handling-storekeys:
go test -v -run Test_KeyGenerationAndHandling/StoreKeys
test-key-storage:
go test -v -run Test_KeyStorageAndLoading
# Individual key storage subtests
test-key-storage-file:
go test -v -run Test_KeyStorageAndLoading/StoreAndLoadFile
test-key-storage-incompat:
go test -v -run Test_KeyStorageAndLoading/StoreAndLoadIncompat
test-key-storage-nonexistent:
go test -v -run Test_KeyStorageAndLoading/LoadNonexistentFile
test-basic-invalid-address:
go test -v -run Test_BasicInvalidAddress
# Aggregate targets
test-all:
go test -v ./...
test-subtests: test-newi2paddrfromstring-valid test-newi2paddrfromstring-invalid test-newi2paddrfromstring-base32 test-newi2paddrfromstring-empty test-newi2paddrfromstring-i2p-suffix test-i2paddr-base32-suffix test-i2paddr-base32-length test-desthashfromstring-valid test-desthashfromstring-invalid test-desthashfromstring-empty test-i2paddr-to-bytes-roundtrip test-i2paddr-to-bytes-comparison test-key-generation-and-handling-loadkeys test-key-generation-and-handling-storekeys-incompat test-key-generation-and-handling-storekeys test-key-storage-file test-key-storage-incompat test-key-storage-nonexistent
test: test-basic test-basic-lookup test-newi2paddrfromstring test-i2paddr test-desthashfromstring test-i2paddr-to-bytes test-key-generation-and-handling test-key-storage test-basic-invalid-address test-subtests test-all

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

@ -3,3 +3,25 @@ i2pkeys
Generates and displays the contents of files that are storing i2p keys in the
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 ##
Logging can be enabled and configured using the DEBUG_I2P environment variable. By default, logging is disabled.
There are three available log levels:
- Debug
```shell
export DEBUG_I2P=debug
```
- Warn
```shell
export DEBUG_I2P=warn
```
- Error
```shell
export DEBUG_I2P=error
```
If DEBUG_I2P is set to an unrecognized variable, it will fall back to "debug".

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)
}

11
go.mod
View File

@ -1,3 +1,10 @@
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 (
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // 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=

21
log.go Normal file
View File

@ -0,0 +1,21 @@
package i2pkeys
import (
"github.com/go-i2p/logger"
)
var log *logger.Logger
func InitializeI2PKeysLogger() {
logger.InitializeGoI2PLogger()
log = logger.GetGoI2PLogger()
}
// GetI2PKeysLogger returns the initialized logger
func GetI2PKeysLogger() *logger.Logger {
return logger.GetGoI2PLogger()
}
func init() {
InitializeI2PKeysLogger()
}