Under the Hood: A Security Deep Dive into CTAP2 hmac-secret

In Part 1 and Part 2, we explored the WebAuthn PRF extension for building end-to-end encrypted web applications. For security architects and systems engineers, however, the JavaScript API is just the top layer. The real power originates at the protocol level: the Client to Authenticator Protocol (CTAP2), and specifically, the hmac-secret extension.

This underlying technology unlocks more than just web-based encryption. It enables powerful system-level security features, such as secure offline workstation access. For example, Microsoft Windows uses hmac-secret to allow users to log into their domain-joined machines with a security key even when disconnected from the network. This article dissects the hmac-secret extension and analyzes a complete reference implementation using libfido2 to demonstrate how to apply these robust cryptographic patterns in any non-web context.

Official Specifications & Security Analyses

This guide is based on the latest public working drafts of the FIDO Alliance and W3C specifications. For a deeper understanding of the security guarantees and threat models, we strongly recommend developers and security architects review the canonical sources:

From WebAuthn to CTAP: The Protocol Stack

When your web application calls navigator.credentials.get(), a chain of events is initiated. The WebAuthn prf extension is the browser API name for the feature formally known as the hmac-secret extension at the CTAP2 protocol level.

A key security feature is how browsers handle salts for domain separation. To prevent a web page for evil-site.com from requesting a secret using a salt known to be used by a native application (like LUKS disk encryption), the browser does not send the developer’s salt directly. Instead, per the W3C WebAuthn specification, it computes a new salt:

actualSalt = SHA-256(UTF8Encode("WebAuthn PRF") || 0x00 || developerSalt)

This transformation, performed by the client, ensures that secrets derived in a web context are cryptographically isolated. Native applications using libfido2 speak directly to the authenticator and must manage their own domain separation strategy.

Threat Model for Local Client-Side Encryption

The FIDO2 architecture is a partnership between an authenticator, a client, and a Relying Party (RP). In a web context, these roles are distinct (YubiKey, browser, web server). However, the prf.c example in this article demonstrates a different model: a self-contained local application.

In this model, the application acts as both the client (using libfido2 to talk to the YubiKey) and its own Relying Party (defining rpId = "localhost"). The primary goal is not to authenticate to a remote server, but to derive a key to encrypt and decrypt local data. This reframes the threat model:

  • Authentication vs. Key Derivation: We are less concerned with verifying the signature on the assertion (which would prove authentication to a server) and far more concerned with the integrity and confidentiality of the PRF secret derived from the YubiKey.

  • Remote vs. Local Threats: The risk of remote phishing is minimal (as localhost is not a valuable target), while the risk of local malware that could intercept the derived key is paramount.

The following table analyzes the threats within this specific context.

Threat

Mitigation by Role

Data Compromise at Rest

  • Authenticator: Guarantees the PRF seed key never leaves the secure element. This is the root of trust.

  • Application (Client/RP): Responsible for using the derived PRF secret to encrypt the local data. The data is only as secure as the encryption implementation.

Phishing

  • Authenticator: The YubiKey still enforces the origin check against rpId = "localhost". This provides a layer of defense against more sophisticated local attacks.

  • Application (Client/RP): Primarily responsible for not being tricked into performing an operation for the wrong credential or salt. In this local model, the threat is less about fake websites and more about ensuring the application’s own logic is sound.

Client-Side Attack (Malware)

  • Authenticator: Not directly involved, as the secret has already been passed to the client.

  • Application (Client/RP): Critically responsible for handling the derived secret securely. It must immediately pass the raw secret to a KDF to derive a purpose-bound key, and then securely wipe the memory containing the raw secret using a function designed to prevent compiler optimization, such as OPENSSL_cleanse() or explicit_bzero().

Lost or Stolen Authenticator

  • Authenticator: Protects credentials with User Presence (touch) and optionally a PIN or fingerprint, preventing trivial use by an attacker.

  • Application (Client/RP): Responsible for the user’s data recovery story. Since the data is encrypted with a key derived from the YubiKey, losing the key means losing the data. The multi-device "Envelope Encryption" pattern from Part 2 is the recommended solution.

Verifying Authenticator Trust with FIDO Attestation and the MDS

