89 Commits

Author SHA1 Message Date
eyedeekay
ca5c7feb9f Reduce redundant logic in validateAddressFormat 2025-05-26 22:36:53 -04:00
eyedeekay
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
eyedeekay
faf6b8e93e fix validateAddressFormat 2025-05-26 22:28:54 -04:00
eyedeekay
d844519847 Merge branch 'master' of github.com:go-i2p/i2pkeys 2025-05-26 22:27:00 -04:00
eyedeekay
25ef151100 Improve validation of bytes when generating address from byte slice 2025-05-26 22:26:13 -04:00
eyedeekay
f772cc42a3 Improve validator function 2025-05-26 22:24:18 -04:00
eyedeekay
de91aa824e gitignore fixes 2025-05-26 22:11:47 -04:00
eyedeekay
565bc65808 move load and store to own files 2025-05-26 22:10:36 -04:00
eyedeekay
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
eyedeekay
9694fe011c Add ability to pass different flags to the DEST GENERATE function 2024-12-08 15:48:46 -05:00
eyedeekay
8e42fd9a18 add .gitignore 2024-11-29 19:06:08 -05:00
eyedeekay
b47ca226eb Start working on figuring out why the test is failing 2024-11-29 19:05:24 -05:00
eyedeekay
ef2203a6c4 Rename some files for consistency 2024-11-29 18:49:31 -05:00
eyedeekay
a9fd0e6202 Add still failing test 2024-11-29 18:48:12 -05:00
eyedeekay
8587d33d3a Work on making SecretKeys type-safe 2024-11-29 18:21:01 -05:00
eyedeekay
a8a977d576 Split out I2PSecretKey.go 2024-11-29 18:14:03 -05:00
eyedeekay
d8a31854b9 Separate out generate address function and make it's SAM port configurable 2024-11-29 18:01:00 -05:00
eyedeekay
40e34d7089 Simplify, simplify, simplify 2024-11-29 17:15:40 -05:00
eyedeekay
f82fb12470 Simplify, simplify, simplify 2024-11-29 17:13:10 -05:00
eyedeekay
50d395b12d Split I2PAddr and I2PDestHash into their own files 2024-11-29 14:20:55 -05:00
eyedeekay
11f71aa2c5 checkin go.sum 2024-11-21 18:51:15 -05:00
eyedeekay
ce5a2a34aa use external logger 2024-11-21 18:51:01 -05:00
eyedeekay
a4d9cec9b8 Update release process 2024-11-16 16:21:01 -05:00
eyedeekay
b4e5b3ef61 go report card 2024-11-14 10:37:32 -05:00
eyedeekay
4a2db938f7 go mod tidy 2024-11-14 10:36:36 -05:00
eyedeekay
e10de5e607 bump version 2024-11-13 14:34:22 -05:00
eyedeekay
e4f5ccdff8 Fix merge conflict 2024-11-08 15:03:32 -05:00
eyedeekay
8669fb7db8 setup auto-assign workflow 2024-11-08 15:01:13 -05:00
eyedeekay
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
Haris Khan
1712957252 add unit test 2024-11-07 11:39:04 -05:00
Haris Khan
eb51810ddd minor grammar tweaks 2024-11-07 11:16:05 -05:00
Haris Khan
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
Haris Khan
2e82fab112 update Makefile 2024-11-03 12:01:55 -05:00
Haris Khan
daa08faa71 Minor typo fix 2024-11-02 19:42:22 -04:00
Haris Khan
a05ca99118 Merge branch 'loadkeys' into refactor 2024-10-26 22:59:30 -04:00
Haris Khan
b99e77153e Make LoadKeys() create new keys if the keyfile doesn't exist 2024-10-26 22:59:06 -04:00
Haris Khan
b56afeb346 tweaks 2024-10-26 19:20:37 -04:00
Haris Khan
890d71f974 comments 2024-10-26 19:20:05 -04:00
Haris Khan
bfcde005b3 Refactored LoadKeysIncompat
-Previously: would log errors regardless at the end
-Better error handling
2024-10-26 19:18:08 -04:00
Haris Khan
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
Haris Khan
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
Haris Khan
9e61a8e00f Merge branch 'eyedeekay:master' into logging 2024-10-23 00:10:22 -04:00
Haris Khan
fb2ca1a92c fix logger naming collision with other libs 2024-10-23 00:09:36 -04:00
Haris Khan
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
Haris Khan
a4a38460e3 tweaks 2024-10-17 14:42:38 -04:00
Haris Khan
75a7b2aec9 Update README.md to reflect logging 2024-10-17 14:38:35 -04:00
Haris Khan
43a6a0e07f adjusted log.go 2024-10-16 14:59:01 -04:00
Haris Khan
936c39746c tweaks 2024-10-15 19:48:13 -04:00
Haris Khan
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
Haris Khan
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
Haris Khan
8eee571a7b -remove boilerplate artifact 2024-09-22 20:04:55 -04:00
Haris Khan
95744f9498 -bump version
-added testing from make
2024-09-22 19:52:02 -04:00
eyedeekay
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
Haris Khan
afae8e6f14 changed urls to fit with tests 2024-09-15 21:56:23 -04:00
Haris Khan
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
Haris Khan
ada0d39af4 Investigating LoadKeysIncompat (amend instead) 2024-09-15 15:17:33 -04:00
Haris Khan
59bffea3f3 Merge remote-tracking branch 'origin/tests-keys' into tests-keys
# Conflicts:
#	I2PAddr_test.go
2024-09-15 13:13:36 -04:00
Haris Khan
316dc840d6 Investigating LoadKeysIncompat (amend instead) 2024-09-15 13:08:11 -04:00
Haris Khan
90da121025 Investigating LoadKeysIncompat x4 2024-09-15 12:31:39 -04:00
Haris Khan
d362997650 Investigating LoadKeysIncompat x3 2024-09-15 11:28:47 -04:00
Haris Khan
4004d3050d Investigating LoadKeysIncompat x2 2024-09-15 11:16:46 -04:00
Haris Khan
0d58ebfa78 Investigating LoadKeysIncompat 2024-09-15 11:15:18 -04:00
Haris Khan
acbf68bc58 Added broken tests for keys (TODO) 2024-09-15 00:46:32 -04:00
Haris Khan
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
Haris Khan
aae46b4dec added TestKeyGenerationAndHandling 2024-09-14 21:31:52 -04:00
Haris Khan
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
Haris Khan
4cf76aeec2 Added various tests 2024-09-14 07:59:56 -04:00
Haris Khan
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
Haris Khan
c86c07c1df refactored error handling in HostnameEntry() 2024-09-09 20:20:43 -04:00
Haris Khan
fde718e1d8 refactored error handling in StoreKeysIncompat() 2024-09-09 20:17:21 -04:00
Haris Khan
3a99966c42 refactored error handling in LoadKeys() 2024-09-09 20:07:43 -04:00
Haris Khan
d23cb52f2c refactored error handling in fileExists() 2024-09-09 20:02:56 -04:00
eyedeekay
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
idk
18a714c195 fix missing LICENSE file 2022-09-11 19:55:14 -04:00
idk
1048b5ce6b update index.html 2022-08-04 18:07:22 -04:00
idk
910de44ac8 Make the StoreKeys function automatically set up the file if it's not already there 2022-08-03 16:46:30 -04:00
23 changed files with 1537 additions and 334 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

