github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/usermd/hooks.go (about) 1 // Copyright (c) 2020-2021 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package usermd 6 7 import ( 8 "encoding/hex" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io" 13 "strconv" 14 "strings" 15 16 backend "github.com/decred/politeia/politeiad/backendv2" 17 "github.com/decred/politeia/politeiad/backendv2/tstorebe/plugins" 18 "github.com/decred/politeia/politeiad/plugins/usermd" 19 "github.com/decred/politeia/util" 20 "github.com/google/uuid" 21 ) 22 23 // hookNewRecordPre adds plugin specific validation onto the tstore backend 24 // RecordNew method. 25 func (p *usermdPlugin) hookNewRecordPre(payload string) error { 26 var nr plugins.HookNewRecordPre 27 err := json.Unmarshal([]byte(payload), &nr) 28 if err != nil { 29 return err 30 } 31 32 return userMetadataVerify(nr.Metadata, nr.Files) 33 } 34 35 // hookNewRecordPre caches plugin data from the tstore backend RecordNew 36 // method. 37 func (p *usermdPlugin) hookNewRecordPost(payload string) error { 38 var nr plugins.HookNewRecordPost 39 err := json.Unmarshal([]byte(payload), &nr) 40 if err != nil { 41 return err 42 } 43 44 // Decode user metadata 45 um, err := userMetadataDecode(nr.Metadata) 46 if err != nil { 47 return err 48 } 49 50 // Add token to the user cache 51 err = p.userCacheAddToken(um.UserID, nr.RecordMetadata.State, 52 nr.RecordMetadata.Token) 53 if err != nil { 54 return err 55 } 56 57 return nil 58 } 59 60 // hookEditRecordPre adds plugin specific validation onto the tstore backend 61 // RecordEdit method. 62 func (p *usermdPlugin) hookEditRecordPre(payload string) error { 63 var er plugins.HookEditRecord 64 err := json.Unmarshal([]byte(payload), &er) 65 if err != nil { 66 return err 67 } 68 69 // Verify user metadata 70 err = userMetadataVerify(er.Metadata, er.Files) 71 if err != nil { 72 return err 73 } 74 75 // Verify user ID has not changed 76 um, err := userMetadataDecode(er.Metadata) 77 if err != nil { 78 return err 79 } 80 umCurr, err := userMetadataDecode(er.Record.Metadata) 81 if err != nil { 82 return err 83 } 84 if um.UserID != umCurr.UserID { 85 return backend.PluginError{ 86 PluginID: usermd.PluginID, 87 ErrorCode: uint32(usermd.ErrorCodeUserIDInvalid), 88 ErrorContext: fmt.Sprintf("user id cannot change: got %v, want %v", 89 um.UserID, umCurr.UserID), 90 } 91 } 92 93 return nil 94 } 95 96 // hookEditRecordPre adds plugin specific validation onto the tstore backend 97 // RecordEdit method. 98 func (p *usermdPlugin) hookEditMetadataPre(payload string) error { 99 var em plugins.HookEditMetadata 100 err := json.Unmarshal([]byte(payload), &em) 101 if err != nil { 102 return err 103 } 104 105 // User metadata should not change on metadata updates 106 return userMetadataPreventUpdates(em.Record.Metadata, em.Metadata) 107 } 108 109 // hookSetStatusRecordPre adds plugin specific validation onto the tstore 110 // backend RecordSetStatus method. 111 func (p *usermdPlugin) hookSetRecordStatusPre(payload string) error { 112 var srs plugins.HookSetRecordStatus 113 err := json.Unmarshal([]byte(payload), &srs) 114 if err != nil { 115 return err 116 } 117 118 // User metadata should not change on status changes 119 err = userMetadataPreventUpdates(srs.Record.Metadata, srs.Metadata) 120 if err != nil { 121 return err 122 } 123 124 // Verify status change metadata 125 err = statusChangeMetadataVerify(srs.RecordMetadata, srs.Metadata) 126 if err != nil { 127 return err 128 } 129 130 return nil 131 } 132 133 // hookNewRecordPre caches plugin data from the tstore backend RecordSetStatus 134 // method. 135 func (p *usermdPlugin) hookSetRecordStatusPost(payload string) error { 136 var srs plugins.HookSetRecordStatus 137 err := json.Unmarshal([]byte(payload), &srs) 138 if err != nil { 139 return err 140 } 141 rm := srs.RecordMetadata 142 143 // When a record is made public the token must be moved from the 144 // unvetted list to the vetted list in the user cache. 145 if rm.Status == backend.StatusPublic { 146 um, err := userMetadataDecode(srs.Metadata) 147 if err != nil { 148 return err 149 } 150 err = p.userCacheMoveTokenToVetted(um.UserID, rm.Token) 151 if err != nil { 152 return err 153 } 154 } 155 156 return nil 157 } 158 159 // userMetadataDecode decodes and returns the UserMetadata from the provided 160 // backend metadata streams. If a UserMetadata is not found, nil is returned. 161 func userMetadataDecode(metadata []backend.MetadataStream) (*usermd.UserMetadata, error) { 162 var userMD *usermd.UserMetadata 163 for _, v := range metadata { 164 if v.PluginID != usermd.PluginID || 165 v.StreamID != usermd.StreamIDUserMetadata { 166 // Not the mdstream we're looking for 167 continue 168 } 169 var um usermd.UserMetadata 170 err := json.Unmarshal([]byte(v.Payload), &um) 171 if err != nil { 172 return nil, err 173 } 174 userMD = &um 175 break 176 } 177 return userMD, nil 178 } 179 180 // userMetadataVerify parses a UserMetadata from the metadata streams and 181 // verifies its contents are valid. 182 func userMetadataVerify(metadata []backend.MetadataStream, files []backend.File) error { 183 // Decode user metadata 184 um, err := userMetadataDecode(metadata) 185 if err != nil { 186 return err 187 } 188 if um == nil { 189 return backend.PluginError{ 190 PluginID: usermd.PluginID, 191 ErrorCode: uint32(usermd.ErrorCodeUserMetadataNotFound), 192 } 193 } 194 195 // Verify user ID 196 _, err = uuid.Parse(um.UserID) 197 if err != nil { 198 return backend.PluginError{ 199 PluginID: usermd.PluginID, 200 ErrorCode: uint32(usermd.ErrorCodeUserIDInvalid), 201 } 202 } 203 204 // Verify signature 205 digests := make([]string, 0, len(files)) 206 for _, v := range files { 207 digests = append(digests, v.Digest) 208 } 209 m, err := util.MerkleRoot(digests) 210 if err != nil { 211 return err 212 } 213 mr := hex.EncodeToString(m[:]) 214 err = util.VerifySignature(um.Signature, um.PublicKey, mr) 215 if err != nil { 216 return convertSignatureError(err) 217 } 218 219 return nil 220 } 221 222 // userMetadataPreventUpdates errors if the UserMetadata is being updated. 223 func userMetadataPreventUpdates(current, update []backend.MetadataStream) error { 224 // Decode user metadata 225 c, err := userMetadataDecode(current) 226 if err != nil { 227 return err 228 } 229 u, err := userMetadataDecode(update) 230 if err != nil { 231 return err 232 } 233 234 // Verify user metadata has not changed 235 switch { 236 case u.UserID != c.UserID: 237 return backend.PluginError{ 238 PluginID: usermd.PluginID, 239 ErrorCode: uint32(usermd.ErrorCodeUserIDInvalid), 240 ErrorContext: fmt.Sprintf("user id cannot change: got %v, want %v", 241 u.UserID, c.UserID), 242 } 243 244 case u.PublicKey != c.PublicKey: 245 return backend.PluginError{ 246 PluginID: usermd.PluginID, 247 ErrorCode: uint32(usermd.ErrorCodePublicKeyInvalid), 248 ErrorContext: fmt.Sprintf("public key cannot change: got %v, want %v", 249 u.PublicKey, c.PublicKey), 250 } 251 252 case c.Signature != c.Signature: 253 return backend.PluginError{ 254 PluginID: usermd.PluginID, 255 ErrorCode: uint32(usermd.ErrorCodeSignatureInvalid), 256 ErrorContext: fmt.Sprintf("signature cannot change: got %v, want %v", 257 u.Signature, c.Signature), 258 } 259 } 260 261 return nil 262 } 263 264 // statusChangesDecode decodes and returns the StatusChangeMetadata from the 265 // metadata streams if one is present. 266 func statusChangesDecode(metadata []backend.MetadataStream) ([]usermd.StatusChangeMetadata, error) { 267 statuses := make([]usermd.StatusChangeMetadata, 0, 16) 268 for _, v := range metadata { 269 if v.PluginID != usermd.PluginID || 270 v.StreamID != usermd.StreamIDStatusChanges { 271 // Not the mdstream we're looking for 272 continue 273 } 274 d := json.NewDecoder(strings.NewReader(v.Payload)) 275 for { 276 var sc usermd.StatusChangeMetadata 277 err := d.Decode(&sc) 278 if errors.Is(err, io.EOF) { 279 break 280 } else if err != nil { 281 return nil, err 282 } 283 statuses = append(statuses, sc) 284 } 285 break 286 } 287 return statuses, nil 288 } 289 290 var ( 291 // statusReasonRequired contains the list of record statuses that 292 // require an accompanying reason to be given in the status change. 293 statusReasonRequired = map[backend.StatusT]struct{}{ 294 backend.StatusCensored: {}, 295 backend.StatusArchived: {}, 296 } 297 ) 298 299 // statusChangeMetadataVerify parses the status change metadata from the 300 // metadata streams and verifies that its contents are valid. 301 func statusChangeMetadataVerify(rm backend.RecordMetadata, metadata []backend.MetadataStream) error { 302 // Decode status change metadata 303 statusChanges, err := statusChangesDecode(metadata) 304 if err != nil { 305 return err 306 } 307 308 // Verify that status change metadata is present 309 if len(statusChanges) == 0 { 310 return backend.PluginError{ 311 PluginID: usermd.PluginID, 312 ErrorCode: uint32(usermd.ErrorCodeStatusChangeMetadataNotFound), 313 } 314 } 315 scm := statusChanges[len(statusChanges)-1] 316 317 // Verify token matches 318 if scm.Token != rm.Token { 319 return backend.PluginError{ 320 PluginID: usermd.PluginID, 321 ErrorCode: uint32(usermd.ErrorCodeTokenInvalid), 322 ErrorContext: fmt.Sprintf("status change token does not match "+ 323 "record metadata token: got %v, want %v", scm.Token, rm.Token), 324 } 325 } 326 327 // Verify status matches 328 if scm.Status != uint32(rm.Status) { 329 return backend.PluginError{ 330 PluginID: usermd.PluginID, 331 ErrorCode: uint32(usermd.ErrorCodeStatusInvalid), 332 ErrorContext: fmt.Sprintf("status from metadata does not "+ 333 "match status from record metadata: got %v, want %v", 334 scm.Status, rm.Status), 335 } 336 } 337 338 // Verify reason was included on required status changes 339 _, ok := statusReasonRequired[rm.Status] 340 if ok && scm.Reason == "" { 341 return backend.PluginError{ 342 PluginID: usermd.PluginID, 343 ErrorCode: uint32(usermd.ErrorCodeReasonMissing), 344 ErrorContext: "a reason must be given for this status change", 345 } 346 } 347 348 // Verify signature 349 status := strconv.FormatUint(uint64(scm.Status), 10) 350 version := strconv.FormatUint(uint64(scm.Version), 10) 351 msg := scm.Token + version + status + scm.Reason 352 err = util.VerifySignature(scm.Signature, scm.PublicKey, msg) 353 if err != nil { 354 return convertSignatureError(err) 355 } 356 357 return nil 358 } 359 360 func convertSignatureError(err error) backend.PluginError { 361 var e util.SignatureError 362 var s usermd.ErrorCodeT 363 if errors.As(err, &e) { 364 switch e.ErrorCode { 365 case util.ErrorStatusPublicKeyInvalid: 366 s = usermd.ErrorCodePublicKeyInvalid 367 case util.ErrorStatusSignatureInvalid: 368 s = usermd.ErrorCodeSignatureInvalid 369 } 370 } 371 return backend.PluginError{ 372 PluginID: usermd.PluginID, 373 ErrorCode: uint32(s), 374 ErrorContext: e.ErrorContext, 375 } 376 }