For high-security applications, you must prove a credential was created on a genuine, certified authenticator. This is the role of FIDO Attestation. During the create() ceremony, you can request an attestation statement from the YubiKey.

To trust this statement, you must validate it against a known, trusted source: the FIDO Metadata Service (MDS). The MDS is a centralized repository, managed by the FIDO Alliance, containing cryptographic material and status reports for certified authenticators.

Best Practices for Verifying Attestation

  1. Preliminary Client-Side Check: Before attempting registration, the client can call fido_dev_get_info (via libfido2) or a similar SDK function to inspect the authenticator’s capabilities. If "hmac-secret" is not present in the extensions array of the response, the client can inform the user that the device is not suitable for this feature, failing fast.

  2. Fetch and Cache the MDS: Your server should periodically fetch the FIDO MDS blob from the official FIDO Alliance links. Do not query the live service for every registration.

  3. Look Up the AAGUID: When you receive an attestation object, extract the AAGUID (Authenticator Attestation Globally Unique Identifier) from its authenticator data.

  4. Find the Match in MDS: Look up the AAGUID in your cached copy of the MDS. If no entry is found, you may choose to reject the credential.

  5. Verify the Signature: Use the attestationRootCertificates from the MDS entry to build a trusted certificate chain and verify the attestation statement’s signature.

  6. Check the Status: Crucially, check the statusReports for the authenticator in the MDS. This will tell you if any security vulnerabilities have been discovered for that model, allowing you to reject credentials from compromised devices.

By using the MDS, you can enforce a policy that only genuine, certified hardware like a YubiKey can be used to generate PRF-derived keys for your service.

For Systems Developers: A Complete libfido2 + OpenSSL Example

For C/C++ developers, the open-source libfido2 library, built and maintained by Yubico, is the essential tool. The following C code provides a complete, self-contained command-line tool that demonstrates the entire cryptographic lifecycle: creating a PRF-enabled credential, encrypting data, and decrypting data.

Analysis of the Reference Code

This example is a strong model for implementation because it correctly demonstrates several key cryptographic principles:

  1. Separation of Concerns: The code is cleanly divided into functions for credential management (prf_make), raw secret derivation (get_prf_secret), key derivation (derive_key_hkdf), and cryptographic operations (prf_encrypt, prf_decrypt).

  2. KDF Best Practice: The derive_key_hkdf function effectively implements the KDF pattern recommended in Part 2. It takes the raw 32-byte secret from the YubiKey and uses it as Input Keying Material (IKM) for HKDF.

  3. Purpose-Binding: The info parameter in the HKDF call ("AES-GCM-256-Key-v1") provides cryptographic domain separation, ensuring the derived key is suitable for one purpose only.

  4. Secure AEAD Choice: The prf_encrypt function correctly uses AES-256-GCM.

    Note

    When using a 12-byte (96-bit) random nonce with AES-GCM, NIST recommendations suggest that no more than 2^32 encryption operations should be performed with a single key to avoid a high probability of nonce collision. For applications that may exceed this limit, consider using a different nonce generation scheme or a nonce-reuse resistant cipher.

  5. Secure Memory Handling: The code demonstrates critical security hygiene by calling OPENSSL_cleanse(), a function designed to securely wipe sensitive key material from memory and prevent it from being left behind by compiler optimizations. Using standard functions like memset() is not sufficient for this purpose.

  6. Implementation Note: For simplicity, this example passes the message to be encrypted as a command-line argument. A production application would likely use file I/O (reading from stdin/a file, writing to stdout/a file) to handle data of arbitrary length, which would also remove the need for hex-encoding the ciphertext.

prf.c Reference Implementation

/*
 * Copyright (c) 2025 Yubico AB. All rights reserved.
 * Use of this source code is governed by a BSD-style
 * license that can be found in the LICENSE file.
 * SPDX-License-Identifier: BSD-2-Clause
 */

