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  }