code.pfad.fr/gohmekit@v0.2.1/storage/jsonfile.go (about)

     1  package storage
     2  
     3  import (
     4  	"crypto/ed25519"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io/fs"
     9  	"log"
    10  	"os"
    11  	"path/filepath"
    12  	"sync"
    13  
    14  	"code.pfad.fr/gohmekit/discovery"
    15  	"code.pfad.fr/gohmekit/pairing"
    16  )
    17  
    18  type jsonData struct {
    19  	// for Database
    20  	LongTermPublicKeys map[string][]byte
    21  
    22  	// for AccessoryDevice
    23  	PairingID         string
    24  	Pin               string
    25  	Ed25519PrivateKey []byte
    26  	// for c# field
    27  	Version uint16
    28  }
    29  
    30  func loadJsonData(path string) (*jsonData, error) {
    31  	f, err := os.Open(path)
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  	defer f.Close()
    36  	var data jsonData
    37  	if err := json.NewDecoder(f).Decode(&data); err != nil {
    38  		return nil, err
    39  	}
    40  	return &data, nil
    41  }
    42  
    43  func (j jsonData) NewAccessoryDevice() (pairing.AccessoryDevice, error) {
    44  	return pairing.NewDeviceWithPin([]byte(j.PairingID), j.Pin, j.Ed25519PrivateKey)
    45  }
    46  
    47  // Option is to set some default values on first run.
    48  type Option func(*jsonData)
    49  
    50  // WithPairingID will set the pairing ID if it wasn't previously set.
    51  func WithPairingID(id []byte) Option {
    52  	return func(j *jsonData) {
    53  		if len(j.PairingID) > 0 {
    54  			return
    55  		}
    56  		j.PairingID = string(id)
    57  	}
    58  }
    59  
    60  // WithPin will set the pin if it wasn't previously set.
    61  func WithPin(pin string) Option {
    62  	return func(j *jsonData) {
    63  		if j.Pin != "" {
    64  			return
    65  		}
    66  		j.Pin = pin
    67  	}
    68  }
    69  
    70  // WithEd25519PrivateKey will set the private key if it wasn't previously set.
    71  func WithEd25519PrivateKey(key []byte) Option {
    72  	return func(j *jsonData) {
    73  		if len(j.Ed25519PrivateKey) > 0 {
    74  			return
    75  		}
    76  		j.Ed25519PrivateKey = key
    77  	}
    78  }
    79  
    80  // NewJSONFile will use (and create if missing) a JSON file to act as a storage.
    81  // It will use the given options, only if the concerned parameters are not already set.
    82  // It will generate random pin and private key if let unspecified.
    83  func NewJSONFile(path string, options ...Option) (*JSONFile, error) {
    84  	data, err := loadJsonData(path)
    85  	if err != nil {
    86  		if !errors.Is(err, fs.ErrNotExist) {
    87  			return nil, err
    88  		}
    89  
    90  		err = os.MkdirAll(filepath.Dir(path), 0700)
    91  		if err != nil {
    92  			return nil, fmt.Errorf("could not mkdir: %w", err)
    93  		}
    94  		data = &jsonData{}
    95  	}
    96  
    97  	for _, o := range options {
    98  		o(data)
    99  	}
   100  
   101  	if len(data.PairingID) == 0 {
   102  		data.PairingID = string(pairing.NewRandomPairingID())
   103  	}
   104  	if data.Pin == "" {
   105  		data.Pin = pairing.NewRandomPin()
   106  	}
   107  	if len(data.LongTermPublicKeys) == 0 {
   108  		data.LongTermPublicKeys = make(map[string][]byte)
   109  		log.Println("generated pin:", data.Pin)
   110  	}
   111  	if len(data.Ed25519PrivateKey) == 0 {
   112  		_, data.Ed25519PrivateKey, err = ed25519.GenerateKey(nil)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  	}
   117  
   118  	// increment version on every "boot"
   119  	data.Version++
   120  	if data.Version == 0 {
   121  		data.Version = 1
   122  	}
   123  
   124  	j := &JSONFile{
   125  		Path: path,
   126  		data: data,
   127  	}
   128  	if err = j.overwriteLocked(); err != nil {
   129  		return nil, err
   130  	}
   131  	j.AccessoryDevice, err = j.data.NewAccessoryDevice()
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	return j, nil
   136  }
   137  
   138  // JSONFile implements pairing.Database and pairing.AccessoryDevice, storing their settings in a JSON file.
   139  type JSONFile struct {
   140  	Path string
   141  	pairing.AccessoryDevice
   142  
   143  	mu              sync.RWMutex
   144  	data            *jsonData
   145  	isPairedWatcher func(bool)
   146  	// versionWatcher  func(uint16)
   147  }
   148  
   149  // the caller must held a Write lock.
   150  func (j *JSONFile) overwriteLocked() error {
   151  	f, err := os.Create(j.Path)
   152  	if err != nil {
   153  		return err
   154  	}
   155  	defer f.Close()
   156  	if err := json.NewEncoder(f).Encode(j.data); err != nil {
   157  		return err
   158  	}
   159  	return f.Close()
   160  }
   161  
   162  // /////////////////////////////
   163  // pairing.Database interface //
   164  // /////////////////////////////
   165  
   166  var _ pairing.Database = &JSONFile{}
   167  
   168  // IsPaired is defined by the pairing.Database interface.
   169  func (j *JSONFile) IsPaired() bool {
   170  	j.mu.RLock()
   171  	defer j.mu.RUnlock()
   172  
   173  	return len(j.data.LongTermPublicKeys) > 0
   174  }
   175  
   176  // IsPairedWatcher will trigger the callback on pairing change.
   177  // It will overwrite any existing callback.
   178  func (j *JSONFile) IsPairedWatcher(cb func(bool)) bool {
   179  	j.mu.Lock()
   180  	j.isPairedWatcher = cb
   181  	j.mu.Unlock()
   182  
   183  	return j.IsPaired()
   184  }
   185  
   186  // GetLongTermPublicKey is defined by the pairing.Database interface.
   187  func (j *JSONFile) GetLongTermPublicKey(id []byte) ([]byte, error) {
   188  	j.mu.RLock()
   189  	defer j.mu.RUnlock()
   190  
   191  	if key, ok := j.data.LongTermPublicKeys[string(id)]; ok {
   192  		return key, nil
   193  	}
   194  	return nil, errors.New("unknown key")
   195  }
   196  
   197  // AddLongTermPublicKey is defined by the pairing.Database interface.
   198  func (j *JSONFile) AddLongTermPublicKey(c pairing.Controller) error {
   199  	j.mu.Lock()
   200  	defer j.mu.Unlock()
   201  
   202  	wasPaired := len(j.data.LongTermPublicKeys) > 0
   203  	j.data.LongTermPublicKeys[string(c.PairingID)] = c.LongTermPublicKey
   204  	isPaired := len(j.data.LongTermPublicKeys) > 0
   205  	if isPaired != wasPaired && j.isPairedWatcher != nil {
   206  		go j.isPairedWatcher(isPaired)
   207  	}
   208  
   209  	return j.overwriteLocked()
   210  }
   211  
   212  // RemoveLongTermPublicKey is defined by the pairing.Database interface.
   213  func (j *JSONFile) RemoveLongTermPublicKey(id []byte) error {
   214  	j.mu.Lock()
   215  	defer j.mu.Unlock()
   216  	delete(j.data.LongTermPublicKeys, string(id))
   217  	return j.overwriteLocked()
   218  }
   219  
   220  // ListLongTermPublicKey is defined by the pairing.Database interface.
   221  func (j *JSONFile) ListLongTermPublicKey() ([]pairing.Controller, error) {
   222  	j.mu.RLock()
   223  	defer j.mu.RUnlock()
   224  
   225  	c := make([]pairing.Controller, 0, len(j.data.LongTermPublicKeys))
   226  	for id, key := range j.data.LongTermPublicKeys {
   227  		c = append(c, pairing.Controller{
   228  			PairingID:         []byte(id),
   229  			LongTermPublicKey: key,
   230  		})
   231  	}
   232  	return c, nil
   233  }
   234  
   235  // ////////////////////////////////////
   236  // pairing.AccessoryDevice interface //
   237  // ////////////////////////////////////
   238  
   239  var _ pairing.AccessoryDevice = &JSONFile{}
   240  
   241  // VersionWatcher will never trigger the callback, since the version is changed
   242  // on every restart.
   243  func (j *JSONFile) VersionWatcher(_ func(uint16)) uint16 {
   244  	// support updates?
   245  	// j.mu.Lock()
   246  	// j.versionWatcher = cb
   247  	// j.mu.Unlock()
   248  
   249  	j.mu.RLock()
   250  	defer j.mu.RUnlock()
   251  	return j.data.Version
   252  }
   253  
   254  // ///////////////////
   255  // discovery helper //
   256  // ///////////////////
   257  
   258  // DiscoveryService returns a discovery service, ready to be announced.
   259  func (j *JSONFile) DiscoveryService(serviceName string, port int, category discovery.Category) discovery.Service {
   260  	return discovery.Service{
   261  		Name:            serviceName,
   262  		ModelName:       serviceName,
   263  		Port:            port,
   264  		DeviceID:        string(j.PairingID()),
   265  		VersionWatcher:  j.VersionWatcher,
   266  		IsPairedWatcher: j.IsPairedWatcher,
   267  		Category:        category,
   268  	}
   269  }