chromium/device/fido/mac/credential_metadata.h

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This file provides methods to encode credential metadata for storage in the
// macOS keychain.

#ifndef DEVICE_FIDO_MAC_CREDENTIAL_METADATA_H_
#define DEVICE_FIDO_MAC_CREDENTIAL_METADATA_H_

#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/component_export.h"
#include "base/containers/span.h"
#include "base/feature_list.h"
#include "crypto/aead.h"
#include "crypto/hmac.h"
#include "crypto/symmetric_key.h"
#include "device/fido/features.h"

namespace device {

class PublicKeyCredentialUserEntity;

namespace fido::mac {

// CredentialMetadata is the metadata for a Touch ID credential.
//
// This struct is CBOR serialized and stored encrypted in the keychain item for
// the credential. Originally, the encrypted CredentialMetadata was the
// credential ID, and thus all of the contained data was immutable. With
// `Version::kV3` we switched to randomly generated credential IDs, and now
// write the encrypted metadata to a separate attribute, so that it can be
// updated.
struct COMPONENT_EXPORT(DEVICE_FIDO) CredentialMetadata {
 public:
  enum class Version : uint8_t {
    // Initial version.
    kV0 = 0,
    // Added `is_resident`. Previous credentials are non-resident.
    kV1 = 1,
    // Credentials now use a fixed-zero signCounter. Previous credentials
    // encoded timestamps as the signCounter. Also removed the unencrypted
    // version prefix from the credential ID.
    kV2 = 2,
    // Switched to 16-byte random credential IDs. Previous credentials used the
    // encrypted CBOR encoding of `CredentialMetadata` as the credential ID. V3
    // credentials store metadata separate from the credential ID.
    // Also added `uses_timestamp_sign_counter` so we can migrate older
    // instances to V3.
    kV3 = 3,
    // Credentials are created without a `kSecAttrAccessControl` attribute, so
    // that Assertions can now be generated without providing user verification
    // if necessary. Older credentials use a `SecAccessControl` instance with
    // `kSecAccessControlUserPresence` and require the user to pass a device
    // password or Touch ID challenge whenever a signature is produced.
    kV4 = 4,
    // Also update CurrentVersion() when adding values here.
    MAX_VERSION = kV4,
  };

  // Whether the signature counter for the credential is a timestamp or fixed at
  // zero.
  enum class SignCounter : uint8_t {
    kTimestamp = 0,
    kZero = 1,
  };

  // Returns the Version to use for newly created credentials.
  static Version CurrentVersion();

  static CredentialMetadata FromPublicKeyCredentialUserEntity(
      const PublicKeyCredentialUserEntity&,
      bool is_resident);

  CredentialMetadata(Version version,
                     std::vector<uint8_t> user_id,
                     std::string user_name,
                     std::string user_display_name,
                     bool is_resident,
                     SignCounter counter_type);
  CredentialMetadata(const CredentialMetadata&);
  CredentialMetadata(CredentialMetadata&&);
  CredentialMetadata& operator=(const CredentialMetadata&);
  CredentialMetadata& operator=(CredentialMetadata&&);
  ~CredentialMetadata();

  bool operator==(const CredentialMetadata&) const;

  PublicKeyCredentialUserEntity ToPublicKeyCredentialUserEntity() const;

  // The version used when unsealing the credential ID.
  Version version;

  // The following correspond to the fields of the same name in
  // PublicKeyCredentialUserEntity
  // (https://www.w3.org/TR/webauthn/#sctn-user-credential-params).
  //
  // The |user_name| and |user_display_name| fields may be truncated before
  // encryption. The truncated values are guaranteed to be valid UTF-8.
  std::vector<uint8_t> user_id;
  std::string user_name;
  std::string user_display_name;

  // Whether this credential has the resident key (rk) bit and may be returned
  // in response to GetAssertion requests with an empty allowList.
  bool is_resident = false;