/*
 * Example demonstrating the CTAP2 hmac-secret extension.
 * This shows how to:
 * 1. Create a credential with hmac-secret extension enabled
 * 2. Encrypt a message using PRF-derived key with HKDF + AES-GCM
 * 3. Decrypt the message back to plaintext
 *
 * Usage:
 * prf -M [-P pin] <device>                         # Make credential with PRF support
 * prf -E [-P pin] <device> <cred_id_hex> <message>      # Encrypt message
 * prf -D [-P pin] <device> <cred_id_hex> <ciphertext>   # Decrypt message
 *
 * This tool serves as a reference implementation for developers building native
 * applications that require strong, phishing-resistant, client-side encryption.
 * While this example demonstrates modern cryptographic best practices, it is
 * intended as an educational example. Developers must perform their own security
 * reviews and threat modeling to ensure the patterns and cryptographic choices
 * are appropriate for their specific use case.
 */

#include <errno.h>
#include <fido.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/kdf.h>

#include "../openbsd-compat/openbsd-compat.h"
#include "extern.h"

static const unsigned char cdh[32] = {
    0xf9, 0x64, 0x57, 0xe7, 0x2d, 0x97, 0xf6, 0xbb,
    0xdd, 0xd7, 0xfb, 0x06, 0x37, 0x62, 0xea, 0x26,
    0x20, 0x44, 0x8e, 0x69, 0x7c, 0x03, 0xf2, 0x31,
    0x2f, 0x99, 0xdc, 0xaf, 0x3e, 0x8a, 0x91, 0x6b,
};

static const unsigned char user_id[32] = {
    0x78, 0x1c, 0x78, 0x60, 0xad, 0x88, 0xd2, 0x63,
    0x32, 0x62, 0x2a, 0xf1, 0x74, 0x5d, 0xed, 0xb2,
    0xe7, 0xa4, 0x2b, 0x44, 0x89, 0x29, 0x39, 0xc5,
    0x56, 0x64, 0x01, 0x27, 0x0d, 0xbb, 0xc4, 0x49,
};

static void
usage(void)
{
    fprintf(stderr, "usage: prf -M [-P pin] <device>\n");
    fprintf(stderr, "       prf -E [-P pin] <device> <cred_id_hex> <message>\n");
    fprintf(stderr, "       prf -D [-P pin] <device> <cred_id_hex> <ciphertext_hex>\n");
    fprintf(stderr, "\n");
    fprintf(stderr, "  -M          make a new PRF-capable credential\n");
    fprintf(stderr, "  -E          encrypt a message using a PRF-derived key\n");
    fprintf(stderr, "  -D          decrypt a message using a PRF-derived key\n");
    fprintf(stderr, "  -P pin      use PIN for authentication\n");
    exit(EXIT_FAILURE);
}

static void
print_hex(const char *label, const unsigned char *ptr, size_t len)
{
    size_t i;

    printf("%s", label);
    for (i = 0; i < len; i++) {
        printf("%02x", ptr[i]);
    }
    printf("\n");
}

static unsigned char *
hex_decode(const char *hex_str, size_t *len)
{
    size_t hex_len = strlen(hex_str);
    unsigned char *buf;
    size_t i;

    if (hex_len % 2 != 0)
        errx(1, "hex string must have even length");

    *len = hex_len / 2;
    if ((buf = malloc(*len)) == NULL)
        errx(1, "malloc");

    for (i = 0; i < *len; i++) {
        if (sscanf(hex_str + i * 2, "%2hhx", &buf[i]) != 1)
            errx(1, "invalid hex character");
    }

    return buf;
}

