Compare commits
86 Commits
Author | SHA1 | Date | |
---|---|---|---|
ca5c7feb9f | |||
fdb8745ee0 | |||
faf6b8e93e | |||
d844519847 | |||
25ef151100 | |||
f772cc42a3 | |||
de91aa824e | |||
565bc65808 | |||
d0d5f80a55 | |||
d166f5c31e | |||
9694fe011c | |||
8e42fd9a18 | |||
b47ca226eb | |||
ef2203a6c4 | |||
a9fd0e6202 | |||
8587d33d3a | |||
a8a977d576 | |||
d8a31854b9 | |||
40e34d7089 | |||
f82fb12470 | |||
50d395b12d | |||
11f71aa2c5 | |||
ce5a2a34aa | |||
a4d9cec9b8 | |||
b4e5b3ef61 | |||
4a2db938f7 | |||
e10de5e607 | |||
e4f5ccdff8 | |||
8669fb7db8 | |||
3e6b00e10d | |||
edace64685 | |||
1712957252 | |||
eb51810ddd | |||
c4cbd6e041 | |||
84d1ec1a12 | |||
43fb66564b | |||
2e82fab112 | |||
daa08faa71 | |||
a05ca99118 | |||
b99e77153e | |||
b56afeb346 | |||
890d71f974 | |||
bfcde005b3 | |||
a9a4310a04 | |||
4cba6e2edd | |||
25eedfeed8 | |||
8632c8275d | |||
9e61a8e00f | |||
fb2ca1a92c | |||
1bf0437e54 | |||
0af72cba75 | |||
a4a38460e3 | |||
75a7b2aec9 | |||
43a6a0e07f | |||
936c39746c | |||
8df0f31a4d | |||
c1b05d6ede | |||
0ef26b9207 | |||
8eee571a7b | |||
95744f9498 | |||
225e230a81 | |||
b4e9da89e9 | |||
afae8e6f14 | |||
6c95fc6ac7 | |||
ada0d39af4 | |||
59bffea3f3 | |||
316dc840d6 | |||
90da121025 | |||
d362997650 | |||
4004d3050d | |||
0d58ebfa78 | |||
acbf68bc58 | |||
dc8ef52d46 | |||
aae46b4dec | |||
597e1da68d | |||
4cf76aeec2 | |||
80bfc77145 | |||
5ae94bc639 | |||
c86c07c1df | |||
fde718e1d8 | |||
3a99966c42 | |||
d23cb52f2c | |||
81f9b8a8cc | |||
28d6bf4d97 | |||
9307ae9cf4 | |||
e6cb984e8f |
20
.github/workflows/auto-assign.yml
vendored
Normal file
20
.github/workflows/auto-assign.yml
vendored
Normal 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
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
log
|
||||
i2p-backup
|
||||
/tmp
|
||||
/*.txt
|
417
I2PAddr.go
417
I2PAddr.go
@ -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
|
||||
}
|
||||
|
323
I2PAddr_test.go
323
I2PAddr_test.go
@ -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
87
I2PDestHash.go
Normal 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
62
I2PKeyTypes.go
Normal 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
105
I2PKeys.go
Normal 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
|
||||
}
|
@ -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
73
I2PSecretKey.go
Normal 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
61
I2PSecretKeys_test.go
Normal 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
68
LoadKeys.go
Normal 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
119
Makefile
@ -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
173
NewI2PKeys.go
Normal 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
|
||||
}
|
22
README.md
22
README.md
@ -3,3 +3,25 @@ i2pkeys
|
||||
|
||||
Generates and displays the contents of files that are storing i2p keys in the
|
||||
incompatible format used for sam3
|
||||
|
||||
[](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
45
StoreKeys.go
Normal 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
11
go.mod
@ -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
17
go.sum
Normal 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
21
log.go
Normal 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()
|
||||
}
|
Reference in New Issue
Block a user