//#define Trace
// WinZipAes.cs
// ------------------------------------------------------------------
//
// Copyright (c) 2009-2011 Dino Chiesa.
// All rights reserved.
//
// This code module is part of DotNetZip, a zipfile class library.
//
// ------------------------------------------------------------------
//
// This code is licensed under the Microsoft Public License.
// See the file License.txt for the license details.
// More info on: http://dotnetzip.codeplex.com
//
// ------------------------------------------------------------------
//
// last saved (in emacs):
// Time-stamp: <2011-July-12 13:42:06>
//
// ------------------------------------------------------------------
//
// This module defines the classes for dealing with WinZip's AES encryption,
// according to the specifications for the format available on WinZip's website.
//
// Created: January 2009
//
// ------------------------------------------------------------------
using System;
using System.IO;
using System.Collections.Generic;
using System.Security.Cryptography;
#if AESCRYPTO
namespace OfficeOpenXml.Packaging.Ionic.Zip
{
///
/// This is a helper class supporting WinZip AES encryption.
/// This class is intended for use only by the DotNetZip library.
///
///
///
/// Most uses of the DotNetZip library will not involve direct calls into
/// the WinZipAesCrypto class. Instead, the WinZipAesCrypto class is
/// instantiated and used by the ZipEntry() class when WinZip AES
/// encryption or decryption on an entry is employed.
///
internal class WinZipAesCrypto
{
internal byte[] _Salt;
internal byte[] _providedPv;
internal byte[] _generatedPv;
internal int _KeyStrengthInBits;
private byte[] _MacInitializationVector;
private byte[] _StoredMac;
private byte[] _keyBytes;
private Int16 PasswordVerificationStored;
private Int16 PasswordVerificationGenerated;
private int Rfc2898KeygenIterations = 1000;
private string _Password;
private bool _cryptoGenerated ;
private WinZipAesCrypto(string password, int KeyStrengthInBits)
{
_Password = password;
_KeyStrengthInBits = KeyStrengthInBits;
}
public static WinZipAesCrypto Generate(string password, int KeyStrengthInBits)
{
WinZipAesCrypto c = new WinZipAesCrypto(password, KeyStrengthInBits);
int saltSizeInBytes = c._KeyStrengthInBytes / 2;
c._Salt = new byte[saltSizeInBytes];
Random rnd = new Random();
rnd.NextBytes(c._Salt);
return c;
}
public static WinZipAesCrypto ReadFromStream(string password, int KeyStrengthInBits, Stream s)
{
// from http://www.winzip.com/aes_info.htm
//
// Size(bytes) Content
// -----------------------------------
// Variable Salt value
// 2 Password verification value
// Variable Encrypted file data
// 10 Authentication code
//
// ZipEntry.CompressedSize represents the size of all of those elements.
// salt size varies with key length:
// 128 bit key => 8 bytes salt
// 192 bits => 12 bytes salt
// 256 bits => 16 bytes salt
WinZipAesCrypto c = new WinZipAesCrypto(password, KeyStrengthInBits);
int saltSizeInBytes = c._KeyStrengthInBytes / 2;
c._Salt = new byte[saltSizeInBytes];
c._providedPv = new byte[2];
s.Read(c._Salt, 0, c._Salt.Length);
s.Read(c._providedPv, 0, c._providedPv.Length);
c.PasswordVerificationStored = (Int16)(c._providedPv[0] + c._providedPv[1] * 256);
if (password != null)
{
c.PasswordVerificationGenerated = (Int16)(c.GeneratedPV[0] + c.GeneratedPV[1] * 256);
if (c.PasswordVerificationGenerated != c.PasswordVerificationStored)
throw new BadPasswordException("bad password");
}
return c;
}
public byte[] GeneratedPV
{
get
{
if (!_cryptoGenerated) _GenerateCryptoBytes();
return _generatedPv;
}
}
public byte[] Salt
{
get
{
return _Salt;
}
}
private int _KeyStrengthInBytes
{
get
{
return _KeyStrengthInBits / 8;
}
}
public int SizeOfEncryptionMetadata
{
get
{
// 10 bytes after, (n-10) before the compressed data
return _KeyStrengthInBytes / 2 + 10 + 2;
}
}
public string Password
{
set
{
_Password = value;
if (_Password != null)
{
PasswordVerificationGenerated = (Int16)(GeneratedPV[0] + GeneratedPV[1] * 256);
if (PasswordVerificationGenerated != PasswordVerificationStored)
throw new Ionic.Zip.BadPasswordException();
}
}
private get
{
return _Password;
}
}
private void _GenerateCryptoBytes()
{
//Console.WriteLine(" provided password: '{0}'", _Password);
System.Security.Cryptography.Rfc2898DeriveBytes rfc2898 =
new System.Security.Cryptography.Rfc2898DeriveBytes(_Password, Salt, Rfc2898KeygenIterations);
_keyBytes = rfc2898.GetBytes(_KeyStrengthInBytes); // 16 or 24 or 32 ???
_MacInitializationVector = rfc2898.GetBytes(_KeyStrengthInBytes);
_generatedPv = rfc2898.GetBytes(2);
_cryptoGenerated = true;
}
public byte[] KeyBytes
{
get
{
if (!_cryptoGenerated) _GenerateCryptoBytes();
return _keyBytes;
}
}
public byte[] MacIv
{
get
{
if (!_cryptoGenerated) _GenerateCryptoBytes();
return _MacInitializationVector;
}
}
public byte[] CalculatedMac;
public void ReadAndVerifyMac(System.IO.Stream s)
{
bool invalid = false;
// read integrityCheckVector.
// caller must ensure that the file pointer is in the right spot!
_StoredMac = new byte[10]; // aka "authentication code"
s.Read(_StoredMac, 0, _StoredMac.Length);
if (_StoredMac.Length != CalculatedMac.Length)
invalid = true;
if (!invalid)
{
for (int i = 0; i < _StoredMac.Length; i++)
{
if (_StoredMac[i] != CalculatedMac[i])
invalid = true;
}
}
if (invalid)
throw new Ionic.Zip.BadStateException("The MAC does not match.");
}
}
#region DONT_COMPILE_BUT_KEEP_FOR_POTENTIAL_FUTURE_USE
#if NO
internal class Util
{
private static void _Format(System.Text.StringBuilder sb1,
byte[] b,
int offset,
int length)
{
System.Text.StringBuilder sb2 = new System.Text.StringBuilder();
sb1.Append("0000 ");
int i;
for (i = 0; i < length; i++)
{
int x = offset+i;
if (i != 0 && i % 16 == 0)
{
sb1.Append(" ")
.Append(sb2)
.Append("\n")
.Append(String.Format("{0:X4} ", i));
sb2.Remove(0,sb2.Length);
}
sb1.Append(System.String.Format("{0:X2} ", b[x]));
if (b[x] >=32 && b[x] <= 126)
sb2.Append((char)b[x]);
else
sb2.Append(".");
}
if (sb2.Length > 0)
{
sb1.Append(new String(' ', ((16 - i%16) * 3) + 4))
.Append(sb2);
}
}
internal static string FormatByteArray(byte[] b, int limit)
{
System.Text.StringBuilder sb1 = new System.Text.StringBuilder();
if ((limit * 2 > b.Length) || limit == 0)
{
_Format(sb1, b, 0, b.Length);
}
else
{
// first N bytes of the buffer
_Format(sb1, b, 0, limit);
if (b.Length > limit * 2)
sb1.Append(String.Format("\n ...({0} other bytes here)....\n", b.Length - limit * 2));
// last N bytes of the buffer
_Format(sb1, b, b.Length - limit, limit);
}
return sb1.ToString();
}
internal static string FormatByteArray(byte[] b)
{
return FormatByteArray(b, 0);
}
}
#endif
#endregion
///
/// A stream that encrypts as it writes, or decrypts as it reads. The
/// Crypto is AES in CTR (counter) mode, which is compatible with the AES
/// encryption employed by WinZip 12.0.
///
///
///
/// The AES/CTR encryption protocol used by WinZip works like this:
///
/// - start with a counter, initialized to zero.
///
/// - to encrypt, take the data by 16-byte blocks. For each block:
/// - apply the transform to the counter
/// - increement the counter
/// - XOR the result of the transform with the plaintext to
/// get the ciphertext.
/// - compute the mac on the encrypted bytes
/// - when finished with all blocks, store the computed MAC.
///
/// - to decrypt, take the data by 16-byte blocks. For each block:
/// - compute the mac on the encrypted bytes,
/// - apply the transform to the counter
/// - increement the counter
/// - XOR the result of the transform with the ciphertext to
/// get the plaintext.
/// - when finished with all blocks, compare the computed MAC against
/// the stored MAC
///
///
///
//
internal class WinZipAesCipherStream : Stream
{
private WinZipAesCrypto _params;
private System.IO.Stream _s;
private CryptoMode _mode;
private int _nonce;
private bool _finalBlock;
internal HMACSHA1 _mac;
// Use RijndaelManaged from .NET 2.0.
// AesManaged came in .NET 3.5, but we want to limit
// dependency to .NET 2.0. AES is just a restricted form
// of Rijndael (fixed block size of 128, some crypto modes not supported).
internal RijndaelManaged _aesCipher;
internal ICryptoTransform _xform;
private const int BLOCK_SIZE_IN_BYTES = 16;
private byte[] counter = new byte[BLOCK_SIZE_IN_BYTES];
private byte[] counterOut = new byte[BLOCK_SIZE_IN_BYTES];
// I've had a problem when wrapping a WinZipAesCipherStream inside
// a DeflateStream. Calling Read() on the DeflateStream results in
// a Read() on the WinZipAesCipherStream, but the buffer is larger
// than the total size of the encrypted data, and larger than the
// initial Read() on the DeflateStream! When the encrypted
// bytestream is embedded within a larger stream (As in a zip
// archive), the Read() doesn't fail with EOF. This causes bad
// data to be returned, and it messes up the MAC.
// This field is used to provide a hard-stop to the size of
// data that can be read from the stream. In Read(), if the buffer or
// read request goes beyond the stop, we truncate it.
private long _length;
private long _totalBytesXferred;
private byte[] _PendingWriteBlock;
private int _pendingCount;
private byte[] _iobuf;
///
/// The constructor.
///
/// The underlying stream
/// To either encrypt or decrypt.
/// The pre-initialized WinZipAesCrypto object.
/// The maximum number of bytes to read from the stream.
internal WinZipAesCipherStream(System.IO.Stream s, WinZipAesCrypto cryptoParams, long length, CryptoMode mode)
: this(s, cryptoParams, mode)
{
// don't read beyond this limit!
_length = length;
//Console.WriteLine("max length of AES stream: {0}", _length);
}
#if WANT_TRACE
Stream untransformed;
String traceFileUntransformed;
Stream transformed;
String traceFileTransformed;
#endif
internal WinZipAesCipherStream(System.IO.Stream s, WinZipAesCrypto cryptoParams, CryptoMode mode)
: base()
{
TraceOutput("-------------------------------------------------------");
TraceOutput("Create {0:X8}", this.GetHashCode());
_params = cryptoParams;
_s = s;
_mode = mode;
_nonce = 1;
if (_params == null)
throw new BadPasswordException("Supply a password to use AES encryption.");
int keySizeInBits = _params.KeyBytes.Length * 8;
if (keySizeInBits != 256 && keySizeInBits != 128 && keySizeInBits != 192)
throw new ArgumentOutOfRangeException("keysize",
"size of key must be 128, 192, or 256");
_mac = new HMACSHA1(_params.MacIv);
_aesCipher = new System.Security.Cryptography.RijndaelManaged();
_aesCipher.BlockSize = 128;
_aesCipher.KeySize = keySizeInBits; // 128, 192, 256
_aesCipher.Mode = CipherMode.ECB;
_aesCipher.Padding = PaddingMode.None;
byte[] iv = new byte[BLOCK_SIZE_IN_BYTES]; // all zeroes
// Create an ENCRYPTOR, regardless whether doing decryption or encryption.
// It is reflexive.
_xform = _aesCipher.CreateEncryptor(_params.KeyBytes, iv);
if (_mode == CryptoMode.Encrypt)
{
_iobuf = new byte[2048];
_PendingWriteBlock = new byte[BLOCK_SIZE_IN_BYTES];
}
#if WANT_TRACE
traceFileUntransformed = "unpack\\WinZipAesCipherStream.trace.untransformed.out";
traceFileTransformed = "unpack\\WinZipAesCipherStream.trace.transformed.out";
untransformed = System.IO.File.Create(traceFileUntransformed);
transformed = System.IO.File.Create(traceFileTransformed);
#endif
}
private void XorInPlace(byte[] buffer, int offset, int count)
{
for (int i = 0; i < count; i++)
{
buffer[offset + i] = (byte)(counterOut[i] ^ buffer[offset + i]);
}
}
private void WriteTransformOneBlock(byte[] buffer, int offset)
{
System.Array.Copy(BitConverter.GetBytes(_nonce++), 0, counter, 0, 4);
_xform.TransformBlock(counter,
0,
BLOCK_SIZE_IN_BYTES,
counterOut,
0);
XorInPlace(buffer, offset, BLOCK_SIZE_IN_BYTES);
_mac.TransformBlock(buffer, offset, BLOCK_SIZE_IN_BYTES, null, 0);
}
private void WriteTransformBlocks(byte[] buffer, int offset, int count)
{
int posn = offset;
int last = count + offset;
while (posn < buffer.Length && posn < last)
{
WriteTransformOneBlock (buffer, posn);
posn += BLOCK_SIZE_IN_BYTES;
}
}
private void WriteTransformFinalBlock()
{
if (_pendingCount == 0)
throw new InvalidOperationException("No bytes available.");
if (_finalBlock)
throw new InvalidOperationException("The final block has already been transformed.");
System.Array.Copy(BitConverter.GetBytes(_nonce++), 0, counter, 0, 4);
counterOut = _xform.TransformFinalBlock(counter,
0,
BLOCK_SIZE_IN_BYTES);
XorInPlace(_PendingWriteBlock, 0, _pendingCount);
_mac.TransformFinalBlock(_PendingWriteBlock, 0, _pendingCount);
_finalBlock = true;
}
private int ReadTransformOneBlock(byte[] buffer, int offset, int last)
{
if (_finalBlock)
throw new NotSupportedException();
int bytesRemaining = last - offset;
int bytesToRead = (bytesRemaining > BLOCK_SIZE_IN_BYTES)
? BLOCK_SIZE_IN_BYTES
: bytesRemaining;
// update the counter
System.Array.Copy(BitConverter.GetBytes(_nonce++), 0, counter, 0, 4);
// Determine if this is the final block
if ((bytesToRead == bytesRemaining) &&
(_length > 0) &&
(_totalBytesXferred + last == _length))
{
_mac.TransformFinalBlock(buffer, offset, bytesToRead);
counterOut = _xform.TransformFinalBlock(counter,
0,
BLOCK_SIZE_IN_BYTES);
_finalBlock = true;
}
else
{
_mac.TransformBlock(buffer, offset, bytesToRead, null, 0);
_xform.TransformBlock(counter,
0, // offset
BLOCK_SIZE_IN_BYTES,
counterOut,
0); // offset
}
XorInPlace(buffer, offset, bytesToRead);
return bytesToRead;
}
private void ReadTransformBlocks(byte[] buffer, int offset, int count)
{
int posn = offset;
int last = count + offset;
while (posn < buffer.Length && posn < last )
{
int n = ReadTransformOneBlock (buffer, posn, last);
posn += n;
}
}
public override int Read(byte[] buffer, int offset, int count)
{
if (_mode == CryptoMode.Encrypt)
throw new NotSupportedException();
if (buffer == null)
throw new ArgumentNullException("buffer");
if (offset < 0)
throw new ArgumentOutOfRangeException("offset",
"Must not be less than zero.");
if (count < 0)
throw new ArgumentOutOfRangeException("count",
"Must not be less than zero.");
if (buffer.Length < offset + count)
throw new ArgumentException("The buffer is too small");
// When I wrap a WinZipAesStream in a DeflateStream, the
// DeflateStream asks its captive to read 4k blocks, even if the
// encrypted bytestream is smaller than that. This is a way to
// limit the number of bytes read.
int bytesToRead = count;
if (_totalBytesXferred >= _length)
{
return 0; // EOF
}
long bytesRemaining = _length - _totalBytesXferred;
if (bytesRemaining < count) bytesToRead = (int)bytesRemaining;
int n = _s.Read(buffer, offset, bytesToRead);
#if WANT_TRACE
untransformed.Write(buffer, offset, bytesToRead);
#endif
ReadTransformBlocks(buffer, offset, bytesToRead);
#if WANT_TRACE
transformed.Write(buffer, offset, bytesToRead);
#endif
_totalBytesXferred += n;
return n;
}
///
/// Returns the final HMAC-SHA1-80 for the data that was encrypted.
///
public byte[] FinalAuthentication
{
get
{
if (!_finalBlock)
{
// special-case zero-byte files
if ( _totalBytesXferred != 0)
throw new BadStateException("The final hash has not been computed.");
// Must call ComputeHash on an empty byte array when no data
// has run through the MAC.
byte[] b = { };
_mac.ComputeHash(b);
// fall through
}
byte[] macBytes10 = new byte[10];
System.Array.Copy(_mac.Hash, 0, macBytes10, 0, 10);
return macBytes10;
}
}
public override void Write(byte[] buffer, int offset, int count)
{
if (_finalBlock)
throw new InvalidOperationException("The final block has already been transformed.");
if (_mode == CryptoMode.Decrypt)
throw new NotSupportedException();
if (buffer == null)
throw new ArgumentNullException("buffer");
if (offset < 0)
throw new ArgumentOutOfRangeException("offset",
"Must not be less than zero.");
if (count < 0)
throw new ArgumentOutOfRangeException("count",
"Must not be less than zero.");
if (buffer.Length < offset + count)
throw new ArgumentException("The offset and count are too large");
if (count == 0)
return;
TraceOutput("Write off({0}) count({1})", offset, count);
#if WANT_TRACE
untransformed.Write(buffer, offset, count);
#endif
// For proper AES encryption, an AES encryptor application calls
// TransformBlock repeatedly for all 16-byte blocks except the
// last. For the last block, it then calls TransformFinalBlock().
//
// This class is a stream that encrypts via Write(). But, it's not
// possible to recognize which are the "last" bytes from within the call
// to Write(). The caller can call Write() several times in succession,
// with varying buffers. This class only "knows" that the last bytes
// have been written when the app calls Close().
//
// Therefore, this class buffers writes: After completion every Write(),
// a 16-byte "pending" block (_PendingWriteBlock) must hold between 1
// and 16 bytes, which will be used in TransformFinalBlock if the app
// calls Close() immediately thereafter. Also, every write must
// transform any pending bytes, before transforming the data passed in
// to the current call.
//
// In operation, after the first call to Write() and before the call to
// Close(), one full or partial block of bytes is always available,
// pending. At time of Close(), this class calls
// WriteTransformFinalBlock() to flush the pending bytes.
//
// This approach works whether the caller writes in odd-sized batches,
// for example 5000 bytes, or in batches that are neat multiples of the
// blocksize (16).
//
// Logicaly, what we do is this:
//
// 1. if there are fewer than 16 bytes (pending + current), then
// just copy them into th pending buffer and return.
//
// 2. there are more than 16 bytes to write. So, take the leading slice
// of bytes from the current buffer, enough to fill the pending
// buffer. Transform the pending block, and write it out.
//
// 3. Take the trailing slice of bytes (a full block or a partial block),
// and copy it to the pending block for next time.
//
// 4. transform and write all the other blocks, the middle slice.
//
// There are 16 or fewer bytes, so just buffer the bytes.
if (count + _pendingCount <= BLOCK_SIZE_IN_BYTES)
{
Buffer.BlockCopy(buffer,
offset,
_PendingWriteBlock,
_pendingCount,
count);
_pendingCount += count;
// At this point, _PendingWriteBlock contains up to
// BLOCK_SIZE_IN_BYTES bytes, and _pendingCount ranges from 0 to
// BLOCK_SIZE_IN_BYTES. We don't want to xform+write them yet,
// because this may have been the last block. The last block gets
// written at Close().
return;
}
// We know there are at least 17 bytes, counting those in the current
// buffer, along with the (possibly empty) pending block.
int bytesRemaining = count;
int curOffset = offset;
// workitem 12815
//
// xform chunkwise ... Cannot transform in place using the original
// buffer because that is user-maintained.
if (_pendingCount != 0)
{
// We have more than one block of data to write, therefore it is safe
// to xform+write.
int fillCount = BLOCK_SIZE_IN_BYTES - _pendingCount;
// fillCount is possibly zero here. That happens when the pending
// buffer held 16 bytes (one complete block) before this call to
// Write.
if (fillCount > 0)
{
Buffer.BlockCopy(buffer,
offset,
_PendingWriteBlock,
_pendingCount,
fillCount);
// adjust counts:
bytesRemaining -= fillCount;
curOffset += fillCount;
}
// xform and write:
WriteTransformOneBlock(_PendingWriteBlock, 0);
_s.Write(_PendingWriteBlock, 0, BLOCK_SIZE_IN_BYTES);
_totalBytesXferred += BLOCK_SIZE_IN_BYTES;
_pendingCount = 0;
}
// At this point _PendingWriteBlock is empty, and bytesRemaining is
// always greater than 0.
// Now, xform N blocks, where N = floor((bytesRemaining-1)/16). If
// writing 32 bytes, then xform 1 block, and stage the remaining 16. If
// writing 10037 bytes, xform 627 blocks of 16 bytes, then stage the
// remaining 5 bytes.
int blocksToXform = (bytesRemaining-1)/BLOCK_SIZE_IN_BYTES;
_pendingCount = bytesRemaining - (blocksToXform * BLOCK_SIZE_IN_BYTES);
// _pendingCount is ALWAYS between 1 and 16.
// Put the last _pendingCount bytes into the pending block.
Buffer.BlockCopy(buffer,
curOffset + bytesRemaining - _pendingCount,
_PendingWriteBlock,
0,
_pendingCount);
bytesRemaining -= _pendingCount;
_totalBytesXferred += bytesRemaining; // will be true after the loop
// now, transform all the full blocks preceding that.
// bytesRemaining is always a multiple of 16 .
if (blocksToXform > 0)
{
do
{
int c = _iobuf.Length;
if (c > bytesRemaining) c = bytesRemaining;
Buffer.BlockCopy(buffer,
curOffset,
_iobuf,
0,
c);
WriteTransformBlocks(_iobuf, 0, c);
_s.Write(_iobuf, 0, c);
bytesRemaining -= c;
curOffset += c;
} while(bytesRemaining > 0);
}
}
///
/// Close the stream.
///
public override void Close()
{
TraceOutput("Close {0:X8}", this.GetHashCode());
// In the degenerate case, no bytes have been written to the
// stream at all. Need to check here, and NOT emit the
// final block if Write has not been called.
if (_pendingCount > 0)
{
WriteTransformFinalBlock();
_s.Write(_PendingWriteBlock, 0, _pendingCount);
_totalBytesXferred += _pendingCount;
_pendingCount = 0;
}
_s.Close();
#if WANT_TRACE
untransformed.Close();
transformed.Close();
Console.WriteLine("\nuntransformed bytestream is in {0}", traceFileUntransformed);
Console.WriteLine("\ntransformed bytestream is in {0}", traceFileTransformed);
#endif
TraceOutput("-------------------------------------------------------");
}
///
/// Returns true if the stream can be read.
///
public override bool CanRead
{
get
{
if (_mode != CryptoMode.Decrypt) return false;
return true;
}
}
///
/// Always returns false.
///
public override bool CanSeek
{
get { return false; }
}
///
/// Returns true if the CryptoMode is Encrypt.
///
public override bool CanWrite
{
get { return (_mode == CryptoMode.Encrypt); }
}
///
/// Flush the content in the stream.
///
public override void Flush()
{
_s.Flush();
}
///
/// Getting this property throws a NotImplementedException.
///
public override long Length
{
get { throw new NotImplementedException(); }
}
///
/// Getting or Setting this property throws a NotImplementedException.
///
public override long Position
{
get { throw new NotImplementedException(); }
set { throw new NotImplementedException(); }
}
///
/// This method throws a NotImplementedException.
///
public override long Seek(long offset, System.IO.SeekOrigin origin)
{
throw new NotImplementedException();
}
///
/// This method throws a NotImplementedException.
///
public override void SetLength(long value)
{
throw new NotImplementedException();
}
[System.Diagnostics.ConditionalAttribute("Trace")]
private void TraceOutput(string format, params object[] varParams)
{
lock(_outputLock)
{
int tid = System.Threading.Thread.CurrentThread.GetHashCode();
Console.ForegroundColor = (ConsoleColor) (tid % 8 + 8);
Console.Write("{0:000} WZACS ", tid);
Console.WriteLine(format, varParams);
Console.ResetColor();
}
}
private object _outputLock = new Object();
}
}
#endif