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 }