0
.nojekyll Normal file
View File

View File

@@ -1,361 +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 {
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")
}
})*/
}

24
LICENSE Normal file
View File

@@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

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=

BIN
i2plogo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

51
index.html Normal file
View File

@@ -0,0 +1,51 @@
<html>
<head>
<title>
i2pkeys
</title>
<meta name="author" content="eyedeekay" />
<meta name="description" content="i2pkeys" />
<meta name="keywords" content="master" />
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<div id="navbar">
<a href="#shownav">
Show navigation
</a>
<div id="shownav">
<div id="hidenav">
<ul>
<li>
<a href="index.html">
index
</a>
</li>
</ul>
<br>
<a href="#hidenav">
Hide Navigation
</a>
</div>
</div>
</div>
<h1>
<a href="/">
i2pkeys
</a>
</h1>
<p>
Generates and displays the contents of files that are storing i2p keys in the
incompatible format used for sam3
</p>
<div>
<iframe src="https://snowflake.torproject.org/embed.html" width="320" height="240" frameborder="0" scrolling="no"></iframe>
</div>
<div>
<a href="https://geti2p.net/">
<img src="i2plogo.png"></img>
I2P
</a>
</div>
</body>
</html>

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

157
style.css Normal file
View File

@@ -0,0 +1,157 @@
/* edgar default CSS file */
body {
font-family: "Roboto";
font-family: monospace;
text-align: justify;
background-color: #373636;
color: whitesmoke;
font-size: 1.15em;
}
ul {
width: 55%;
display: block;
}
ol {
width: 55%;
display: block;
}
li {
margin-top: 1%;
}
p {
max-width: 90%;
margin-top: 1%;
margin-left: 3%;
margin-right: 3%;
}
img {
float: left;
top: 5%;
left: 5%;
max-width: 60%;
display: inline;
}
.inline {
display: inline;
}
.link-button:focus {
outline: none;
}
.link-button:active {
color: red;
}
code {
font-family: monospace;
border-radius: 5%;
padding: 1%;
border-color: darkgray;
font-size: .9em;
}
a {
color: #C6D9FE;
padding: 1%;
}
ul li {
color: #C6D9FE;
}
iframe {
background: aliceblue;
border-radius: 15%;
margin: 2%;
}
.container {
width: 36vw;
height: 64vh;
display: inline-block;
margin: 0;
padding: 0;
}
.editor-toolbar a {
display: inline-block;
text-align: center;
text-decoration: none !important;
color: whitesmoke !important;
}
#feed {
width: 60vw;
height: unset !important;
margin: 0;
padding: 0;
float: right;
background-color: #373636;
color: whitesmoke;
border: #C6D9FE solid 1px;
}
.thread-post,
.thread {
color: whitesmoke !important;
background-color: #373636;
border: 1px solid darkgray;
font-size: inherit;
padding-top: 1%;
padding-bottom: 1%;
}
.thread-post {
margin-left: 4%;
}
input {
text-align: center;
color: whitesmoke !important;
background-color: #373636;
border: 1px solid darkgray;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
padding-top: 1%;
padding-bottom: 1%;
}
.thread-hash {
text-align: right;
color: whitesmoke !important;
background-color: #373636;
border: 1px solid darkgray;
font-size: inherit;
padding-top: 1%;
padding-bottom: 1%;
}
.post-body {
text-align: left;
color: whitesmoke !important;
font-size: inherit;
padding-top: 1%;
padding-bottom: 1%;
}
#show {display:none; }
#hide {display:block; }
#show:target {display: block; }
#hide:target {display: none; }
#shownav {display:none; }
#hidenav {display:block; }
#shownav:target {display: block; }
#hidenav:target {display: none; }
#navbar {
float: right;
width: 10%;
}