Files
i2pkeys/I2PKeys.go
2025-05-26 22:06:42 -04:00

202 lines
5.9 KiB
Go

package i2pkeys
import (
"bytes"
"crypto"
"encoding/base32"
"encoding/base64"
"errors"
"fmt"
"io"
"os"
"strings"
)
var (
i2pB64enc *base64.Encoding = base64.NewEncoding("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~")
i2pB32enc *base32.Encoding = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567")
)
// If you set this to true, Addr will return a base64 String()
var StringIsBase64 bool
// The public and private keys associated with an I2P destination. I2P hides the
// details of exactly what this is, so treat them as blobs, but generally: One
// pair of DSA keys, one pair of ElGamal keys, and sometimes (almost never) also
// a certificate. String() returns you the full content of I2PKeys and Addr()
// returns the public keys.
type I2PKeys struct {
Address I2PAddr // only the public key
Both string // both public and private keys
}
// Creates I2PKeys from an I2PAddr and a public/private keypair string (as
// generated by String().)
func NewKeys(addr I2PAddr, both string) I2PKeys {
log.WithField("addr", addr).Debug("Creating new I2PKeys")
return I2PKeys{addr, both}
}
// fileExists checks if a file exists and is not a directory before we
// try using it to prevent further errors.
func fileExists(filename string) (bool, error) {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
log.WithField("filename", filename).Debug("File does not exist")
return false, nil
} else if err != nil {
log.WithError(err).WithField("filename", filename).Error("Error checking file existence")
return false, fmt.Errorf("error checking file existence: %w", err)
}
exists := !info.IsDir()
if exists {
log.WithField("filename", filename).Debug("File exists")
} else {
log.WithField("filename", filename).Debug("File is a directory")
}
return !info.IsDir(), nil
}
// LoadKeysIncompat loads keys from a non-standard format
func LoadKeysIncompat(r io.Reader) (I2PKeys, error) {
log.Debug("Loading keys from reader")
var buff bytes.Buffer
_, err := io.Copy(&buff, r)
if err != nil {
log.WithError(err).Error("Error copying from reader, did not load keys")
return I2PKeys{}, fmt.Errorf("error copying from reader: %w", err)
}
parts := strings.Split(buff.String(), "\n")
if len(parts) < 2 {
err := errors.New("invalid key format: not enough data")
log.WithError(err).Error("Error parsing keys")
return I2PKeys{}, err
}
k := I2PKeys{I2PAddr(parts[0]), parts[1]}
log.WithField("keys", k).Debug("Loaded keys")
return k, nil
}
// load keys from non-standard format by specifying a text file.
// If the file does not exist, generate keys, otherwise, fail
// closed.
func LoadKeys(r string) (I2PKeys, error) {
log.WithField("filename", r).Debug("Loading keys from file")
exists, err := fileExists(r)
if err != nil {
log.WithError(err).Error("Error checking if file exists")
return I2PKeys{}, err
}
if !exists {
// File doesn't exist so we'll generate new keys
log.WithError(err).Debug("File does not exist, attempting to generate new keys")
k, err := NewDestination()
if err != nil {
log.WithError(err).Error("Error generating new keys")
return I2PKeys{}, err
}
// Save the new keys to the file
err = StoreKeys(*k, r)
if err != nil {
log.WithError(err).Error("Error saving new keys to file")
return I2PKeys{}, err
}
return *k, nil
}
fi, err := os.Open(r)
if err != nil {
log.WithError(err).WithField("filename", r).Error("Error opening file")
return I2PKeys{}, fmt.Errorf("error opening file: %w", err)
}
defer fi.Close()
log.WithField("filename", r).Debug("File opened successfully")
return LoadKeysIncompat(fi)
}
// store keys in non standard format
func StoreKeysIncompat(k I2PKeys, w io.Writer) error {
log.Debug("Storing keys")
_, err := io.WriteString(w, k.Address.Base64()+"\n"+k.Both)
if err != nil {
log.WithError(err).Error("Error writing keys")
return fmt.Errorf("error writing keys: %w", err)
}
log.WithField("keys", k).Debug("Keys stored successfully")
return nil
}
func StoreKeys(k I2PKeys, r string) error {
log.WithField("filename", r).Debug("Storing keys to file")
if _, err := os.Stat(r); err != nil {
if os.IsNotExist(err) {
log.WithField("filename", r).Debug("File does not exist, creating new file")
fi, err := os.Create(r)
if err != nil {
log.WithError(err).Error("Error creating file")
return err
}
defer fi.Close()
return StoreKeysIncompat(k, fi)
}
}
fi, err := os.Open(r)
if err != nil {
log.WithError(err).Error("Error opening file")
return err
}
defer fi.Close()
return StoreKeysIncompat(k, fi)
}
func (k I2PKeys) Network() string {
return k.Address.Network()
}
// Returns the public keys of the I2PKeys 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
}