go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/pagination/dscursor/vault.go (about)

     1  // Copyright 2021 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package dscursor
    16  
    17  import (
    18  	"context"
    19  
    20  	"go.chromium.org/luci/common/pagination"
    21  	"go.chromium.org/luci/gae/service/datastore"
    22  	"go.chromium.org/luci/server/secrets"
    23  )
    24  
    25  // Vault is a utility type that can convert datastore.Cursor to/from an
    26  // encrypted, URL safe page token. It uses AEAD to ensure that
    27  // 1. potential sensitive information contained in the cursor is not leaked.
    28  // 2. user can not use a page token that is not generated by the server.
    29  type Vault struct {
    30  	additionalData []byte
    31  }
    32  
    33  // PageToken converts a datastore.Cursor to an encrypted, URL safe page token.
    34  func (v *Vault) PageToken(ctx context.Context, cursor datastore.Cursor) (string, error) {
    35  	if cursor == nil {
    36  		return "", nil
    37  	}
    38  
    39  	return secrets.URLSafeEncrypt(ctx, []byte(cursor.String()), v.additionalData)
    40  }
    41  
    42  // Cursor converts a page token to a datastore.Cursor.
    43  // Returns pagination.ErrInvalidPageToken if the token is malformed or can't be
    44  // decrypted and secrets.ErrNoPrimaryAEAD if the encryption key is not
    45  // configured.
    46  func (v *Vault) Cursor(ctx context.Context, pageToken string) (datastore.Cursor, error) {
    47  	if pageToken == "" {
    48  		return nil, nil
    49  	}
    50  
    51  	cursorString, err := secrets.URLSafeDecrypt(ctx, pageToken, v.additionalData)
    52  	switch err {
    53  	case nil:
    54  		// Continue
    55  	case secrets.ErrNoPrimaryAEAD:
    56  		return nil, err
    57  	default:
    58  		return nil, pagination.ErrInvalidPageToken
    59  	}
    60  
    61  	cursor, err := datastore.DecodeCursor(ctx, string(cursorString))
    62  	if err != nil {
    63  		return nil, pagination.ErrInvalidPageToken
    64  	}
    65  	return cursor, nil
    66  }
    67  
    68  const additionalDataPrefix = "common/pagination/dscursor:"
    69  
    70  // NewVault creates a new page token vault with the specified additional data.
    71  //
    72  // Notes:
    73  // * server/secrets module must be initialized with PrimaryTinkAEADKey during
    74  // server start up.
    75  // * The additionalData is used for encryption. Vaults used in different places
    76  // should have different additional data.
    77  func NewVault(additionalData []byte) Vault {
    78  	// Copy the additional data to ensure that it can't be mutated once the
    79  	// vault is initialized.
    80  	ad := make([]byte, 0, len(additionalDataPrefix)+len(additionalData))
    81  	ad = append(ad, additionalDataPrefix...)
    82  	ad = append(ad, additionalData...)
    83  
    84  	return Vault{
    85  		additionalData: ad,
    86  	}
    87  }