aboutsummaryrefslogtreecommitdiffstats
path: root/hosts
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2025-04-11 15:49:30 -0400
committerSam Anthony <sam@samanthony.xyz>2025-04-11 15:49:30 -0400
commit711fa555be918c95d045b52daa973921b4a015d1 (patch)
tree236018526880d020d1f666d44c9695d55b07c3ac /hosts
parentc0611f71a8d005169a7f15b9ba21be41125cd58c (diff)
downloadhose-711fa555be918c95d045b52daa973921b4a015d1.zip
load/store known hosts file
Diffstat (limited to 'hosts')
-rw-r--r--hosts/hosts.go100
1 files changed, 100 insertions, 0 deletions
diff --git a/hosts/hosts.go b/hosts/hosts.go
new file mode 100644
index 0000000..d31ed5e
--- /dev/null
+++ b/hosts/hosts.go
@@ -0,0 +1,100 @@
+package hosts
+
+import (
+ "bufio"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "github.com/adrg/xdg"
+ "net"
+ "net/netip"
+ "os"
+ "path/filepath"
+ "strings"
+)
+
+var knownHostsFile = filepath.Join(xdg.DataHome, "hose", "known_hosts")
+
+// Set sets the public key of a remote host.
+// It replaces or creates an entry in the known hosts file.
+func Set(host net.Addr, pubkey [32]byte) error {
+ addr, err := netip.ParseAddr(host.String())
+ if err != nil {
+ return err
+ }
+
+ hosts, err := Load()
+ if err != nil {
+ return err
+ }
+
+ hosts[addr] = pubkey
+
+ return Store(hosts)
+}
+
+// Load loads the set of known hosts and their associated public keys
+// from disc.
+func Load() (map[netip.Addr][32]byte, error) {
+ hosts := make(map[netip.Addr][32]byte)
+
+ f, err := os.Open(knownHostsFile)
+ if errors.Is(err, os.ErrNotExist) {
+ return hosts, nil // no known hosts yet.
+ } else if err != nil {
+ return hosts, err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ for line := 1; scanner.Scan(); line++ {
+ host, pubkey, err := parseHostKeyPair(scanner.Text())
+ if err != nil {
+ return hosts, fmt.Errorf("error parsing known hosts file: %s:%d: %v", err)
+ }
+ if _, ok := hosts[host]; ok {
+ return hosts, fmt.Errorf("duplicate entry in known hosts file: %s", host)
+ }
+ hosts[host] = pubkey
+ }
+ return hosts, scanner.Err()
+}
+
+// parseHostKeyPair parses a line of the known hosts file.
+func parseHostKeyPair(s string) (netip.Addr, [32]byte, error) {
+ fields := strings.Fields(s)
+ if len(fields) != 2 {
+ return netip.Addr{}, [32]byte{}, fmt.Errorf("expected 2 fields; got %d", len(fields))
+ }
+
+ addr, err := netip.ParseAddr(fields[0])
+ if err != nil {
+ return netip.Addr{}, [32]byte{}, err
+ }
+
+ var key [32]byte
+ if hex.DecodedLen(len(fields[1])) != len(key) {
+ return netip.Addr{}, [32]byte{}, fmt.Errorf("malformed key: %s", fields[1])
+ }
+ if _, err := hex.Decode(key[:], []byte(fields[1])); err != nil {
+ return netip.Addr{}, [32]byte{}, err
+ }
+
+ return addr, key, nil
+}
+
+// Store stores the set of known hosts and their associated public keys
+// to disc. It overwrites the entire file.
+func Store(hosts map[netip.Addr][32]byte) error {
+ f, err := os.Create(knownHostsFile)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ for host, key := range hosts {
+ fmt.Fprintf(f, "%s %x", host, key)
+ }
+
+ return nil
+}