static unsigned char *
get_prf_secret(const char *device_path, const unsigned char *cred_id, size_t cred_id_len, const char *pin)
{
    fido_dev_t *dev;
    fido_assert_t *assert;
    unsigned char salt[32];
    unsigned char *secret;
    int r;

    /* Create application-specific salt */
    memset(salt, 0, sizeof(salt));
    strcpy((char *)salt, "my-app-encryption-v1");

    if ((dev = fido_dev_new()) == NULL)
        errx(1, "fido_dev_new");
    if ((r = fido_dev_open(dev, device_path)) != FIDO_OK)
        errx(1, "fido_dev_open: %s (0x%x)", fido_strerr(r), r);

    if ((assert = fido_assert_new()) == NULL)
        errx(1, "fido_assert_new");

    /* Set assertion parameters */
    if ((r = fido_assert_set_clientdata_hash(assert, cdh, sizeof(cdh))) != FIDO_OK)
        errx(1, "fido_assert_set_clientdata_hash: %s (0x%x)", fido_strerr(r), r);
    if ((r = fido_assert_set_rp(assert, "localhost")) != FIDO_OK)
        errx(1, "fido_assert_set_rp: %s (0x%x)", fido_strerr(r), r);
    if ((r = fido_assert_allow_cred(assert, cred_id, cred_id_len)) != FIDO_OK)
        errx(1, "fido_assert_allow_cred: %s (0x%x)", fido_strerr(r), r);

    /* Enable hmac-secret extension and set salt */
    if ((r = fido_assert_set_extensions(assert, FIDO_EXT_HMAC_SECRET)) != FIDO_OK)
        errx(1, "fido_assert_set_extensions: %s (0x%x)", fido_strerr(r), r);
    if ((r = fido_assert_set_hmac_salt(assert, salt, sizeof(salt))) != FIDO_OK)
        errx(1, "fido_assert_set_hmac_salt: %s (0x%x)", fido_strerr(r), r);

    if ((r = fido_dev_get_assert(dev, assert, pin)) != FIDO_OK)
        errx(1, "fido_dev_get_assert: %s (0x%x)", fido_strerr(r), r);

    if (fido_assert_count(assert) != 1)
        errx(1, "unexpected assertion count %zu", fido_assert_count(assert));

    /* Copy the secret */
    if (fido_assert_hmac_secret_ptr(assert, 0) == NULL)
        errx(1, "no hmac-secret returned");

    if ((secret = malloc(32)) == NULL)
        errx(1, "malloc");
    memcpy(secret, fido_assert_hmac_secret_ptr(assert, 0), 32);

    fido_assert_free(&assert);
    fido_dev_close(dev);
    fido_dev_free(&dev);

    return secret;
}

static int
derive_key_hkdf(unsigned char *prf_secret, unsigned char *aes_key)
{
    EVP_PKEY_CTX *pctx;
    unsigned char info[] = "AES-GCM-256-Key-v1";
    size_t outlen = 32;

    if ((pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL)) == NULL)
        return -1;

    if (EVP_PKEY_derive_init(pctx) <= 0 ||
        EVP_PKEY_CTX_set_hkdf_md(pctx, EVP_sha256()) <= 0 ||
        EVP_PKEY_CTX_set1_hkdf_key(pctx, prf_secret, 32) <= 0 ||
        EVP_PKEY_CTX_add1_hkdf_info(pctx, info, sizeof(info) - 1) <= 0 ||
        EVP_PKEY_derive(pctx, aes_key, &outlen) <= 0) {
        EVP_PKEY_CTX_free(pctx);
        return -1;
    }

    EVP_PKEY_CTX_free(pctx);
    return 0;
}

static int
prf_encrypt(const char *device_path, const char *cred_id_hex, const char *message, const char *pin)
{
    unsigned char *cred_id, *prf_secret, aes_key[32];
    unsigned char iv[12], tag[16], *ciphertext;
    size_t cred_id_len, message_len, ciphertext_len;
    EVP_CIPHER_CTX *ctx;
    int len;

    /* Decode credential ID */
    cred_id = hex_decode(cred_id_hex, &cred_id_len);

    /* Get PRF secret */
    prf_secret = get_prf_secret(device_path, cred_id, cred_id_len, pin);

    /* Derive AES key using HKDF */
    if (derive_key_hkdf(prf_secret, aes_key) != 0)
        errx(1, "HKDF key derivation failed");

    /* Generate random IV */
    if (RAND_bytes(iv, sizeof(iv)) != 1)
        errx(1, "RAND_bytes failed");

    message_len = strlen(message);
    if ((ciphertext = malloc(message_len)) == NULL)
        errx(1, "malloc");

    /* Encrypt */
    if ((ctx = EVP_CIPHER_CTX_new()) == NULL)
        errx(1, "EVP_CIPHER_CTX_new");

    if (EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 ||
        EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, sizeof(iv), NULL) != 1 ||
        EVP_EncryptInit_ex(ctx, NULL, NULL, aes_key, iv) != 1 ||
        EVP_EncryptUpdate(ctx, ciphertext, &len, (const unsigned char *)message, (int)message_len) != 1 ||
        EVP_EncryptFinal_ex(ctx, ciphertext + len, &len) != 1 ||
        EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, sizeof(tag), tag) != 1)
        errx(1, "encryption failed");

    ciphertext_len = message_len;

    /* Output: IV + ciphertext + tag (all hex encoded) */
    print_hex("", iv, sizeof(iv));
    print_hex("", ciphertext, ciphertext_len);
    print_hex("", tag, sizeof(tag));

    /* Clean up */
    OPENSSL_cleanse(prf_secret, 32);
    OPENSSL_cleanse(aes_key, sizeof(aes_key));
    free(cred_id);
    free(prf_secret);
    free(ciphertext);
    EVP_CIPHER_CTX_free(ctx);

    return 0;
}

