{"record":{"id":"c41d22b07042c9b9","repo":"JuliusBrussee/caveman","slug":"envelope-tenant-scope-mismatch","errorCode":null,"errorMessage":"envelope: tenant scope mismatch","messagePattern":"envelope: tenant scope mismatch","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/envelope/envelope.go","lineNumber":134,"sourceCode":"// OpenForScope opens v2 ciphertext only for its authenticated scope. It also\n// reads v1 ciphertext during migration; all new tenant-object writes use v2.\nfunc OpenForScope(ciphertext []byte, metaJSON []byte, scope Scope) ([]byte, error) {\n\tvar meta Metadata\n\tif err := json.Unmarshal(metaJSON, &meta); err != nil {\n\t\treturn nil, fmt.Errorf(\"envelope: parse metadata: %w\", err)\n\t}\n\tif meta.Scheme == schemeV1 {\n\t\treturn open(ciphertext, meta, nil)\n\t}\n\tif meta.Scheme != schemeV2 {\n\t\treturn nil, fmt.Errorf(\"envelope: unknown scheme %q\", meta.Scheme)\n\t}\n\taad, scopeHash, err := scopeAAD(scope)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif meta.ScopeHash != scopeHash {\n\t\treturn nil, fmt.Errorf(\"envelope: tenant scope mismatch\")\n\t}\n\treturn open(ciphertext, meta, aad)\n}\n\nfunc open(ciphertext []byte, meta Metadata, aad []byte) ([]byte, error) {\n\twrapped, err := base64.StdEncoding.DecodeString(meta.WrappedDataKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"envelope: decode wrapped key: %w\", err)\n\t}\n\tdataKey, err := secretbox.DecryptPayloadKey(wrapped)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"envelope: unwrap data key: %w\", err)\n\t}\n\tblock, err := aes.NewCipher(dataKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"envelope: aes: %w\", err)\n\t}\n\tgcm, err := cipher.NewGCM(block)","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/shared/platform/envelope/envelope.go#L116-L152","documentation":"OpenForScope computed the scope hash for the caller-supplied tenant/object Scope and it does not match the ScopeHash authenticated in the ciphertext metadata. The AAD binding proves the ciphertext was sealed for a different tenant or object — copying rows between tenants fails decryption by design, even with storage and KMS access.","triggerScenarios":"Thrown at shared/platform/envelope/envelope.go:134 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","solutions":["Pass the scope the ciphertext was originally sealed for (correct tenant/project/object)","If objects legitimately move scopes, re-seal the plaintext under the new scope instead of copying ciphertext"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}