microsoft / microsoft/mssql-rs

Always Encrypted result metadata exposes ciphertext type instead of the decrypted column’s logical type

Open
#469 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Rust
Stars
53
Forks
14
Avg merge
1d 15h
Merged PRs (30d)
137

Description

Describe the bug

For an Always Encrypted result column, ColumnMetadata exposes:

  • Public data_type
  • Public type_info
  • Crate-private crypto_metadata

The public fields describe the ciphertext transmitted on the wire, normally a binary type. The plaintext metadata is stored in CryptoMetadata as base_data_type and base_type_info, but neither CryptoMetadata nor those fields are accessible outside mssql-tds.

This distinction is explicitly represented in metadata.rs:12-38. The plaintext fields are defined in the crate-private CryptoMetadata structure in metadata.rs:288-310.

The row-decoding path can access the private metadata and therefore decrypt the ciphertext into the correct plaintext ColumnValues, as shown in token_stream.rs:569-596 and decoder.rs:134-175.

External metadata consumers cannot perform the same normalization. For example, async py-core receives ColumnMetadata and derives the Python type, size, precision, and scale only from the public ciphertext fields in async_description.rs:60-184.

This can produce an internally inconsistent result:

  • The fetched value is a decrypted Python int, str, Decimal, date, or other plaintext type.
  • The corresponding cursor description reports bytes and the ciphertext’s binary size.

The lower layer should expose effective/logical result metadata normalized to the plaintext type whenever column decryption is active. It does not need to expose key material or other cryptographic implementation details.

Steps to reproduce

Prerequisites

  • SQL Server 2016 or newer with Always Encrypted support.
  • An mssql-tds client configured with:
    1. ColumnEncryptionSetting::Enabled
    2. A registered column-encryption key-store provider
  • A column master key and column encryption key.
  • A table containing an encrypted INT column.

The repository already contains a self-provisioning Always Encrypted harness in test_always_encrypted.rs:109-180.

SQL shape
Create an encrypted integer column using the same pattern as test_always_encrypted.rs:204-223:

CREATE TABLE dbo.AeMetadataTest
(
id INT IDENTITY(1,1) PRIMARY KEY,
val INT ENCRYPTED WITH
(
COLUMN_ENCRYPTION_KEY = AeTestCek,
ENCRYPTION_TYPE = DETERMINISTIC,
ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256'
) NULL
);

Then:

  1. Insert an integer through an encrypted parameter, for example 1234567.
  2. Execute: SELECT val FROM dbo.AeMetadataTest;
  3. Inspect client.get_metadata()[0] from an external mssql-tds consumer.
  4. Fetch the first row.
  5. Compare the public result metadata with the decoded row value.

Conceptually:

client.execute("SELECT val FROM dbo.AeMetadataTest".to_string(), ()).await?;

let metadata = &client.get_metadata()[0];
// Public metadata describes ciphertext/binary representation.
// Plaintext base_data_type and base_type_info are inaccessible here.

let row = client.next_row().await?.unwrap();
// row[0] is decrypted to ColumnValues::Int(1_234_567).

The existing integer round-trip test in test_always_encrypted.rs:386-434 confirms that decrypted integer values are returned correctly. It was rerun against SQL Server 2025 and passed.

Expected behavior

When Always Encrypted is enabled and a column is successfully decrypted, mssql-tds should expose consumer-facing result metadata representing the logical plaintext column.

For an encrypted INT column:

  • Logical data type: Int4/integer
  • Consumer type: Python int
  • Display/internal size: values appropriate for SQL Server INT
  • Precision and scale: values appropriate for INT

The ciphertext descriptor must remain available internally for wire decoding, but external consumers should have a public normalized metadata view that corresponds to the value they receive.

Actual behavior

The row is successfully decrypted and returned as an integer, but the only public metadata describes the ciphertext’s binary wire representation.

For an encrypted INT column:

  • Fetched row value: decrypted integer
  • Public ColumnMetadata::data_type: ciphertext binary type
  • Public ColumnMetadata::type_info: ciphertext binary length information
  • Plaintext base_data_type and base_type_info: inaccessible outside mssql-tds

Consequently, py-core’s async description can report:

  • Type: Python bytes
  • Size: ciphertext binary size

while fetchone() returns a Python int.
There is currently no public lower-layer API through which py-core can correct this description.

Version

mssql-tds crate: 0.1.0, declared in Cargo.toml:1-3

Affected crate

mssql-tds

Environment

WSL Ubuntu setup

Additional context

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the public and private metadata definitions in metadata.rs:12-38 and metadata.rs:288-310, then trace decryption through token_stream.rs:569-596 and decoder.rs:134-175. Compare the existing encrypted integer coverage in test_always_encrypted.rs:386-434 with py-core’s metadata use in async_description.rs:60-184. Done means external consumers receive logical plaintext metadata for decrypted columns while wire ciphertext metadata remains available internally.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
databases
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.