static int
prf_decrypt(const char *device_path, const char *cred_id_hex, const char *ciphertext_hex, const char *pin)
{
    unsigned char *cred_id, *prf_secret, aes_key[32];
    unsigned char *combined_data, iv[12], tag[16], *ciphertext, *plaintext;
    size_t cred_id_len, combined_len, ciphertext_len;
    EVP_CIPHER_CTX *ctx;
    int len, plaintext_len;

    /* Decode credential ID and ciphertext */
    cred_id = hex_decode(cred_id_hex, &cred_id_len);
    combined_data = hex_decode(ciphertext_hex, &combined_len);

    /* Extract IV, ciphertext, and tag */
    if (combined_len < sizeof(iv) + sizeof(tag))
        errx(1, "ciphertext too short");

    memcpy(iv, combined_data, sizeof(iv));
    ciphertext_len = combined_len - sizeof(iv) - sizeof(tag);
    ciphertext = combined_data + sizeof(iv);
    memcpy(tag, combined_data + sizeof(iv) + ciphertext_len, sizeof(tag));

    if ((plaintext = malloc(ciphertext_len + 1)) == NULL)
        errx(1, "malloc");

    /* Get PRF secret */
    prf_secret = get_prf_secret(device_path, cred_id, cred_id_len, pin);

    /* Derive AES key using HKDF */
    if (derive_key_hkdf(prf_secret, aes_key) != 0)
        errx(1, "HKDF key derivation failed");

    /* Decrypt */
    if ((ctx = EVP_CIPHER_CTX_new()) == NULL)
        errx(1, "EVP_CIPHER_CTX_new");

    if (EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), NULL, NULL, NULL) != 1 ||
        EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, sizeof(iv), NULL) != 1 ||
        EVP_DecryptInit_ex(ctx, NULL, NULL, aes_key, iv) != 1 ||
        EVP_DecryptUpdate(ctx, plaintext, &len, ciphertext, (int)ciphertext_len) != 1)
        errx(1, "decryption failed");

    plaintext_len = len;

    if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, sizeof(tag), tag) != 1 ||
        EVP_DecryptFinal_ex(ctx, plaintext + len, &len) != 1)
        errx(1, "authentication failed - wrong key or corrupted data");

    plaintext[plaintext_len] = '\0';
    printf("%s\n", plaintext);

    /* Clean up */
    OPENSSL_cleanse(prf_secret, 32);
    OPENSSL_cleanse(aes_key, sizeof(aes_key));
    free(cred_id);
    free(prf_secret);
    free(combined_data);
    free(plaintext);
    EVP_CIPHER_CTX_free(ctx);

    return 0;
}

