Building on an excellent explanation of encryption internals by Paul White as well as Dirceu Resende’s fine script below is an extended and hopefully more readable version of this solution, which handles large-size objects better (for example procedures with hundreds of lines of code). Additionally, if @CreateDecryptedVersion = 1 is specified the script will rename the original encrypted object by appending “_ENCRYPTED” to the end of the name and create its decrypted version, with the original name. To run it you need to connect using Dedicated Admin Connection and if needed enable it first.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
USE [YourDbName]
GO

SET NOCOUNT ON;
SET XACT_ABORT ON;

/* ====================================================================================================================== */
/* Author:      CleanSql.com                                                                                              */
/* an extended and hopefully improved version of:                                                                         */
/* https://www.sqlteam.com/forums/topic.asp?TOPIC_ID=76258                                                                */
/* https://sqlperformance.com/2016/05/sql-performance/the-internals-of-with-encryption                                    */
/* https://en.dirceuresende.com/blog/sql-server-como-recuperar-o-codigo-fonte-de-um-objeto-criptografado-with-encryption/ */
/* https://stackoverflow.com/questions/7670636/how-to-decrypt-stored-procedure-in-sql-server-2008/7671944#7671944         */
/* Create date: 2023-02-11                                                                                                */
/* Description: Deobfuscate and show the definition of an "encrypted" db object; additionally                             */
/*              if @CreateDecryptedVersion = 1 is specified it will rename the original encrypted object                  */
/*              by appending "_ENCRYPTED" to the end of the name and create the decrypted version with the original name  */
/* ====================================================================================================================== */
/* Change History:                                                                                                        */
/* ---------------------------------------------------------------------------------------------------------------------- */
/* Date:       User:           Version:  Change:                                                                          */
/* ---------------------------------------------------------------------------------------------------------------------- */
/* 2023-02-11  CleanSql.com    1.0       Created                                                                          */
/* ---------------------------------------------------------------------------------------------------------------------- */
/* ====================================================================================================================== */

/* ====================================================================================================================== */
/* ----------------------------------------- Variable and Temp Table Declarations: -------------------------------------- */
/* ====================================================================================================================== */

/* user varaibles (supply values as needed) */
DECLARE @EncryptedObjectOwnerOrSchema SYSNAME       = N'dbo'
      , @EncryptedObjectName          SYSNAME       = N'YourEncryptedObjectName' /* for example: p_TestEncryption */
      , @CreateDecryptedVersion       BIT           = 0 /* if set to 1 the script will rename the original encrypted object
                                                          by appending "_ENCRYPTED" to the end of the name
                                                          and create its decrypted version, with the original name*/
      , @PrintOutObjectDefinition     BIT           = 1;

/* internal variables (no need to supply any values) */
DECLARE @SessionId              INT
      , @ObjectID               INT
      , @ObjectType             NVARCHAR(128)
      , @TriggerOnSchema        SYSNAME
      , @TriggerOnTable         SYSNAME
      , @TriggerForType         NVARCHAR(32)/* Maximum possible length is 22 => SELECT LEN('INSERT, UPDATE, DELETE') */
      , @RealEncryptedObject    NVARCHAR(MAX)
      , @FakePlainTextObject    NVARCHAR(MAX)
      , @FakeEncryptedObject    NVARCHAR(MAX)
      , @RealDecryptedObject    NVARCHAR(MAX)
      , @ObjectDataLength       INT
      , @PointerDecryptedString INT
      , @PointerBeginOfNewLine  INT
      , @CrLf                   CHAR(2)      = CHAR(13) + CHAR(10)
      , @DecryptedLineOfCode    NVARCHAR(MAX);

/* error message varaiables: */
DECLARE @ErrorNumber   INT
      , @ErrorMessage  NVARCHAR(MAX)
      , @ErrorSeverity INT
      , @ErrorState    INT
      , @ErrorLine     INT;

