Invoke-DbaDbDecryptObject - Add NoDAC to decrypt without a dedicated admin connection - #10581
Open
howarthcd wants to merge 2 commits into
Open
Invoke-DbaDbDecryptObject - Add NoDAC to decrypt without a dedicated admin connection#10581howarthcd wants to merge 2 commits into
howarthcd wants to merge 2 commits into
Conversation
…admin connection (do Invoke-DbaDbDecryptObject) Adds -NoDAC, which reads the encrypted definition straight from the raw data pages with DBCC PAGE instead of opening a dedicated admin connection and altering each object inside a rolled back transaction. Nothing is written to the database on this path. Omitting the switch keeps the original behaviour. The reader lives in four new private functions. Get-EncryptedObjectImageValue is the engine, seeking the sysobjvalues clustered index and falling back to a page scan; ConvertFrom-DbccPageDump, ConvertFrom-EncryptedObjectChunk and Get-EncryptedObjectKeystream are split out so they can be unit tested without an instance. Also fixed while here: - A dedicated admin connection this command opens is now closed even when the run fails. The instance loop body is wrapped in try/finally, because an instance allows only one and a leaked session blocked every later run. - Encrypted INSTEAD OF triggers on views are now found, and can only be decrypted with -NoDAC. The default method derives a known plaintext by rewriting the trigger as AFTER INSERT, which a view rejects. - Multi database runs no longer carry objects between databases. - Trigger discovery no longer costs one query per table, and IsEncrypted is added to the SMO init fields, which takes the test suite from 490s to 92s. - -EncodingType warns when bound with -NoDAC, because it is ignored there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nit tests (do Invoke-DbaDbDecryptObject) The unit tests reach the new private functions with & (Get-Module dbatools), which only works while exactly one dbatools module is loaded. Invoke-ManualPester imports dbatools.psd1 and dbatools.psm1, leaving a binary module and a script module both named dbatools, so Get-Module returned two objects. PowerShell joined their names and looked for a command called "dbatools dbatools", failing all ten tests that call a private function. The script module that carries the private functions is now resolved once in a Describe level BeforeAll and reused, and the tests throw a clear message if no such module is loaded rather than failing one by one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
(do Invoke-DbaDbDecryptObject)
Adds -NoDAC, which reads the encrypted definition straight from the raw data pages with DBCC PAGE instead of opening a dedicated admin connection and altering each object inside a rolled back transaction. Nothing is written to the database on this path. Omitting the switch keeps the original behaviour.
The reader lives in four new private functions. Get-EncryptedObjectImageValue is the engine, seeking the sysobjvalues clustered index and falling back to a page scan; ConvertFrom-DbccPageDump, ConvertFrom-EncryptedObjectChunk and Get-EncryptedObjectKeystream are split out so they can be unit tested without an instance.
Also fixed while here:
Type of Change
Invoke-ManualPester -Path <command> -ScriptAnalyzer -Compliance)Purpose
The existing command can only reach an encrypted definition through a dedicated admin connection, and it obtains the known plaintext it needs by altering every object inside a transaction that is rolled back. That rules the command out where a DAC is unavailable or where writing to the database, even transiently, is unacceptable, and an instance allows only one DAC at a time.
It also cannot decrypt an encrypted
INSTEAD OFtrigger defined on a view at all, because the known plaintext it builds rewrites the object as anAFTERtrigger and a view rejects that.Approach
-NoDACderives the RC4 key from public metadata rather than obtaining a known plaintext, so it needs no dedicated admin connection and writes nothing. The scheme is set out under Learning below.The ciphertext lives in
sys.sysobjvalues, which is DAC-only through T-SQL, so the raw pages are read withDBCC PAGE ... WITH TABLERESULTSand the family GUID withDBCC DBINFO WITH TABLERESULTS. Both need sysadmin, checked up front so the failure is a clear message rather than a permission error midway through reading pages.Rows are found by seeking the
sysobjvaluesclustered index, about five page reads whatever the size of the database, with a full page scan as the fallback and as the test oracle.Comment-based help was updated throughout, including a note that a view trigger requires
-NoDAC.Commands to test
The help examples cover the normal paths. Beyond those:
Learning
The part worth writing down is the obfuscation scheme itself.
WITH ENCRYPTIONis widely described as "not really encryption", but the actual construction does not appear to be written up anywhere, so it was reverse engineered for this change and is documented here in case it is useful to anyone else.SQL Server stores the module text in
sys.sysobjvalues.imageval, keyed on the object id withvalclass = 1. The bytes are the UCS-2 (UTF-16LE) source text XORed with an RC4 keystream. There is no secret: the RC4 key is a SHA1 over 22 bytes of metadata that any sysadmin can already read.Four details are load bearing, and each of them fails in a way that is quiet rather than obvious:
System.Guidlayout, where the first three fields are little endian, not the order the GUID prints in. Using the string order produces a valid looking key and complete garbage. The value isdbi_familyGUIDfromDBCC DBINFO, and it is a property of the database family rather than of the object.colIdis an input to the key, so a definition that spans more than onesysobjvaluesrow needs a separate keystream per row. Deriving one keystream for the whole object leaves the first chunk perfectly readable and everything after it mojibake, which reads like an encoding bug rather than a key bug. The chunks also have to be concatenated incolIdorder rather than in the order the rows were read.This is also why the existing method works at all. It never derives the key: it alters the object to a placeholder of exactly the same length inside a transaction that is rolled back, which yields a known plaintext and its matching ciphertext, and XORing the three values together recovers the original. That is a clever way around not knowing the key, but it costs a DAC, a write, and it cannot be applied to an object whose definition it is unable to legally rewrite, which is exactly the
INSTEAD OFtrigger on a view case. Deriving the key directly removes all three constraints.