static int
prf_make(const char *path, const char *pin)
{
    fido_dev_t *dev;
    fido_cred_t *cred;
    int r;

    if ((dev = fido_dev_new()) == NULL)
        errx(1, "fido_dev_new");
    if ((r = fido_dev_open(dev, path)) != FIDO_OK)
        errx(1, "fido_dev_open: %s (0x%x)", fido_strerr(r), r);

    if ((cred = fido_cred_new()) == NULL)
        errx(1, "fido_cred_new");

    /* Set credential parameters */
    if ((r = fido_cred_set_type(cred, COSE_ES256)) != FIDO_OK)
        errx(1, "fido_cred_set_type: %s (0x%x)", fido_strerr(r), r);
    if ((r = fido_cred_set_clientdata_hash(cred, cdh, sizeof(cdh))) != FIDO_OK)
        errx(1, "fido_cred_set_clientdata_hash: %s (0x%x)", fido_strerr(r), r);
    if ((r = fido_cred_set_rp(cred, "localhost", "localhost")) != FIDO_OK)
        errx(1, "fido_cred_set_rp: %s (0x%x)", fido_strerr(r), r);
    if ((r = fido_cred_set_user(cred, user_id, sizeof(user_id), "john",
        "John Doe", NULL)) != FIDO_OK)
        errx(1, "fido_cred_set_user: %s (0x%x)", fido_strerr(r), r);

    /*
     * Enable the hmac-secret extension. This is the crucial step
     * that instructs the authenticator to generate the necessary
     * internal key material for future PRF operations.
     */
    if ((r = fido_cred_set_extensions(cred, FIDO_EXT_HMAC_SECRET)) != FIDO_OK)
        errx(1, "fido_cred_set_extensions: %s (0x%x)", fido_strerr(r), r);

    if ((r = fido_dev_make_cred(dev, cred, pin)) != FIDO_OK)
        errx(1, "fido_dev_make_cred: %s (0x%x)", fido_strerr(r), r);

    /* Output credential ID and public key */
    print_hex("", fido_cred_id_ptr(cred), fido_cred_id_len(cred));
    print_hex("", fido_cred_pubkey_ptr(cred), fido_cred_pubkey_len(cred));

    fido_cred_free(&cred);
    fido_dev_close(dev);
    fido_dev_free(&dev);

    return 0;
}

int
main(int argc, char **argv)
{
    bool make_cred = false;
    bool encrypt = false;
    bool decrypt = false;
    char *pin = NULL;
    int ch;

    while ((ch = getopt(argc, argv, "MEDP:")) != -1) {
        switch (ch) {
        case 'M':
            make_cred = true;
            break;
        case 'E':
            encrypt = true;
            break;
        case 'D':
            decrypt = true;
            break;
        case 'P':
            pin = optarg;
            break;
        default:
            usage();
        }
    }

    argc -= optind;
    argv += optind;

    if (((int)make_cred + (int)encrypt + (int)decrypt) != 1)
        usage();

    if (make_cred) {
        if (argc != 1)
            usage();
        return prf_make(argv[0], pin);
    } else if (encrypt) {
        if (argc != 3)
            usage();
        return prf_encrypt(argv[0], argv[1], argv[2], pin);
    } else { /* decrypt */
        if (argc != 3)
            usage();
        return prf_decrypt(argv[0], argv[1], argv[2], pin);
    }
}

Example Workflow

After compiling the prf.c example within the libfido2 build environment, you can use the resulting prf executable to perform the full cryptographic lifecycle.

  1. Step 1: Create PRF-enabled credential

    $ ./prf -M -P <pin_if_set> /dev/hidraw0
    # Output will be two hex strings:
    # <credential_id_hex>
    # <public_key_hex>
    

    Save the first hex string, which is the credential ID for your new, PRF-enabled credential.

  2. Step 2: Encrypt a message

    $ ./prf -E -P <pin_if_set> /dev/hidraw0 <credential_id_hex> "Hello, secure world"
    # Output will be three concatenated hex strings:
    # <iv_hex><ciphertext_hex><tag_hex>
    

    This command derives the secret from your YubiKey, uses HKDF to create an AES key, encrypts your message, and outputs the necessary components for decryption.

  3. Step 3: Decrypt the message

    # Combine the three hex strings from step 2 into one long string
    ./prf -D -P <pin_if_set> /dev/hidraw0 <credential_id_hex> <iv_hex><ciphertext_hex><tag_hex>
    # Expected output:
    Hello, secure world
    

    This command re-derives the exact same key from your YubiKey, uses the IV to initialize the AES-GCM cipher, decrypts the ciphertext, and verifies the authentication tag. If the tag is valid, it prints the original plaintext.

This complete workflow demonstrates the power of the hmac-secret extension providing hardware-backed, phishing-resistant encryption that is ideal for real-world applications like password managers, encrypted note apps, or secure system utilities.