From 024f0a427bb4dd135a6db72d8c4596872fe270f8 Mon Sep 17 00:00:00 2001 From: Shane Date: Mon, 6 Jul 2026 16:31:48 -0700 Subject: [PATCH] [PM-40047] Use 'format_version' property probe to determine if cipher JSON is blob encrypted --- src/Core/Vault/Entities/Cipher.cs | 31 ++++++++++++++++++++-- test/Core.Test/Vault/Models/CipherTests.cs | 14 ++++++---- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/Core/Vault/Entities/Cipher.cs b/src/Core/Vault/Entities/Cipher.cs index 6200f5a7c47d..c49d272cd0f5 100644 --- a/src/Core/Vault/Entities/Cipher.cs +++ b/src/Core/Vault/Entities/Cipher.cs @@ -1,6 +1,7 @@ // FIXME: Update this file to be null safe and then delete the line below #nullable disable +using System.Text; using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Utilities; @@ -39,8 +40,34 @@ public bool IsDataBlobEncrypted() return false; } - var span = Data.AsSpan().TrimStart(); - return span.Length > 0 && span[0] != '{'; + try + { + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(Data)); + + // Blob-encrypted data is a JSON object carrying a top-level "format_version" + // key; legacy field-level CipherData JSON never contains it. Probe only the + // top-level properties rather than materializing the whole document. + if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject) + { + return false; + } + + while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName) + { + if (reader.ValueTextEquals("format_version")) + { + return true; + } + + reader.Skip(); + } + + return false; + } + catch (JsonException) + { + return false; + } } public Dictionary GetAttachments() diff --git a/test/Core.Test/Vault/Models/CipherTests.cs b/test/Core.Test/Vault/Models/CipherTests.cs index 053327f98fff..e06a6f905c3b 100644 --- a/test/Core.Test/Vault/Models/CipherTests.cs +++ b/test/Core.Test/Vault/Models/CipherTests.cs @@ -30,17 +30,21 @@ public void Clone_OrganizationCipher_CreatesExactCopy(Cipher cipher) [InlineData(" ")] [InlineData("{\"Name\":\"x\"}")] [InlineData(" { \"Name\": \"x\" }")] - public void IsDataBlobEncrypted_LegacyOrEmpty_ReturnsFalse(string? data) + [InlineData("2.iv|ct|mac")] + [InlineData("plain string")] + [InlineData(" 2.iv|ct|mac")] + [InlineData("{\"other_key\":1}")] + [InlineData("\"format_version\"")] + public void IsDataBlobEncrypted_LegacyEmptyOrInvalid_ReturnsFalse(string? data) { var cipher = new Cipher { Data = data! }; Assert.False(cipher.IsDataBlobEncrypted()); } [Theory] - [InlineData("2.iv|ct|mac")] - [InlineData("plain string")] - [InlineData(" 2.iv|ct|mac")] - public void IsDataBlobEncrypted_OpaqueData_ReturnsTrue(string data) + [InlineData("{\"format_version\":1,\"wrapped_cek\":\"abc\",\"envelope\":\"def\"}")] + [InlineData(" { \"format_version\": 1, \"wrapped_cek\": \"abc\", \"envelope\": \"def\" }")] + public void IsDataBlobEncrypted_BlobData_ReturnsTrue(string data) { var cipher = new Cipher { Data = data }; Assert.True(cipher.IsDataBlobEncrypted());