2021-10-08 12:39:31 +00:00
|
|
|
// SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021
|
|
|
|
//
|
2021-01-25 10:39:05 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
// Package keys could be it's own thing between go-ssb and this but not today
|
|
|
|
package keys
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
2022-11-07 09:18:13 +00:00
|
|
|
"github.com/ssbc/go-secretstream/secrethandshake"
|
|
|
|
refs "github.com/ssbc/go-ssb-refs"
|
2021-05-21 07:27:12 +00:00
|
|
|
"go.cryptoscope.co/nocomment"
|
2021-01-25 10:39:05 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var SecretPerms = os.FileMode(0600)
|
|
|
|
|
2021-01-25 12:23:03 +00:00
|
|
|
// KeyPair contains a seret handshake keypair and the assosicated feed
|
2021-01-25 10:39:05 +00:00
|
|
|
type KeyPair struct {
|
2021-01-25 15:35:22 +00:00
|
|
|
Feed refs.FeedRef
|
2021-01-25 10:39:05 +00:00
|
|
|
Pair secrethandshake.EdKeyPair
|
|
|
|
}
|
|
|
|
|
|
|
|
// the format of the .ssb/secret file as defined by the js implementations
|
|
|
|
type ssbSecret struct {
|
2021-01-25 15:35:22 +00:00
|
|
|
Curve string `json:"curve"`
|
|
|
|
ID refs.FeedRef `json:"id"`
|
|
|
|
Private string `json:"private"`
|
|
|
|
Public string `json:"public"`
|
2021-01-25 10:39:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// IsValidFeedFormat checks if the passed FeedRef is for one of the two supported formats,
|
|
|
|
// legacy/crapp or GabbyGrove.
|
2021-01-25 15:35:22 +00:00
|
|
|
func IsValidFeedFormat(r refs.FeedRef) error {
|
2022-11-07 09:18:13 +00:00
|
|
|
if r.Algo() != refs.RefAlgoFeedSSB1 && r.Algo() != refs.RefAlgoFeedGabby {
|
|
|
|
return fmt.Errorf("ssb: unsupported feed format:%s", r.Algo())
|
2021-01-25 10:39:05 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewKeyPair generates a fresh KeyPair using the passed io.Reader as a seed.
|
|
|
|
// Passing nil is fine and will use crypto/rand.
|
|
|
|
func NewKeyPair(r io.Reader) (*KeyPair, error) {
|
|
|
|
// generate new keypair
|
|
|
|
kp, err := secrethandshake.GenEdKeyPair(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("ssb: error building key pair: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-11-07 09:18:13 +00:00
|
|
|
feed, err := refs.NewFeedRefFromBytes(kp.Public[:], refs.RefAlgoFeedSSB1)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("ssb: error building key pair: %w", err)
|
|
|
|
}
|
|
|
|
|
2021-01-25 10:39:05 +00:00
|
|
|
keyPair := KeyPair{
|
2022-11-07 09:18:13 +00:00
|
|
|
Feed: feed,
|
2021-01-25 10:39:05 +00:00
|
|
|
Pair: *kp,
|
|
|
|
}
|
|
|
|
|
|
|
|
return &keyPair, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// SaveKeyPair serializes the passed KeyPair to path.
|
|
|
|
// It errors if path already exists.
|
2021-01-25 15:35:22 +00:00
|
|
|
func SaveKeyPair(kp KeyPair, path string) error {
|
2021-01-25 12:23:03 +00:00
|
|
|
if err := IsValidFeedFormat(kp.Feed); err != nil {
|
2021-01-25 10:39:05 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
|
|
return fmt.Errorf("ssb.SaveKeyPair: key already exists:%q", path)
|
|
|
|
}
|
|
|
|
err := os.MkdirAll(filepath.Dir(path), 0700)
|
|
|
|
if err != nil && !os.IsExist(err) {
|
|
|
|
return fmt.Errorf("failed to create folder for keypair: %w", err)
|
|
|
|
}
|
|
|
|
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, SecretPerms)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("ssb.SaveKeyPair: failed to create file: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := EncodeKeyPairAsJSON(kp, f); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := f.Close(); err != nil {
|
|
|
|
return fmt.Errorf("ssb.SaveKeyPair: failed to close file: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// EncodeKeyPairAsJSON serializes the passed Keypair into the writer w
|
2021-01-25 15:35:22 +00:00
|
|
|
func EncodeKeyPairAsJSON(kp KeyPair, w io.Writer) error {
|
2021-01-25 10:39:05 +00:00
|
|
|
var sec = ssbSecret{
|
|
|
|
Curve: "ed25519",
|
2021-01-25 12:23:03 +00:00
|
|
|
ID: kp.Feed,
|
2021-01-25 10:39:05 +00:00
|
|
|
Private: base64.StdEncoding.EncodeToString(kp.Pair.Secret[:]) + ".ed25519",
|
|
|
|
Public: base64.StdEncoding.EncodeToString(kp.Pair.Public[:]) + ".ed25519",
|
|
|
|
}
|
|
|
|
err := json.NewEncoder(w).Encode(sec)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("ssb.EncodeKeyPairAsJSON: encoding failed: %w", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// LoadKeyPair opens fname, ignores any line starting with # and passes it ParseKeyPair
|
|
|
|
func LoadKeyPair(fname string) (*KeyPair, error) {
|
|
|
|
f, err := os.Open(fname)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("ssb.LoadKeyPair: could not open key file %s: %w", fname, err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
info, err := f.Stat()
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("ssb.LoadKeyPair: could not stat key file %s: %w", fname, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if perms := info.Mode().Perm(); perms != SecretPerms {
|
|
|
|
return nil, fmt.Errorf("ssb.LoadKeyPair: expected key file permissions %s, but got %s", SecretPerms, perms)
|
|
|
|
}
|
|
|
|
|
|
|
|
return ParseKeyPair(nocomment.NewReader(f))
|
|
|
|
}
|
|
|
|
|
|
|
|
// ParseKeyPair json decodes an object from the reader.
|
|
|
|
// It expects std base64 encoded data under the `private` and `public` fields.
|
|
|
|
func ParseKeyPair(r io.Reader) (*KeyPair, error) {
|
|
|
|
var s ssbSecret
|
|
|
|
if err := json.NewDecoder(r).Decode(&s); err != nil {
|
|
|
|
return nil, fmt.Errorf("ssb.Parse: JSON decoding failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := IsValidFeedFormat(s.ID); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
public, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(s.Public, ".ed25519"))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("ssb.Parse: base64 decode of public part failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
private, err := base64.StdEncoding.DecodeString(strings.TrimSuffix(s.Private, ".ed25519"))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("ssb.Parse: base64 decode of private part failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
pair, err := secrethandshake.NewKeyPair(public, private)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("ssb.Parse: base64 decode of private part failed: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ssbkp := KeyPair{
|
2021-01-25 12:23:03 +00:00
|
|
|
Feed: s.ID,
|
2021-01-25 10:39:05 +00:00
|
|
|
Pair: *pair,
|
|
|
|
}
|
|
|
|
return &ssbkp, nil
|
|
|
|
}
|