  // The type of signature counter used for this credential.
  //  - V0 and V1 credentials implicitly used a timestamp counter.
  //  - V2 implicitly used a zero counter.
  //  - V3 credentials store this field explicitly, but all newly created
  //    credentials use `SignCounter::kZero`. (This allows metadata for older
  //    credentials (with `SignCounter::kTimestamp`) to be reencoded as V3 on
  //    update.)
  SignCounter sign_counter_type = SignCounter::kZero;
};

// Generates a random secret for encrypting and authenticating credential
// metadata for storage in the macOS keychain.
//
// Chrome stores this secret in the Profile Prefs. This allows us to logically
// separate credentials per Chrome profile despite being stored in the same
// keychain. It also guarantees that account metadata in the OS keychain is
// rendered unusable after the Chrome profile and the associated encryption key
// have been deleted, in order to limit leakage of credential metadata into the
// keychain.
COMPONENT_EXPORT(DEVICE_FIDO)
std::string GenerateCredentialMetadataSecret();

// SealCredentialMetadata encodes and encrypts the given CredentialMetadata for
// storage.
COMPONENT_EXPORT(DEVICE_FIDO)
std::vector<uint8_t> SealCredentialMetadata(const std::string& secret,
                                            const std::string& rp_id,
                                            const CredentialMetadata& metadata);

// UnsealCredentialId attempts to decrypt a CredentialMetadata from a credential
// id for version <= kV2.
COMPONENT_EXPORT(DEVICE_FIDO)
std::optional<CredentialMetadata> UnsealMetadataFromLegacyCredentialId(
    const std::string& secret,
    const std::string& rp_id,
    base::span<const uint8_t> credential_id);

// UnsealMetadataFromApplicationTag attempts to decrypt CredentialMetadata from
// an kSecAttrApplicationTag attribute for version >= kV3.
COMPONENT_EXPORT(DEVICE_FIDO)
std::optional<CredentialMetadata> UnsealMetadataFromApplicationTag(
    const std::string& secret,
    const std::string& rp_id,
    base::span<const uint8_t> application_tag);

// EncodeRpIdAndUserIdDeprecated encodes the concatenation of RP ID and user ID
// for storage in the macOS keychain.
//
// This encoding allows lookup of credentials for a given RP and user but
// without the credential ID. This is "deprecated" because we're going to
// abandon that encoding for CredentialMetadata v3. Querying by user ID will
// require iterating over all credentials for the RP ID and looking at the
// unsealed metadata.
COMPONENT_EXPORT(DEVICE_FIDO)
std::string EncodeRpIdAndUserIdDeprecated(const std::string& secret,
                                          const std::string& rp_id,
                                          base::span<const uint8_t> user_id);

// EncodeRpId encodes the given RP ID for storage in the macOS keychain. The
// returned value is guaranteed to be a valid UTF-8 string, to ensure it can
// safely be converted to an NSString and used as a string property in a
// parameters dictionary.
COMPONENT_EXPORT(DEVICE_FIDO)
std::string EncodeRpId(const std::string& secret, const std::string& rp_id);

// DecodeRpId attempts to decode a given RP ID from the keychain.
//
// This can be used to test whether a set of credential metadata was created
// under the given secret without knowing the RP ID (which would be required to
// unseal a credential ID).
COMPONENT_EXPORT(DEVICE_FIDO)
std::optional<std::string> DecodeRpId(const std::string& secret,
                                      const std::string& ciphertext);

// Seals a legacy V0, V1 or V2 credential ID.
COMPONENT_EXPORT(DEVICE_FIDO)
std::vector<uint8_t> SealLegacyCredentialIdForTestingOnly(
    CredentialMetadata::Version version,
    const std::string& secret,
    const std::string& rp_id,
    const std::vector<uint8_t>& user_id,
    const std::string& user_name,
    const std::string& user_display_name,
    bool is_resident);

}  // namespace fido::mac

}  // namespace device

#endif  // DEVICE_FIDO_MAC_CREDENTIAL_METADATA_H_