{"record":{"id":"490dcd56f286f8bd","repo":"tailscale/tailscale","slug":"failed-to-unseal-data","errorCode":null,"errorMessage":"failed to unseal data","messagePattern":"failed to unseal data","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"feature/tpm/tpm.go","lineNumber":355,"sourceCode":"\t\tData:  sealedData,\n\t}, nil\n}\n\nfunc unseal(logf logger.Logf, data encryptedData) (*decryptedData, error) {\n\tif len(data.Nonce) != 24 {\n\t\treturn nil, fmt.Errorf(\"nonce should be 24 bytes long, got %d\", len(data.Nonce))\n\t}\n\n\tunsealedKey, err := tpmUnseal(logf, data.Key)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unseal encryption key with TPM: %w\", err)\n\t}\n\tif len(unsealedKey) != 32 {\n\t\treturn nil, fmt.Errorf(\"unsealed key should be 32 bytes long, got %d\", len(unsealedKey))\n\t}\n\tunsealedData, ok := secretbox.Open(nil, data.Data, (*[24]byte)(data.Nonce), (*[32]byte)(unsealedKey))\n\tif !ok {\n\t\treturn nil, errors.New(\"failed to unseal data\")\n\t}\n\n\treturn &decryptedData{\n\t\tKey:  *(*[32]byte)(unsealedKey),\n\t\tData: unsealedData,\n\t}, nil\n}\n\ntype tpmSealedData struct {\n\tPrivate []byte\n\tPublic  []byte\n}\n\n// withSRK runs fn with the loaded Storage Root Key (SRK) handle. The SRK is\n// flushed after fn returns.\nfunc withSRK(logf logger.Logf, tpm transport.TPM, fn func(srk tpm2.AuthHandle) error) error {\n\tsrkCmd := tpm2.CreatePrimary{\n\t\tPrimaryHandle: tpm2.TPMRHOwner,","sourceCodeStart":337,"sourceCodeEnd":373,"githubUrl":"https://github.com/tailscale/tailscale/blob/cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042/feature/tpm/tpm.go#L337-L373","documentation":"During decryption the TPM successfully unsealed the 32-byte key, but NaCl secretbox.Open rejected the payload: MAC verification failed. The ciphertext or nonce does not match this key, meaning the data is corrupted, was encrypted under a different key, or came from another machine/TPM. The plaintext is unrecoverable from this path.","triggerScenarios":"Calling decryptData on a sealed payload where data.Data or data.Nonce was altered or truncated, or where the payload was encrypted under a different 32-byte key (TPM re-provisioned/cleared, state blob copied from another node).","commonSituations":"Cloning or moving a state directory between machines; TPM firmware update or clear that invalidated the sealing key; disk corruption or partial writes to the state file.","solutions":["If the TPM was reset or re-provisioned, recover from the old backend or regenerate, then re-seal with the current TPM","Restore the state from backup if the blob is corrupted","Verify the state directory belongs to this machine and was never cloned from another node"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// cheap integrity pre-check before decrypting\ngot := sha256.Sum256(blob)\nif !bytes.Equal(got[:], expectedSum) {\n\treturn errors.New(\"state blob checksum mismatch; refusing to decrypt\")\n}","typeGuard":null,"tryCatchPattern":"dd, err := decryptData(logf, data)\nif err != nil {\n\tif strings.Contains(err.Error(), \"failed to unseal data\") {\n\t\t// key/ciphertext mismatch: TPM reset, cloned state, or corruption.\n\t\t// Enter recovery flow; do not retry the same input.\n\t\treturn recoveryMode()\n\t}\n\treturn err\n}","preventionTips":["Keep a backup of the plaintext or a re-seal path before any TPM maintenance","Never clone or move TPM-sealed state directories between machines","Store a checksum next to the sealed blob to distinguish corruption from key mismatch"],"tags":["go","tpm","encryption","data-integrity"],"backgroundTag":null,"analyzedSha":"cfe32b8be6a33f8e24fbc369cbfbf7c729d9e042","analyzedAt":"2026-08-15T19:58:31.583Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}