package saltyim import ( "bufio" "bytes" "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "github.com/keys-pub/keys" "go.mills.io/salty" ) func readUser(fd io.Reader) (*Addr, error) { scan := bufio.NewScanner(fd) for scan.Scan() { if strings.HasPrefix(scan.Text(), "# user:") { addr, err := ParseAddr(strings.TrimSpace(strings.TrimPrefix(scan.Text(), "# user:"))) if err == nil { return addr, nil } } } return nil, scan.Err() } // DefaultIdentity returns a default identity file (if one exists) otherwise // returns an empty string func DefaultIdentity() string { return os.ExpandEnv("$HOME/.config/salty/$USER.key") } // CreateIdentity ... func CreateIdentity(options ...IdentityOption) (*Identity, error) { ident := &Identity{} for _, option := range options { option(ident) } if ident.addr == nil || ident.addr.IsZero() { return nil, fmt.Errorf("unable to find user address in options") } buf := &bytes.Buffer{} key, _ := salty.GenerateKeys(buf) ident.key = key buf.Write([]byte(fmt.Sprintf("# user: %s\n", ident.addr.String()))) ident.contents = buf.Bytes() if ident.path == "" { return ident, nil } fn := ident.path fn = FixUnixHome(fn) f, err := os.OpenFile(fn, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) if err != nil { return ident, fmt.Errorf("error opening identity %q for writing: %w", fn, err) } defer f.Close() if _, err := f.Write(ident.contents); err != nil { return ident, fmt.Errorf("error writing identity %q: %w", fn, err) } if err := f.Sync(); err != nil { return ident, fmt.Errorf("error syncing identity %q for writing: %w", fn, err) } if err := f.Close(); err != nil { return ident, fmt.Errorf("error closing identity %q for writing: %w", fn, err) } return ident, nil } // GetIdentity ... func GetIdentity(options ...IdentityOption) (*Identity, error) { ident := &Identity{} for _, option := range options { option(ident) } if ident.key != nil && ident.addr != nil { return ident, nil } if ident.path != "" { fn := ident.path fn = FixUnixHome(fn) id, err := os.Open(fn) if err != nil { return ident, fmt.Errorf("error opening identity %q: %w", ident.Source(), err) } defer id.Close() identityBytes, err := ioutil.ReadAll(id) if err != nil { return ident, fmt.Errorf("error opening identity %q: %w", ident.Source(), err) } ident.contents = identityBytes } key, err := salty.ParseIdentity(bytes.NewBuffer(ident.contents)) if err != nil { return ident, fmt.Errorf("error reading identity %q: %w", ident.Source(), err) } ident.key = key me, err := readUser(bytes.NewBuffer(ident.contents)) ident.addr = me return ident, err } // GetOrCreateIdentity ... func GetOrCreateIdentity(options ...IdentityOption) (*Identity, error) { ident, err := GetIdentity(options...) if err != nil { ident, err = CreateIdentity(options...) } return ident, err } // Identity allows interaction with CreateIdentity, GetIdentity, and NewClient type Identity struct { // path where identity is read or written to path string // contents where identity is read from or stored contents []byte // key is the identity key key *keys.EdX25519Key // addr is the user / endpoint addr *Addr } func (i *Identity) Source() string { if i != nil && i.path != "" { return i.path } return "[]byte" } func (i *Identity) Contents() []byte { return i.contents } func (i *Identity) Key() *keys.EdX25519Key { return i.key } func (i *Identity) Addr() *Addr { return i.addr } // IdentityOption represents functional options for various identity operations type IdentityOption func(*Identity) // WithIdentityAddr sets the identity Addr option func WithIdentityAddr(addr *Addr) IdentityOption { return func(i *Identity) { i.addr = addr } } // WithIdentity indicates that an identity should be passed in func WithIdentity(ident *Identity) IdentityOption { return func(i *Identity) { i.key = ident.key i.addr = ident.addr } } // WithIdentityPath indicates that an identity should be read / written from a file path func WithIdentityPath(path string) IdentityOption { return func(i *Identity) { i.path = path } } // WithIdentityBytes indicates that the identity should be read from a byte array func WithIdentityBytes(contents []byte) IdentityOption { return func(i *Identity) { i.contents = contents } } // Handle unix home with `~` func FixUnixHome(p string) string { // Handle unix home with `~` if strings.HasPrefix(p, "~/") { dirname, _ := os.UserHomeDir() return filepath.Join(dirname, p[2:]) } return p }