DROP TABLE IF EXISTS [#ObjectDefinition]
CREATE TABLE [#ObjectDefinition]
(
    [LineId] INT PRIMARY KEY CLUSTERED IDENTITY(1,1),
    [DecryptedLineOfCode] NVARCHAR(MAX)
);

/* ====================================================================================================================== */
/* ----------------------------------------- Check if DAC connection is used here: -------------------------------------- */
/* ====================================================================================================================== */

SELECT      @SessionId = ses.session_id
FROM        sys.endpoints AS en
INNER JOIN  sys.dm_exec_sessions ses ON en.endpoint_id = ses.endpoint_id
WHERE       en.name = 'Dedicated Admin Connection';

IF (@@SPID <> (COALESCE(@SessionId, 0)))
BEGIN
    SET @ErrorMessage
        = N'In order to run this script you need to connect using Dedicated Admin Connection (DAC).';
    RAISERROR(@ErrorMessage, 16, 1);
    RETURN;
END

/* ====================================================================================================================== */
/* ----------------------------------------- Check Input: --------------------------------------------------------------- */
/* ====================================================================================================================== */

SET @ObjectID = OBJECT_ID('[' + @EncryptedObjectOwnerOrSchema + '].[' + @EncryptedObjectName + ']');
IF  @ObjectID IS NULL
BEGIN
SET @ErrorMessage
    = N'Object [' + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName
      + N'] does not exist in the database: [' + DB_NAME(DB_ID()) + N'].';
RAISERROR(@ErrorMessage, 16, 1);
RETURN;
END

IF NOT EXISTS
(
    SELECT      1
    FROM        sys.objects
    WHERE       [object_id] = @ObjectID
)
BEGIN
    SET @ErrorMessage
        = N'Object [' + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName + N'] with ID: ['+CONVERT(VARCHAR(32), @ObjectID)+'] in database: ['
          + DB_NAME(DB_ID()) + N'] does not have an entry in sys.objects.';
    RAISERROR(@ErrorMessage, 16, 1);
    RETURN;
END;

IF OBJECTPROPERTY(@ObjectID, 'IsEncrypted') = 0
BEGIN
    SET @ErrorMessage
        = N'Object [' + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName + N'] exists in the database: ['
          + DB_NAME(DB_ID()) + N'] but it is not encrypted.';
    RAISERROR(@ErrorMessage, 16, 1);
    RETURN;
END;

/* ====================================================================================================================== */
/* ----------------------------------------- Determine Object Type: ----------------------------------------------------- */
/* ====================================================================================================================== */

SELECT      @ObjectType =   so.[type]
FROM        sys.objects     so
WHERE       so.[object_id] = @ObjectID
AND         OBJECTPROPERTY(@ObjectID, 'IsEncrypted') = 1
IF  (@ObjectType IS NULL)
    BEGIN
            SET @ErrorMessage
                = N'Could not find Object Type in sys.objects for [' + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName
                  + N'] in the database: [' + DB_NAME(DB_ID()) + N'].';
            RAISERROR(@ErrorMessage, 16, 1);
            RETURN;
    END

IF (@ObjectType NOT IN ('P', 'V', 'TR', 'FN', 'TF', 'IF'))
    BEGIN
        SET @ErrorMessage
            = N'Object [' + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName + N'] exists in the database: ['
              + DB_NAME(DB_ID()) + N'] but the object type: ['+ @ObjectType +'] is not handled by this script. '
              +CHAR(10)+'Currently supported object-types are: '
              +CHAR(10)+'[P] - PROCEDURE,'
              +CHAR(10)+'[V] - VIEW,'
              +CHAR(10)+'[TR]- TRIGGER,'
              +CHAR(10)+'[FN]- FUNCTION,'
              +CHAR(10)+'[TF]- TABLE-VALUED FUNCTION,'
              +CHAR(10)+'[IF]- IN-LINED TABLE-VALUED FUNCTION';
        RAISERROR(@ErrorMessage, 16, 1);
        RETURN;
    END;

/* ====================================================================================================================== */
/* ----------------------------------------- If it's a trigger get its type: -------------------------------------------- */
/* ====================================================================================================================== */

IF (@ObjectType = 'TR')
	BEGIN
	     SELECT     @TriggerOnSchema = sch.[name],
	                @TriggerOnTable = OBJECT_NAME(tr.parent_id),
	                @TriggerForType = REPLACE(LTRIM(RTRIM(
	                                  CASE WHEN OBJECTPROPERTY(so.[object_id], 'ExecIsInsertTrigger') = 1 THEN 'INSERT ' ELSE '' END +
	                                  CASE WHEN OBJECTPROPERTY(so.[object_id], 'ExecIsUpdateTrigger') = 1 THEN 'UPDATE ' ELSE '' END +
	                                  CASE WHEN OBJECTPROPERTY(so.[object_id], 'ExecIsDeleteTrigger') = 1 THEN 'DELETE ' ELSE '' END)), ' ', ', ')
	     FROM       sys.objects  so
	     INNER JOIN sys.triggers tr  ON tr.object_id = so.object_id
	     INNER JOIN sys.tables   st  ON tr.parent_id = st.object_id
	     INNER JOIN sys.schemas  sch ON so.schema_id = sch.schema_id
	     WHERE      so.[type] = 'TR'
	     AND        so.[object_id] = @ObjectID;
	END

/* ====================================================================================================================== */
/* ----------------------------------------- Prepopulate @FakePlainTextObject header: ----------------------------------- */
/* ====================================================================================================================== */

SELECT  @RealEncryptedObject = imageval
FROM    sys.sysobjvalues
WHERE   [objid] = @ObjectID
AND     valclass = 1

SET     @ObjectDataLength = DATALENGTH(@RealEncryptedObject) / 2;

SELECT @FakePlainTextObject =
CASE   @ObjectType
       WHEN (N'P')  THEN N'ALTER PROCEDURE [' + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName + N'] WITH ENCRYPTION AS'
       WHEN (N'V')  THEN N'ALTER VIEW ['      + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName + N'] WITH ENCRYPTION AS SELECT 1 AS [1]'
       WHEN (N'TR') THEN N'ALTER TRIGGER ['   + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName + N'] ON ['
                                              + @TriggerOnSchema + N'].['              + @TriggerOnTable + N'] WITH ENCRYPTION FOR ' + @TriggerForType + N' AS BEGIN SELECT 1 END'
       WHEN (N'FN') THEN N'ALTER FUNCTION ['  + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName + N']() RETURNS INT WITH ENCRYPTION AS BEGIN RETURN 1 END'
       WHEN (N'TF') THEN N'ALTER FUNCTION ['  + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName + N']() RETURNS @t TABLE (p1 INT) WITH ENCRYPTION AS BEGIN INSERT @t SELECT 1 RETURN END'
       WHEN (N'IF') THEN N'ALTER FUNCTION ['  + @EncryptedObjectOwnerOrSchema + N'].[' + @EncryptedObjectName + N']() RETURNS TABLE WITH ENCRYPTION AS RETURN (SELECT 1 AS [1])'
END

/* ====================================================================================================================== */
/* ----------------------------------------- Pad the rest of @FakePlainTextObject with dashes: -------------------------- */
/* ====================================================================================================================== */

WHILE DATALENGTH(@FakePlainTextObject) / 2 < @ObjectDataLength
	  BEGIN
	      IF DATALENGTH(@FakePlainTextObject) / 2 + 4000 < @ObjectDataLength
	          SET @FakePlainTextObject = @FakePlainTextObject + REPLICATE(N'-', 4000);
	      ELSE
	          SET @FakePlainTextObject
	              = @FakePlainTextObject + REPLICATE(N'-', @ObjectDataLength - (DATALENGTH(@FakePlainTextObject) / 2));
	  END;

/* ====================================================================================================================== */
/* ----------------------------------------- Create @FakePlainTextObject to store  -------------------------------------- */
/* ----------------------------------------- its encrypted version in @FakeEncryptedObject: ----------------------------- */
/* ====================================================================================================================== */

/* SET XACT_ABORT OFF; */
BEGIN TRAN;
    EXEC(@FakePlainTextObject);

    SELECT  @FakeEncryptedObject = imageval
    FROM    sys.sysobjvalues
    WHERE   [objid] = @ObjectID
    AND     valclass = 1

IF @@TRANCOUNT > 0
BEGIN
    /* Now that the FakePlainTextObject is created we can rollback its creation to keep the real object as it was */
    ROLLBACK TRAN;
END

SET @FakePlainTextObject = REPLACE(@FakePlainTextObject, 'ALTER PROCEDURE', 'CREATE PROCEDURE');
SET @FakePlainTextObject = REPLACE(@FakePlainTextObject, 'ALTER VIEW', 'CREATE VIEW');
SET @FakePlainTextObject = REPLACE(@FakePlainTextObject, 'ALTER FUNCTION', 'CREATE FUNCTION');
SET @FakePlainTextObject = REPLACE(@FakePlainTextObject, 'ALTER TRIGGER', 'CREATE TRIGGER');

/* ====================================================================================================================== */
/* ----------------------------------------- Pad @RealDecryptedObject with placeholder characters:  --------------------- */
/* ====================================================================================================================== */

SET @RealDecryptedObject = N'';
WHILE DATALENGTH(@RealDecryptedObject) / 2 < @ObjectDataLength
BEGIN
    IF DATALENGTH(@RealDecryptedObject) / 2 + 4000 < @ObjectDataLength
        SET @RealDecryptedObject = @RealDecryptedObject + REPLICATE(N'*', 4000);
    ELSE
        SET @RealDecryptedObject
            = @RealDecryptedObject
              + REPLICATE(N'*', @ObjectDataLength - (DATALENGTH(@RealDecryptedObject) / 2));
END;

/* ====================================================================================================================== */
/* ----------------------------------------- Do the actual decryption into @RealDecryptedObject:  ----------------------- */
/* ====================================================================================================================== */

SET @PointerDecryptedString = 1;
WHILE (@PointerDecryptedString <= @ObjectDataLength)
BEGIN
    /*  
        Replace 1 character at a time in the @RealDecryptedObject at the @PointerDecryptedString position
        with the result of XOR operation (^) between @RealEncryptedObject and the Encryption Key for each character;
		Encryption Key is obtained by applying XOR operation (^) between @FakePlainTextObject and @FakeEncryptedObject
    */
    SET @RealDecryptedObject
        = STUFF(
                   @RealDecryptedObject,
                   @PointerDecryptedString,
                   1,
                   NCHAR(UNICODE(SUBSTRING(@RealEncryptedObject, @PointerDecryptedString, 1))
                         ^ (UNICODE(SUBSTRING(@FakePlainTextObject, @PointerDecryptedString, 1))
                            ^ UNICODE(SUBSTRING(@FakeEncryptedObject, @PointerDecryptedString, 1))
                           )
                        )
               );
    SET @PointerDecryptedString = @PointerDecryptedString + 1;
END;

/* ====================================================================================================================== */
/* ----------------------------------------- Comment out the 'WITH ENCRYPTION' clause:  --------------------------------- */
/* ====================================================================================================================== */

IF (CHARINDEX('WITH ENCRYPTION', @RealDecryptedObject COLLATE Latin1_General_CI_AI)) > 0
BEGIN
    /* COLLATE Latin1_General_CI_AI makes below Case-Insensitive (valid for both: 'WITH ENCRYPTION' and 'with encryption'): */
    SET @RealDecryptedObject = REPLACE(@RealDecryptedObject COLLATE Latin1_General_CI_AI, 'WITH ENCRYPTION', '/* WITH ENCRYPTION */')
END

IF (@CreateDecryptedVersion = 1)
BEGIN
    DECLARE @EncryptedObjectNewName SYSNAME = @EncryptedObjectName + '_ENCRYPTED';
    DECLARE @RenameResult INT
    EXEC @RenameResult = sp_rename @objname = @EncryptedObjectName, @newname = @EncryptedObjectNewName;
    IF @RenameResult <> 0
    BEGIN
        SET @ErrorMessage = CONCAT(N'sp_rename returned: ', ERROR_MESSAGE())
        RAISERROR(@ErrorMessage, 16, 1);
    END
    ELSE
    BEGIN
        PRINT(CONCAT('Successfully renamed: ', QUOTENAME(@EncryptedObjectName), ' to: ', QUOTENAME(@EncryptedObjectNewName)));
    BEGIN TRY
        BEGIN TRY
            EXECUTE sp_executesql @stmt = @RealDecryptedObject;
            PRINT(CONCAT('Successfully created decrypted version of: ', QUOTENAME(@EncryptedObjectName)));
        END TRY
        BEGIN CATCH   
            SELECT
                 @ErrorNumber = ERROR_NUMBER()
                ,@ErrorMessage = ERROR_MESSAGE()
                ,@ErrorSeverity = ERROR_SEVERITY()
                ,@ErrorState = ERROR_STATE()
                ,@ErrorLine = ERROR_LINE();

            RAISERROR('Error %d caught in @RealDecryptedObject at line %d: %s'
                ,@ErrorSeverity
                ,@ErrorState
                ,@ErrorNumber
                ,@ErrorLine
                ,@ErrorMessage);

        END CATCH;
    END TRY
    BEGIN CATCH  
        THROW;
    END CATCH;
    END
END

/* ====================================================================================================================== */
/* ----------------------------------------- Print out (and output) results: -------------------------------------------- */
/* ====================================================================================================================== */

IF (@PrintOutObjectDefinition = 1)
BEGIN
    SET @PointerDecryptedString = 0;
    SET @PointerBeginOfNewLine = -2; /* (-2) because at first iteration we want to catch the first 2 characters of the first line */

    WHILE @PointerDecryptedString <= LEN(@RealDecryptedObject)
    BEGIN
        IF ((SUBSTRING(@RealDecryptedObject, @PointerDecryptedString + 1, 2) = @CrLf) OR (@PointerDecryptedString = LEN(@RealDecryptedObject)))
        BEGIN
            SELECT @DecryptedLineOfCode

                = REPLACE(REPLACE(
                  SUBSTRING(@RealDecryptedObject, @PointerBeginOfNewLine + LEN(@CrLf), (@PointerDecryptedString - @PointerBeginOfNewLine))
                  , CHAR(13), ''), CHAR(10), '')

            PRINT (@DecryptedLineOfCode);
            INSERT INTO [#ObjectDefinition]
            (
                [DecryptedLineOfCode]
            )
            VALUES (@DecryptedLineOfCode);
            SET @PointerBeginOfNewLine = @PointerDecryptedString;
        END
        SET @PointerDecryptedString = @PointerDecryptedString + 1;
    END;

    SELECT
             [LineId],
             [DecryptedLineOfCode]
    FROM     [#ObjectDefinition]
    ORDER BY [LineId]
END

/*
--Check length of each object:
SELECT LEN(@RealEncryptedObject) AS [Length_Real_Object],
       LEN(@FakePlainTextObject) AS [Length_Fake_Object],
       LEN(@FakeEncryptedObject) AS [Length_Temp_Fake_Object],
       LEN(@RealDecryptedObject) AS [Length_Decrypted_Object]
*/