Initial commit
[mono.git] / mcs / class / referencesource / System.Core / System / Security / Cryptography / CapiSymmetricAlgorithm.cs
1 // ==++==
2 // 
3 //   Copyright (c) Microsoft Corporation.  All rights reserved.
4 // 
5 // ==--==
6
7 using System;
8 using System.Diagnostics;
9 using System.Diagnostics.CodeAnalysis;
10 using System.Runtime.InteropServices;
11 using System.Diagnostics.Contracts;
12 using Microsoft.Win32.SafeHandles;
13
14 namespace System.Security.Cryptography {
15     /// <summary>
16     ///     Flag to indicate if we're doing encryption or decryption
17     /// </summary>
18     internal enum EncryptionMode {
19         Encrypt,
20         Decrypt
21     }
22
23     /// <summary>
24     ///     Implementation of a generic CAPI symmetric encryption algorithm. Concrete SymmetricAlgorithm classes
25     ///     which wrap CAPI implementations can use this class to perform the actual encryption work.
26     /// </summary>
27     internal sealed class CapiSymmetricAlgorithm : ICryptoTransform {
28         private int m_blockSize;
29         private byte[] m_depadBuffer;
30         private EncryptionMode m_encryptionMode;
31         [SecurityCritical]
32         private SafeCapiKeyHandle m_key;
33         private PaddingMode m_paddingMode;
34         [SecurityCritical]
35         private SafeCspHandle m_provider;
36
37         [System.Security.SecurityCritical]
38         [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "Reviewed")]
39         public CapiSymmetricAlgorithm(int blockSize,
40                                       int feedbackSize,
41                                       SafeCspHandle provider,
42                                       SafeCapiKeyHandle key,
43                                       byte[] iv,
44                                       CipherMode cipherMode,
45                                       PaddingMode paddingMode,
46                                       EncryptionMode encryptionMode) {
47             Contract.Requires(0 < blockSize && blockSize % 8 == 0);
48             Contract.Requires(0 <= feedbackSize);
49             Contract.Requires(provider != null && !provider.IsInvalid && !provider.IsClosed);
50             Contract.Requires(key != null && !key.IsInvalid && !key.IsClosed);
51             Contract.Ensures(m_provider != null && !m_provider.IsInvalid && !m_provider.IsClosed);
52             
53             m_blockSize = blockSize;
54             m_encryptionMode = encryptionMode;
55             m_paddingMode = paddingMode;
56             m_provider = provider.Duplicate();
57             m_key = SetupKey(key, ProcessIV(iv, blockSize, cipherMode), cipherMode, feedbackSize);
58         }
59
60         public bool CanReuseTransform {
61             get { return true; }
62         }
63
64         public bool CanTransformMultipleBlocks {
65             get { return true; }
66         }
67
68         //
69         // Note: both input and output block size are in bytes rather than bits
70         //
71
72         public int InputBlockSize {
73             [Pure]
74             get { return m_blockSize / 8; }
75         }
76
77         public int OutputBlockSize {
78             get { return m_blockSize / 8; }
79         }
80
81         [SecuritySafeCritical]
82         public void Dispose() {
83             Contract.Ensures(m_key == null || m_key.IsClosed);
84             Contract.Ensures(m_provider == null || m_provider.IsClosed);
85             Contract.Ensures(m_depadBuffer == null);
86
87             if (m_key != null) {
88                 m_key.Dispose();
89             }
90
91             if (m_provider != null) {
92                 m_provider.Dispose();
93             }
94
95             if (m_depadBuffer != null) {
96                 Array.Clear(m_depadBuffer, 0, m_depadBuffer.Length);
97             }
98
99             return;
100         }
101
102         [SecuritySafeCritical]
103         private int DecryptBlocks(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) {
104             Contract.Requires(m_key != null);
105             Contract.Requires(inputBuffer != null && inputCount <= inputBuffer.Length - inputOffset);
106             Contract.Requires(inputOffset >= 0);
107             Contract.Requires(inputCount > 0 && inputCount % InputBlockSize == 0);
108             Contract.Requires(outputBuffer != null && inputCount <= outputBuffer.Length - outputOffset);
109             Contract.Requires(inputOffset >= 0);
110             Contract.Requires(m_depadBuffer == null || (m_paddingMode != PaddingMode.None && m_paddingMode != PaddingMode.Zeros));
111             Contract.Ensures(Contract.Result<int>() >= 0);
112
113             //
114             // If we're decrypting, it's possible to be called with the last blocks of the data, and then
115             // have TransformFinalBlock called with an empty array. Since we don't know if this is the case,
116             // we won't decrypt the last block of the input until either TransformBlock or
117             // TransformFinalBlock is next called.
118             //
119             // We don't need to do this for PaddingMode.None because there is no padding to strip, and
120             // we also don't do this for PaddingMode.Zeros since there is no way for us to tell if the
121             // zeros at the end of a block are part of the plaintext or the padding.
122             //
123
124             int decryptedBytes = 0;
125             if (m_paddingMode != PaddingMode.None && m_paddingMode != PaddingMode.Zeros) {
126                 // If we have data saved from a previous call, decrypt that into the output first
127                 if (m_depadBuffer != null) {
128                     int depadDecryptLength = RawDecryptBlocks(m_depadBuffer, 0, m_depadBuffer.Length);
129                     Buffer.BlockCopy(m_depadBuffer, 0, outputBuffer, outputOffset, depadDecryptLength);
130                     Array.Clear(m_depadBuffer, 0, m_depadBuffer.Length);
131                     outputOffset += depadDecryptLength;
132                     decryptedBytes += depadDecryptLength;
133                 }
134                 else {
135                     m_depadBuffer = new byte[InputBlockSize];
136                 }
137
138                 // Copy the last block of the input buffer into the depad buffer
139                 Debug.Assert(inputCount >= m_depadBuffer.Length, "inputCount >= m_depadBuffer.Length");
140                 Buffer.BlockCopy(inputBuffer,
141                                  inputOffset + inputCount - m_depadBuffer.Length,
142                                  m_depadBuffer,
143                                  0,
144                                  m_depadBuffer.Length);
145                 inputCount -= m_depadBuffer.Length;
146                 Debug.Assert(inputCount % InputBlockSize == 0, "Did not remove whole blocks for depadding");
147             }
148
149             // CryptDecrypt operates in place, so if after reserving the depad buffer there's still data to decrypt,
150             // make a copy of that in the output buffer to work on.
151             if (inputCount > 0) {
152                 Buffer.BlockCopy(inputBuffer, inputOffset, outputBuffer, outputOffset, inputCount);
153                 decryptedBytes += RawDecryptBlocks(outputBuffer, outputOffset, inputCount);
154             }
155
156             return decryptedBytes;
157         }
158
159         /// <summary>
160         ///     Remove the padding from the last blocks being decrypted
161         /// </summary>
162         private byte[] DepadBlock(byte[] block, int offset, int count) {
163             Contract.Requires(block != null && count >= block.Length - offset);
164             Contract.Requires(0 <= offset);
165             Contract.Requires(0 <= count);
166             Contract.Ensures(Contract.Result<byte[]>() != null && Contract.Result<byte[]>().Length <= block.Length);
167
168             int padBytes = 0;
169
170             // See code:System.Security.Cryptography.CapiSymmetricAlgorithm.PadBlock for a description of the
171             // padding modes.
172             switch (m_paddingMode) {
173                 case PaddingMode.ANSIX923:
174                     padBytes = block[offset + count - 1];
175
176                     // Verify the amount of padding is reasonable
177                     if (padBytes <= 0 || padBytes > InputBlockSize) {
178                         throw new CryptographicException(SR.GetString(SR.Cryptography_InvalidPadding));
179                     }
180
181                     // Verify that all the padding bytes are 0s
182                     for (int i = offset + count - padBytes; i < offset + count - 1; i++) {
183                         if (block[i] != 0) {
184                             throw new CryptographicException(SR.GetString(SR.Cryptography_InvalidPadding));
185                         }
186                     }
187
188                     break;
189
190                 case PaddingMode.ISO10126:
191                     padBytes = block[offset + count - 1];
192
193                     // Verify the amount of padding is reasonable
194                     if (padBytes <= 0 || padBytes > InputBlockSize) {
195                         throw new CryptographicException(SR.GetString(SR.Cryptography_InvalidPadding));
196                     }
197
198                     // Since the padding consists of random bytes, we cannot verify the actual pad bytes themselves
199                     break;
200
201                 case PaddingMode.PKCS7:
202                     padBytes = block[offset + count - 1];
203
204                     // Verify the amount of padding is reasonable
205                     if (padBytes <= 0 || padBytes > InputBlockSize) {
206                         throw new CryptographicException(SR.GetString(SR.Cryptography_InvalidPadding));
207                     }
208
209                     // Verify all the padding bytes match the amount of padding
210                     for (int i = offset + count - padBytes; i < offset + count; i++) {
211                         if (block[i] != padBytes) {
212                             throw new CryptographicException(SR.GetString(SR.Cryptography_InvalidPadding));
213                         }
214                     }
215
216                     break;
217
218                     // We cannot remove Zeros padding because we don't know if the zeros at the end of the block
219                     // belong to the padding or the plaintext itself.
220                 case PaddingMode.Zeros:
221                 case PaddingMode.None:
222                     padBytes = 0;
223                     break;
224
225                 default:
226                     throw new CryptographicException(SR.GetString(SR.Cryptography_UnknownPaddingMode));
227             }
228
229             // Copy everything but the padding to the output
230             byte[] depadded = new byte[count - padBytes];
231             Buffer.BlockCopy(block, offset, depadded, 0, depadded.Length);
232             return depadded;
233         }
234
235         /// <summary>
236         ///     Encrypt blocks of plaintext
237         /// </summary>
238         [SecurityCritical]
239         private int EncryptBlocks(byte[] buffer, int offset, int count) {
240             Contract.Requires(m_key != null);
241             Contract.Requires(buffer != null && count <= buffer.Length - offset);
242             Contract.Requires(offset >= 0);
243             Contract.Requires(count > 0 && count % InputBlockSize == 0);
244             Contract.Ensures(Contract.Result<int>() >= 0);
245
246             //
247             // Do the encryption. Note that CapiSymmetricAlgorithm will do all padding itself since the CLR
248             // supports padding modes that CAPI does not, so we will always tell CAPI that we are not working
249             // with the final block.
250             //
251
252             int dataLength = count;
253             unsafe {
254                 fixed (byte* pData = &buffer[offset]) {
255                     if (!CapiNative.UnsafeNativeMethods.CryptEncrypt(m_key,
256                                                                      SafeCapiHashHandle.InvalidHandle,
257                                                                      false,
258                                                                      0,
259                                                                      new IntPtr(pData),
260                                                                      ref dataLength,
261                                                                      buffer.Length - offset)) {
262                         throw new CryptographicException(Marshal.GetLastWin32Error());
263                     }
264                 }
265             }
266
267             return dataLength;
268         }
269
270         /// <summary>
271         ///     Calculate the padding for a block of data
272         /// </summary>
273         [SecuritySafeCritical]
274         private byte[] PadBlock(byte[] block, int offset, int count) {
275             Contract.Requires(m_provider != null);
276             Contract.Requires(block != null && count <= block.Length - offset);
277             Contract.Requires(0 <= offset);
278             Contract.Requires(0 <= count);
279             Contract.Ensures(Contract.Result<byte[]>() != null && Contract.Result<byte[]>().Length % InputBlockSize == 0);
280
281             byte[] result = null;
282             int padBytes = InputBlockSize - (count % InputBlockSize);
283             
284             switch (m_paddingMode) {
285                     // ANSI padding fills the blocks with zeros and adds the total number of padding bytes as
286                     // the last pad byte, adding an extra block if the last block is complete.
287                     //
288                     // x 00 00 00 00 00 00 07
289                 case PaddingMode.ANSIX923:
290                     result = new byte[count + padBytes];
291                     Buffer.BlockCopy(block, 0, result, 0, count);
292                     result[result.Length - 1] = (byte)padBytes;
293                     break;
294
295                     // ISO padding fills the blocks up with random bytes and adds the total number of padding
296                     // bytes as the last pad byte, adding an extra block if the last block is complete.
297                     //
298                     // xx rr rr rr rr rr rr 07
299                 case PaddingMode.ISO10126:
300                     result = new byte[count + padBytes];
301                     
302                     CapiNative.UnsafeNativeMethods.CryptGenRandom(m_provider, result.Length - 1, result);
303                     Buffer.BlockCopy(block, 0, result, 0, count);
304                     result[result.Length - 1] = (byte)padBytes;
305                     break;
306
307                     // No padding requires that the input already be a multiple of the block size
308                 case PaddingMode.None:
309                     if (count % InputBlockSize != 0) {
310                         throw new CryptographicException(SR.GetString(SR.Cryptography_PartialBlock));
311                     }
312
313                     result = new byte[count];
314                     Buffer.BlockCopy(block, offset, result, 0, result.Length);
315                     break;
316
317                     // PKCS padding fills the blocks up with bytes containing the total number of padding bytes
318                     // used, adding an extra block if the last block is complete.
319                     //
320                     // xx xx 06 06 06 06 06 06
321                 case PaddingMode.PKCS7:
322                     result = new byte[count + padBytes];
323                     Buffer.BlockCopy(block, offset, result, 0, count);
324
325                     for (int i = count; i < result.Length; i++) {
326                         result[i] = (byte)padBytes;
327                     }
328                     break;
329
330                     // Zeros padding fills the last partial block with zeros, and does not add a new block to
331                     // the end if the last block is already complete.
332                     //
333                     //  xx 00 00 00 00 00 00 00
334                 case PaddingMode.Zeros:
335                     if (padBytes == InputBlockSize) {
336                         padBytes = 0;
337                     }
338
339                     result = new byte[count + padBytes];
340                     Buffer.BlockCopy(block, offset, result, 0, count);
341                     break;
342
343                 default:
344                     throw new CryptographicException(SR.GetString(SR.Cryptography_UnknownPaddingMode));
345             }
346
347             return result;
348         }
349
350         /// <summary>
351         ///     Validate and transform the user's IV into one that we will pass on to CAPI
352         ///
353         ///     If we have an IV, make a copy of it so that it doesn't get modified while we're using it. If
354         ///     not, and we're not in ECB mode then throw an error, since we cannot decrypt without the IV, and
355         ///     generating a random IV to encrypt with would lead to data which is not decryptable.
356         ///
357         ///     For compatibility with v1.x, we accept IVs which are longer than the block size, and truncate
358         ///     them back.  We will reject an IV which is smaller than the block size however.
359         /// </summary>
360         private static byte[] ProcessIV(byte[] iv, int blockSize, CipherMode cipherMode) {
361             Contract.Requires(blockSize % 8 == 0);
362             Contract.Ensures(cipherMode == CipherMode.ECB ||
363                              (Contract.Result<byte[]>() != null && Contract.Result<byte[]>().Length == blockSize / 8));
364
365             byte[] realIV = null;
366
367             if (iv != null) {
368                 if (blockSize / 8 <= iv.Length) {
369                     realIV = new byte[blockSize / 8];
370                     Buffer.BlockCopy(iv, 0, realIV, 0, realIV.Length);
371                 }
372                 else {
373                     throw new CryptographicException(SR.GetString(SR.Cryptography_InvalidIVSize));
374                 }
375             }
376             else if (cipherMode != CipherMode.ECB) {
377                 throw new CryptographicException(SR.GetString(SR.Cryptography_MissingIV));
378             }
379
380             return realIV;
381         }
382
383         /// <summary>
384         ///     Do a direct decryption of the ciphertext blocks. This method should not be called from anywhere
385         ///     but DecryptBlocks or TransformFinalBlock since it does not account for the depadding buffer and
386         ///     direct use could lead to incorrect decryption values.
387         /// </summary>
388         [SecurityCritical]
389         private int RawDecryptBlocks(byte[] buffer, int offset, int count) {
390             Contract.Requires(m_key != null);
391             Contract.Requires(buffer != null && count <= buffer.Length - offset);
392             Contract.Requires(offset >= 0);
393             Contract.Requires(count > 0 && count % InputBlockSize == 0);
394             Contract.Ensures(Contract.Result<int>() >= 0);
395
396             //
397             // Do the decryption. Note that CapiSymmetricAlgorithm will do all padding itself since the CLR
398             // supports padding modes that CAPI does not, so we will always tell CAPI that we are not working
399             // with the final block.
400             //
401
402             int dataLength = count;
403             unsafe {
404                 fixed (byte* pData = &buffer[offset]) {
405                     if (!CapiNative.UnsafeNativeMethods.CryptDecrypt(m_key,
406                                                                      SafeCapiHashHandle.InvalidHandle,
407                                                                      false,
408                                                                      0,
409                                                                      new IntPtr(pData),
410                                                                      ref dataLength)) {
411                         throw new CryptographicException(Marshal.GetLastWin32Error());
412                     }
413                 }
414             }
415
416             return dataLength;
417         }
418
419         /// <summary>
420         ///     Reset the state of the algorithm so that it can begin processing a new message
421         /// </summary>
422         [SecuritySafeCritical]
423         private void Reset() {
424             Contract.Requires(m_key != null);
425             Contract.Ensures(m_depadBuffer == null);
426
427             //
428             // CryptEncrypt / CryptDecrypt must be called with the Final parameter set to true so that
429             // their internal state is reset. Since we do all padding by hand, this isn't done by
430             // TransformFinalBlock so is done on an empty buffer here.
431             //
432
433             byte[] buffer = new byte[OutputBlockSize];
434             int resetSize = 0;
435             unsafe {
436                 fixed (byte* pBuffer = buffer) {
437                     if (m_encryptionMode == EncryptionMode.Encrypt) {
438                         CapiNative.UnsafeNativeMethods.CryptEncrypt(m_key,
439                                                                     SafeCapiHashHandle.InvalidHandle,
440                                                                     true,
441                                                                     0,
442                                                                     new IntPtr(pBuffer),
443                                                                     ref resetSize,
444                                                                     buffer.Length);
445                     }
446                     else {
447                         CapiNative.UnsafeNativeMethods.CryptDecrypt(m_key,
448                                                                     SafeCapiHashHandle.InvalidHandle,
449                                                                     true,
450                                                                     0,
451                                                                     new IntPtr(pBuffer),
452                                                                     ref resetSize);
453                     }
454                 }
455             }
456
457             // Also erase the depadding buffer so we don't cross data from the previous message into this one
458             if (m_depadBuffer != null) {
459                 Array.Clear(m_depadBuffer, 0, m_depadBuffer.Length);
460                 m_depadBuffer = null;
461             }
462         }
463
464         /// <summary>
465         ///     Encrypt or decrypt a single block of data
466         /// </summary>
467         [SecuritySafeCritical]
468         public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) {
469             Contract.Ensures(Contract.Result<int>() >= 0);
470
471             if (inputBuffer == null) {
472                 throw new ArgumentNullException("inputBuffer");
473             }
474             if (inputOffset < 0) {
475                 throw new ArgumentOutOfRangeException("inputOffset");
476             }
477             if (inputCount <= 0) {
478                 throw new ArgumentOutOfRangeException("inputCount");
479             }
480             if (inputCount % InputBlockSize != 0) {
481                 throw new ArgumentOutOfRangeException("inputCount", SR.GetString(SR.Cryptography_MustTransformWholeBlock));
482             }
483             if (inputCount > inputBuffer.Length - inputOffset) {
484                 throw new ArgumentOutOfRangeException("inputCount", SR.GetString(SR.Cryptography_TransformBeyondEndOfBuffer));
485             }
486             if (outputBuffer == null) {
487                 throw new ArgumentNullException("outputBuffer");
488             }
489             if (inputCount > outputBuffer.Length - outputOffset) {
490                 throw new ArgumentOutOfRangeException("outputOffset", SR.GetString(SR.Cryptography_TransformBeyondEndOfBuffer));
491             }
492
493             if (m_encryptionMode == EncryptionMode.Encrypt) {
494                 // CryptEncrypt operates in place, so make a copy of the original data in the output buffer for
495                 // it to work on.
496                 Buffer.BlockCopy(inputBuffer, inputOffset, outputBuffer, outputOffset, inputCount);
497                 return EncryptBlocks(outputBuffer, outputOffset, inputCount);
498             }
499             else {
500                 return DecryptBlocks(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
501             }
502         }
503
504         /// <summary>
505         ///     Encrypt or decrypt the last block of data in the current message
506         /// </summary>
507         [SecuritySafeCritical]
508         public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) {
509             Contract.Ensures(Contract.Result<byte[]>() != null);
510
511             if (inputBuffer == null) {
512                 throw new ArgumentNullException("inputBuffer");
513             }
514             if (inputOffset < 0) {
515                 throw new ArgumentOutOfRangeException("inputOffset");
516             }
517             if (inputCount < 0) {
518                 throw new ArgumentOutOfRangeException("inputCount");
519             }
520             if (inputCount > inputBuffer.Length - inputOffset) {
521                 throw new ArgumentOutOfRangeException("inputCount", SR.GetString(SR.Cryptography_TransformBeyondEndOfBuffer));
522             }
523
524             byte[] outputData = null;
525
526             if (m_encryptionMode == EncryptionMode.Encrypt) {
527                 // If we're encrypting, we need to pad the last block before encrypting it
528                 outputData = PadBlock(inputBuffer, inputOffset, inputCount);
529                 if (outputData.Length > 0) {
530                     EncryptBlocks(outputData, 0, outputData.Length);
531                 }
532             }
533             else {
534                 // We can't complete decryption on a partial block
535                 if (inputCount % InputBlockSize != 0) {
536                     throw new CryptographicException(SR.GetString(SR.Cryptography_PartialBlock));
537                 }
538
539                 //
540                 // If we have a depad buffer, copy that into the decryption buffer followed by the input data.
541                 // Otherwise the decryption buffer is just the input data.
542                 //
543
544                 byte[] ciphertext = null;
545
546                 if (m_depadBuffer == null) {
547                     ciphertext = new byte[inputCount];
548                     Buffer.BlockCopy(inputBuffer, inputOffset, ciphertext, 0, inputCount);
549                 }
550                 else {
551                     ciphertext = new byte[m_depadBuffer.Length + inputCount];
552                     Buffer.BlockCopy(m_depadBuffer, 0, ciphertext, 0, m_depadBuffer.Length);
553                     Buffer.BlockCopy(inputBuffer, inputOffset, ciphertext, m_depadBuffer.Length, inputCount);
554                 }
555
556                 // Decrypt the data, then strip the padding to get the final decrypted data.
557                 if (ciphertext.Length > 0) {
558                     int decryptedBytes = RawDecryptBlocks(ciphertext, 0, ciphertext.Length);
559                     outputData = DepadBlock(ciphertext, 0, decryptedBytes);
560                 }
561                 else {
562                     outputData = new byte[0];
563                 }
564             }
565
566             Reset();            
567             return outputData;
568         }
569
570         /// <summary>
571         ///     Prepare the cryptographic key for use in the encryption / decryption operation
572         /// </summary>
573         [System.Security.SecurityCritical]
574         private static SafeCapiKeyHandle SetupKey(SafeCapiKeyHandle key, byte[] iv, CipherMode cipherMode, int feedbackSize) {
575             Contract.Requires(key != null);
576             Contract.Requires(cipherMode == CipherMode.ECB || iv != null);
577             Contract.Requires(0 <= feedbackSize);
578             Contract.Ensures(Contract.Result<SafeCapiKeyHandle>() != null &&
579                              !Contract.Result<SafeCapiKeyHandle>().IsInvalid &&
580                              !Contract.Result<SafeCapiKeyHandle>().IsClosed);
581
582             // Make a copy of the key so that we don't modify the properties of the caller's copy
583             SafeCapiKeyHandle encryptionKey = key.Duplicate();
584
585             // Setup the cipher mode first
586             CapiNative.SetKeyParameter(encryptionKey, CapiNative.KeyParameter.Mode, (int)cipherMode);
587
588             // If we're not in ECB mode then setup the IV
589             if (cipherMode != CipherMode.ECB) {
590                 CapiNative.SetKeyParameter(encryptionKey, CapiNative.KeyParameter.IV, iv);
591             }
592
593             // OFB and CFB require a feedback loop size
594             if (cipherMode == CipherMode.CFB || cipherMode == CipherMode.OFB) {
595                 CapiNative.SetKeyParameter(encryptionKey, CapiNative.KeyParameter.ModeBits, feedbackSize);
596             }
597
598             return encryptionKey;
599         }
600     }
601 }