#region Disclaimer / License // Copyright (C) 2011, Kenneth Skovhede // http://www.hexad.dk, opensource@hexad.dk // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // #endregion #region Usage instructions, README /************************************************************* This code is an implementation of the AES Crypt tool: http://www.aescrypt.com The code is primarily ported using the file format description, using the Java code as an example where there were uncertainties. It is tested against the AES Crypt binaries, ensuring that the binaries and this code are compatible. I have NOT tested the version=0 and version=1 formats, they are implemented purely by looking at the file format specs. If you have test data for these version, please let me know if it works. Usage: There are simple static functions that you can call: SharpAESCrypt.Encrypt("password", "inputfile", "outputfile"); SharpAESCrypt.Decrypt("password", "inputfile", "outputfile"); SharpAESCrypt.Decrypt("password", inputStream, outputStream); SharpAESCrypt.Decrypt("password", inputStream, outputStream); You can control what headers are emitted using the static variables: SharpAESCrypt.Extension_CreatedByIdentifier SharpAESCrypt.Extension_InsertCreateByIdentifier SharpAESCrypt.Extension_InsertTimeStamp SharpAESCrypt.Extension_InsertPlaceholder SharpAESCrypt.DefaultFileVersion If you need more advanced processing, you can initiate an instance and use it as a stream: Stream aesStream = new SharpAESCrypt(password, inputStream, mode); You can then modify the Version and Extensions properties on the instance. If you use the stream mode, make sure you call either FlushFinalBlock() or Dispose() when you are done. Have fun! **************************************************************/ #endregion using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Security.Cryptography; namespace SharpAESCrypt { /// /// Enumerates the possible modes for encryption and decryption /// public enum OperationMode { /// /// Indicates encryption, which means that the stream must be writeable /// Encrypt, /// /// Indicates decryption, which means that the stream must be readable /// Decrypt } #region Translateable strings /// /// Placeholder for translateable strings /// public static class Strings { #region Command line /// /// A string displayed when the program is invoked without the correct number of arguments /// public static string CommandlineUsage = "SharpAESCrypt e|d password fromPath toPath"; /// /// A string displayed when an error occurs while running the commandline program /// public static string CommandlineError = "Error: {0}"; /// /// A string displayed if the mode is neither e nor d /// public static string CommandlineUnknownMode = "Invalid operation, must be (e)crypt or (d)ecrypt"; #endregion #region Exception messages /// /// An exception message that indicates that the hash algorithm is not supported /// public static string UnsupportedHashAlgorithmReuse = "The hash algortihm does not support reuse"; /// /// An exception message that indicates that the hash algorithm is not supported /// public static string UnsupportedHashAlgorithmBlocks = "The hash algortihm does not support multiple blocks"; /// /// An exception message that indicates that the hash algorithm is not supported /// public static string UnsupportedHashAlgorithmBlocksize = "Unable to digest {0} bytes, as the hash algorithm only returns {1} bytes"; /// /// An exception message that indicates that an unexpected end of stream was encountered /// public static string UnexpectedEndOfStream = "The stream was exhausted unexpectedly"; /// /// An exception message that indicates that the stream does not support writing /// public static string StreamMustBeWriteAble = "When encrypting, the stream must be writeable"; /// /// An exception messaget that indicates that the stream does not support reading /// public static string StreamMustBeReadAble = "When decrypting, the stream must be readable"; /// /// An exception message that indicates that the mode is not one of the allowed enumerations /// public static string InvalidOperationMode = "Invalid mode supplied"; /// /// An exception message that indicates that file is not in the correct format /// public static string InvalidFileFormat = "Invalid file format"; /// /// An exception message that indicates that the header marker is invalid /// public static string InvalidHeaderMarker = "Invalid header marker"; /// /// An exception message that indicates that the reserved field is not set to zero /// public static string InvalidReservedFieldValue = "Reserved field is not zero"; /// /// An exception message that indicates that the detected file version is not supported /// public static string UnsupportedFileVersion = "Unsuported file version: {0}"; /// /// An exception message that indicates that an extension had an invalid format /// public static string InvalidExtensionData = "Invalid extension data, separator (0x00) not found"; /// /// An exception message that indicates that the format was accepted, but the password was not verified /// public static string InvalidPassword = "Invalid password or corrupted data"; /// /// An exception message that indicates that the length of the file is incorrect /// public static string InvalidFileLength = "File length is invalid"; /// /// An exception message that indicates that the version is readonly when decrypting /// public static string VersionReadonlyForDecryption = "Version is readonly when decrypting"; /// /// An exception message that indicates that the file version is readonly once encryption has started /// public static string VersionReadonly = "Version cannot be changed after encryption has started"; /// /// An exception message that indicates that the supplied version number is unsupported /// public static string VersionUnsupported = "The maximum allowed version is {0}"; /// /// An exception message that indicates that the stream must support seeking /// public static string StreamMustSupportSeeking = "The stream must be seekable writing version 0 files"; /// /// An exception message that indicates that the requsted operation is unsupported /// public static string CannotReadWhileEncrypting = "Cannot read while encrypting"; /// /// An exception message that indicates that the requsted operation is unsupported /// public static string CannotWriteWhileDecrypting = "Cannot read while decrypting"; /// /// An exception message that indicates that the data has been altered /// public static string DataHMACMismatch = "Message has been altered, do not trust content"; /// /// An exception message that indicates that the data has been altered or the password is invalid /// public static string DataHMACMismatch_v0 = "Invalid password or content has been altered"; /// /// An exception message that indicates that the system is missing a text encoding /// public static string EncodingNotSupported = "The required encoding (UTF-16LE) is not supported on this system"; #endregion } #endregion /// /// Provides a stream wrapping an AESCrypt file for either encryption or decryption. /// The file format declare support for 2^64 bytes encrypted data, but .Net has trouble /// with files more than 2^63 bytes long, so this module 'only' supports 2^63 bytes /// (long vs ulong). /// public class SharpAESCrypt : Stream { #region Shared constant values /// /// The header in an AESCrypt file /// private readonly byte[] MAGIC_HEADER = Encoding.UTF8.GetBytes("AES"); /// /// The maximum supported file version /// public const byte MAX_FILE_VERSION = 2; /// /// The size of the block unit used by the algorithm in bytes /// private const int BLOCK_SIZE = 16; /// /// The size of the IV, in bytes, which is the same as the blocksize for AES /// private const int IV_SIZE = 16; /// /// The size of the key. For AES-256 that is 256/8 = 32 /// private const int KEY_SIZE = 32; /// /// The size of the SHA-256 output, which matches the KEY_SIZE /// private const int HASH_SIZE = 32; #endregion #region Private instance variables /// /// The stream being encrypted or decrypted /// private Stream m_stream; /// /// The mode of operation /// private OperationMode m_mode; /// /// The cryptostream used to perform bulk encryption /// private CryptoStream m_crypto; /// /// The HMAC used for validating data /// private HMAC m_hmac; /// /// The length of the data modulus /// private int m_length; /// /// The setup helper instance /// private SetupHelper m_helper; /// /// The list of extensions read from or written to the stream /// private List> m_extensions; /// /// The file format version /// private byte m_version = MAX_FILE_VERSION; /// /// True if the header is written, false otherwise. Used only for encryption. /// private bool m_hasWrittenHeader = false; /// /// True if the footer has been written, false otherwise. Used only for encryption. /// private bool m_hasFlushedFinalBlock = false; /// /// The size of the payload, including padding. Used only for decryption. /// private long m_payloadLength; /// /// The number of bytes read from the encrypted stream. Used only for decryption. /// private long m_readcount; /// /// The number of padding bytes. Used only for decryption. /// private byte m_paddingSize; /// /// True if the header HMAC has been read and verified, false otherwise. Used only for decryption. /// private bool m_hasReadFooter = false; #endregion #region Private helper functions and properties /// /// Helper property to ensure that the crypto stream is initialized before being used /// private CryptoStream Crypto { get { if (m_crypto == null) WriteEncryptionHeader(); return m_crypto; } } /// /// Helper function to read and validate the header /// private void ReadEncryptionHeader(string password) { byte[] tmp = new byte[MAGIC_HEADER.Length + 2]; if (m_stream.Read(tmp, 0, tmp.Length) != tmp.Length) throw new InvalidDataException(Strings.InvalidHeaderMarker); for (int i = 0; i < MAGIC_HEADER.Length; i++) if (MAGIC_HEADER[i] != tmp[i]) throw new InvalidDataException(Strings.InvalidHeaderMarker); m_version = tmp[MAGIC_HEADER.Length]; if (m_version > MAX_FILE_VERSION) throw new InvalidDataException(string.Format(Strings.UnsupportedFileVersion, m_version)); if (m_version == 0) { m_paddingSize = tmp[MAGIC_HEADER.Length + 1]; if (m_paddingSize >= BLOCK_SIZE) throw new InvalidDataException(Strings.InvalidHeaderMarker); } else if (tmp[MAGIC_HEADER.Length + 1] != 0) throw new InvalidDataException(Strings.InvalidReservedFieldValue); //Extensions are only supported in v2+ if (m_version >= 2) { int extensionLength = 0; do { byte[] tmpLength = RepeatRead(m_stream, 2); extensionLength = (((int)tmpLength[0]) << 8) | (tmpLength[1]); if (extensionLength != 0) { byte[] data = RepeatRead(m_stream, extensionLength); int separatorIndex = Array.IndexOf(data, 0); if (separatorIndex < 0) throw new InvalidDataException(Strings.InvalidExtensionData); string key = System.Text.Encoding.UTF8.GetString(data, 0, separatorIndex); byte[] value = new byte[data.Length - separatorIndex - 1]; Array.Copy(data, separatorIndex + 1, value, 0, value.Length); m_extensions.Add(new KeyValuePair(key, value)); } } while (extensionLength > 0); } byte[] iv1 = RepeatRead(m_stream, IV_SIZE); m_helper = new SetupHelper(m_mode, password, iv1); if (m_version >= 1) { byte[] hmac1 = m_helper.DecryptAESKey2(RepeatRead(m_stream, IV_SIZE + KEY_SIZE)); byte[] hmac2 = RepeatRead(m_stream, hmac1.Length); for (int i = 0; i < hmac1.Length; i++) if (hmac1[i] != hmac2[i]) throw new CryptographicException(Strings.InvalidPassword); m_payloadLength = m_stream.Length - m_stream.Position - (HASH_SIZE + 1); } else { m_helper.SetBulkKeyToKey1(); m_payloadLength = m_stream.Length - m_stream.Position - HASH_SIZE; } if (m_payloadLength % BLOCK_SIZE != 0) throw new CryptographicException(Strings.InvalidFileLength); } /// /// Writes the header to the output stream and sets up the crypto stream /// private void WriteEncryptionHeader() { m_stream.Write(MAGIC_HEADER, 0, MAGIC_HEADER.Length); m_stream.WriteByte(m_version); m_stream.WriteByte(0); //Reserved or length % 16 if (m_version >= 2) { foreach (KeyValuePair ext in m_extensions) WriteExtension(ext.Key, ext.Value); m_stream.Write(new byte[] { 0, 0 }, 0, 2); //No more extensions } m_stream.Write(m_helper.IV1, 0, m_helper.IV1.Length); if (m_version == 0) m_helper.SetBulkKeyToKey1(); else { //Generate and encrypt bulk key and its HMAC byte[] tmpKey = m_helper.EncryptAESKey2(); m_stream.Write(tmpKey, 0, tmpKey.Length); tmpKey = m_helper.CalculateKeyHmac(); m_stream.Write(tmpKey, 0, tmpKey.Length); } m_hmac = m_helper.GetHMAC(); //Insert the HMAC before the stream to calculate the HMAC for the ciphertext m_crypto = new CryptoStream(new CryptoStream(new StreamHider(m_stream, 0), m_hmac, CryptoStreamMode.Write), m_helper.CreateCryptoStream(m_mode), CryptoStreamMode.Write); m_hasWrittenHeader = true; } /// /// Writes an extension to the output stream, see: /// http://www.aescrypt.com/aes_file_format.html /// /// The extension identifier /// The data to set in the extension private void WriteExtension(string identifier, byte[] value) { byte[] name = System.Text.Encoding.UTF8.GetBytes(identifier); if (value == null) value = new byte[0]; uint size = (uint)(name.Length + 1 + value.Length); m_stream.WriteByte((byte)((size >> 8) & 0xff)); m_stream.WriteByte((byte)(size & 0xff)); m_stream.Write(name, 0, name.Length); m_stream.WriteByte(0); m_stream.Write(value, 0, value.Length); } #endregion #region Private utility classes and functions /// /// Internal helper class used to encapsulate the setup process /// private class SetupHelper : IDisposable { /// /// The MAC adress to use in case the network interface enumeration fails /// private static readonly byte[] DEFAULT_MAC = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef }; /// /// The hashing algorithm used to digest data /// private const string HASH_ALGORITHM = "SHA-256"; /// /// The algorithm used to encrypt and decrypt data /// private const string CRYPT_ALGORITHM = "Rijndael"; /// /// The algorithm used to generate random data /// private const string RAND_ALGORITHM = "SHA1PRNG"; /// /// The algorithm used to calculate the HMAC /// private const string HMAC_ALGORITHM = "HmacSHA256"; /// /// The encoding scheme for the password. /// UTF-16 should mean UTF-16LE, but Mono rejects the full name. /// A check is made when using the encoding, that it is indeed UTF-16LE. /// private const string PASSWORD_ENCODING = "utf-16"; /// /// The encryption instance /// private SymmetricAlgorithm m_crypt; /// /// The hash instance /// private HashAlgorithm m_hash; /// /// The random number generator instance /// private RandomNumberGenerator m_rand; /// /// The HMAC algorithm /// private HMAC m_hmac; /// /// The IV used to encrypt/decrypt the bulk key /// private byte[] m_iv1; /// /// The private key used to encrypt/decrypt the bulk key /// private byte[] m_aesKey1; /// /// The IV used to encrypt/decrypt bulk data /// private byte[] m_iv2; /// /// The key used to encrypt/decrypt bulk data /// private byte[] m_aesKey2; /// /// Initialize the setup /// /// The mode to prepare for /// The password used to encrypt or decrypt /// The IV used, set to null if encrypting public SetupHelper(OperationMode mode, string password, byte[] iv) { m_crypt = SymmetricAlgorithm.Create(CRYPT_ALGORITHM); //Not sure how to insert this with the CRYPT_ALGORITHM string m_crypt.Padding = PaddingMode.None; m_crypt.Mode = CipherMode.CBC; m_hash = HashAlgorithm.Create(HASH_ALGORITHM); m_rand = RandomNumberGenerator.Create(/*RAND_ALGORITHM*/); m_hmac = HMAC.Create(HMAC_ALGORITHM); if (mode == OperationMode.Encrypt) { m_iv1 = GenerateIv1(); m_aesKey1 = GenerateAESKey1(EncodePassword(password)); m_iv2 = GenerateIv2(); m_aesKey2 = GenerateAESKey2(); } else { m_iv1 = iv; m_aesKey1 = GenerateAESKey1(EncodePassword(password)); } } /// /// Encodes the password in UTF-16LE, /// used to fix missing support for the full encoding /// name under Mono. Verifies that the encoding is correct. /// /// The password to encode as a byte array /// The password encoded as a byte array private byte[] EncodePassword(string password) { Encoding e = Encoding.GetEncoding(PASSWORD_ENCODING); byte[] preamb = e == null ? null : e.GetPreamble(); if (preamb == null || preamb.Length != 2) throw new SystemException(Strings.EncodingNotSupported); if (preamb[0] == 0xff && preamb[1] == 0xfe) return e.GetBytes(password); else if (preamb[0] == 0xfe && preamb[1] == 0xff) { //We have a Big Endian, convert to Little endian byte[] tmp = e.GetBytes(password); if (tmp.Length % 2 != 0) throw new SystemException(Strings.EncodingNotSupported); for (int i = 0; i < tmp.Length; i += 2) { byte x = tmp[i]; tmp[i] = tmp[i + 1]; tmp[i + 1] = x; } return tmp; } else throw new SystemException(Strings.EncodingNotSupported); } /// /// Gets the IV used to encrypt the bulk data key /// public byte[] IV1 { get { return m_iv1; } } /// /// Creates the iv used for encrypting the actual key and IV. /// This IV is calculated using the network MAC adress as input. /// /// An IV private byte[] GenerateIv1() { byte[] iv = new byte[IV_SIZE]; long time = DateTime.Now.Ticks; byte[] mac = null; try { System.Net.NetworkInformation.NetworkInterface[] interfaces = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces(); for (int i = 0; i < interfaces.Length; i++) if (i != System.Net.NetworkInformation.NetworkInterface.LoopbackInterfaceIndex) { mac = interfaces[i].GetPhysicalAddress().GetAddressBytes(); break; } } catch { //Not much to do, just go with default MAC } if (mac == null) mac = DEFAULT_MAC; for (int i = 0; i < 8; i++) iv[i] = (byte)((time >> (i * 8)) & 0xff); Array.Copy(mac, 0, iv, 8, Math.Min(mac.Length, iv.Length - 8)); return DigestRandomBytes(iv, 256); } /// /// Generates a key based on the IV and the password. /// This key is used to encrypt the actual key and IV. /// /// The password supplied /// The key generated private byte[] GenerateAESKey1(byte[] password) { if (!m_hash.CanReuseTransform) throw new CryptographicException(Strings.UnsupportedHashAlgorithmReuse); if (!m_hash.CanTransformMultipleBlocks) throw new CryptographicException(Strings.UnsupportedHashAlgorithmBlocks); if (KEY_SIZE < m_hash.HashSize / 8) throw new CryptographicException(string.Format(Strings.UnsupportedHashAlgorithmBlocksize, KEY_SIZE, m_hash.HashSize / 8)); byte[] key = new byte[KEY_SIZE]; Array.Copy(m_iv1, key, m_iv1.Length); for (int i = 0; i < 8192; i++) { m_hash.Initialize(); m_hash.TransformBlock(key, 0, key.Length, key, 0); m_hash.TransformFinalBlock(password, 0, password.Length); key = m_hash.Hash; } return key; } /// /// Generates a random IV for encrypting data /// /// A random IV private byte[] GenerateIv2() { m_crypt.GenerateIV(); return DigestRandomBytes(m_crypt.IV, 256); } /// /// Generates a random key for encrypting data /// /// private byte[] GenerateAESKey2() { m_crypt.GenerateKey(); return DigestRandomBytes(m_crypt.Key, 32); } /// /// Encrypts the key and IV used to encrypt data with the initial key and IV. /// /// The encrypted AES Key (including IV) public byte[] EncryptAESKey2() { using(MemoryStream ms = new MemoryStream()) using (CryptoStream cs = new CryptoStream(ms, m_crypt.CreateEncryptor(m_aesKey1, m_iv1), CryptoStreamMode.Write)) { cs.Write(m_iv2, 0, m_iv2.Length); cs.Write(m_aesKey2, 0, m_aesKey2.Length); cs.FlushFinalBlock(); return ms.ToArray(); } } /// /// Calculates the HMAC for the encrypted key /// /// The encrypted data to calculate the HMAC from /// The HMAC value public byte[] CalculateKeyHmac() { m_hmac.Initialize(); m_hmac.Key = m_aesKey1; return m_hmac.ComputeHash(EncryptAESKey2()); } /// /// Performs repeated hashing of the data in the byte[] combined with random data. /// The update is performed on the input data, which is also returned. /// /// The bytes to start the digest operation with /// The number of repetitions to perform /// The hashing algorithm instance /// The digested input data, which is the same array as passed in private byte[] DigestRandomBytes(byte[] bytes, int repetitions) { if (bytes.Length > (m_hash.HashSize / 8)) throw new CryptographicException(string.Format(Strings.UnsupportedHashAlgorithmBlocksize, bytes.Length, m_hash.HashSize / 8)); if (!m_hash.CanReuseTransform) throw new CryptographicException(Strings.UnsupportedHashAlgorithmReuse); if (!m_hash.CanTransformMultipleBlocks) throw new CryptographicException(Strings.UnsupportedHashAlgorithmBlocks); m_hash.Initialize(); m_hash.TransformBlock(bytes, 0, bytes.Length, bytes, 0); for (int i = 0; i < repetitions; i++) { m_rand.GetBytes(bytes); m_hash.TransformBlock(bytes, 0, bytes.Length, bytes, 0); } m_hash.TransformFinalBlock(bytes, 0, 0); Array.Copy(m_hash.Hash, bytes, bytes.Length); return bytes; } /// /// Generates the CryptoTransform element used to encrypt/decrypt the bulk data /// /// The operation mode /// An ICryptoTransform instance public ICryptoTransform CreateCryptoStream(OperationMode mode) { if (mode == OperationMode.Encrypt) return m_crypt.CreateEncryptor(m_aesKey2, m_iv2); else return m_crypt.CreateDecryptor(m_aesKey2, m_iv2); } /// /// Creates a fresh HMAC calculation algorithm /// /// An HMAC algortihm using AES Key 2 public HMAC GetHMAC() { HMAC h = HMAC.Create(HMAC_ALGORITHM); h.Key = m_aesKey2; return h; } /// /// Decrypts the bulk key and IV /// /// The encrypted IV followed by the key /// The HMAC value for the key public byte[] DecryptAESKey2(byte[] data) { using (MemoryStream ms = new MemoryStream(data)) using (CryptoStream cs = new CryptoStream(ms, m_crypt.CreateDecryptor(m_aesKey1, m_iv1), CryptoStreamMode.Read)) { m_iv2 = RepeatRead(cs, IV_SIZE); m_aesKey2 = RepeatRead(cs, KEY_SIZE); } m_hmac.Initialize(); m_hmac.Key = m_aesKey1; m_hmac.TransformFinalBlock(data, 0, data.Length); return m_hmac.Hash; } /// /// Sets iv2 and aesKey2 to iv1 and aesKey1 respectively. /// Used only for files with version = 0 /// public void SetBulkKeyToKey1() { m_iv2 = m_iv1; m_aesKey2 = m_aesKey1; } #region IDisposable Members /// /// Disposes all members /// public void Dispose() { if (m_crypt != null) { if (m_aesKey1 != null) Array.Clear(m_aesKey1, 0 , m_aesKey1.Length); if (m_iv1 != null) Array.Clear(m_iv1, 0, m_iv1.Length); if (m_aesKey2 != null) Array.Clear(m_aesKey2, 0, m_aesKey2.Length); if (m_iv2 != null) Array.Clear(m_iv2, 0, m_iv2.Length); m_aesKey1 = null; m_iv1 = null; m_aesKey2 = null; m_iv2 = null; m_hash = null; m_hmac = null; m_rand = null; m_crypt = null; } } #endregion } /// /// Internal helper class, used to hide the trailing bytes from the cryptostream /// private class StreamHider : Stream { /// /// The wrapped stream /// private Stream m_stream; /// /// The number of bytes to hide /// private int m_hiddenByteCount; /// /// Constructs the stream wrapper to hide the desired bytes /// /// The stream to wrap /// The number of bytes to hide public StreamHider(Stream stream, int count) { m_stream = stream; m_hiddenByteCount = count; } #region Basic Stream implementation stuff public override bool CanRead { get { return m_stream.CanRead; } } public override bool CanSeek { get { return m_stream.CanSeek; } } public override bool CanWrite { get { return m_stream.CanWrite; } } public override void Flush() { m_stream.Flush(); } public override long Length { get { return m_stream.Length; } } public override long Seek(long offset, SeekOrigin origin) { return m_stream.Seek(offset, origin); } public override void SetLength(long value) { m_stream.SetLength(value); } public override long Position { get { return m_stream.Position; } set { m_stream.Position = value; } } public override void Write(byte[] buffer, int offset, int count) { m_stream.Write(buffer, offset, count); } #endregion /// /// The overridden read function that ensures that the caller cannot see the hidden bytes /// /// The buffer to read into /// The offset into the buffer /// The number of bytes to read /// The number of bytes read public override int Read(byte[] buffer, int offset, int count) { long allowedCount = Math.Max(0, Math.Min(count, m_stream.Length - (m_stream.Position + m_hiddenByteCount))); if (allowedCount == 0) return 0; else return m_stream.Read(buffer, offset, (int)allowedCount); } } /// /// Helper function to support reading from streams that chunck data. /// Will keep reading a stream until bytes have been read. /// Throws an exception if the stream is exhausted before bytes are read. /// /// The stream to read from /// The number of bytes to read /// The data read internal static byte[] RepeatRead(Stream stream, int count) { byte[] tmp = new byte[count]; while (count > 0) { int r = stream.Read(tmp, tmp.Length - count, count); count -= r; if (r == 0 && count != 0) throw new InvalidDataException(Strings.UnexpectedEndOfStream); } return tmp; } #endregion #region Public static API #region Default extension control variables /// /// The name inserted as the creator software in the extensions when creating output /// public static string Extension_CreatedByIdentifier = string.Format("SharpAESCrypt v{0}", System.Reflection.Assembly.GetExecutingAssembly().GetName().Version); /// /// A value indicating if the extension data should contain the creator software /// public static bool Extension_InsertCreateByIdentifier = true; /// /// A value indicating if the extensions data should contain timestamp data /// public static bool Extension_InsertTimeStamp = false; /// /// A value indicating if the extensions data should contain an empty block as suggested by the file format /// public static bool Extension_InsertPlaceholder = true; #endregion /// /// The file version to use when creating a new file /// public static byte DefaultFileVersion = MAX_FILE_VERSION; /// /// Encrypts a stream using the supplied password /// /// The password to decrypt with /// The stream with unencrypted data /// The encrypted output stream public static void Encrypt(string password, Stream input, Stream output) { int a; byte[] buffer = new byte[1024 * 4]; SharpAESCrypt c = new SharpAESCrypt(password, output, OperationMode.Encrypt); while ((a = input.Read(buffer, 0, buffer.Length)) != 0) c.Write(buffer, 0, a); c.FlushFinalBlock(); } /// /// Decrypts a stream using the supplied password /// /// The password to encrypt with /// The stream with encrypted data /// The unencrypted output stream public static void Decrypt(string password, Stream input, Stream output) { int a; byte[] buffer = new byte[1024 * 4]; SharpAESCrypt c = new SharpAESCrypt(password, input, OperationMode.Decrypt); while ((a = c.Read(buffer, 0, buffer.Length)) != 0) output.Write(buffer, 0, a); } /// /// Encrypts a file using the supplied password /// /// The password to encrypt with /// The file with unencrypted data /// The encrypted output file public static void Encrypt(string password, string inputfile, string outputfile) { using (FileStream infs = File.OpenRead(inputfile)) using (FileStream outfs = File.Create(outputfile)) Encrypt(password, infs, outfs); } /// /// Decrypts a file using the supplied password /// /// The password to decrypt with /// The file with encrypted data /// The unencrypted output file public static void Decrypt(string password, string inputfile, string outputfile) { using (FileStream infs = File.OpenRead(inputfile)) using (FileStream outfs = File.Create(outputfile)) Decrypt(password, infs, outfs); } #endregion #region Public instance API /// /// Constructs a new AESCrypt instance, operating on the supplied stream /// /// The password used for encryption or decryption /// The stream to operate on, must be writeable for encryption, and readable for decryption /// The mode of operation, either OperationMode.Encrypt or OperationMode.Decrypt public SharpAESCrypt(string password, Stream stream, OperationMode mode) { //Basic input checks if (stream == null) throw new ArgumentNullException("stream"); if (password == null) throw new ArgumentNullException("password"); if (mode != OperationMode.Encrypt && mode != OperationMode.Decrypt) throw new ArgumentException(Strings.InvalidOperationMode, "mode"); if (mode == OperationMode.Encrypt && !stream.CanWrite) throw new ArgumentException(Strings.StreamMustBeWriteAble, "stream"); if (mode == OperationMode.Decrypt && !stream.CanRead) throw new ArgumentException(Strings.StreamMustBeReadAble, "stream"); m_mode = mode; m_stream = stream; m_extensions = new List>(); if (mode == OperationMode.Encrypt) { this.Version = DefaultFileVersion; m_helper = new SetupHelper(mode, password, null); //Setup default extensions if (Extension_InsertCreateByIdentifier) m_extensions.Add(new KeyValuePair("CREATED-BY", System.Text.Encoding.UTF8.GetBytes(Extension_CreatedByIdentifier))); if (Extension_InsertTimeStamp) { m_extensions.Add(new KeyValuePair("CREATED-DATE", System.Text.Encoding.UTF8.GetBytes(DateTime.Now.ToString("yyyy-MM-dd")))); m_extensions.Add(new KeyValuePair("CREATED-TIME", System.Text.Encoding.UTF8.GetBytes(DateTime.Now.ToUniversalTime().ToString("hh-mm-ss")))); } if (Extension_InsertPlaceholder) m_extensions.Add(new KeyValuePair("", new byte[127])); //Suggested extension space //We defer creation of the cryptostream until it is needed, // so the caller can change version, extensions, etc. // before we write the header m_crypto = null; } else { //Read and validate ReadEncryptionHeader(password); m_hmac = m_helper.GetHMAC(); //Insert the HMAC before the decryption so the HMAC is calculated for the ciphertext m_crypto = new CryptoStream(new CryptoStream(new StreamHider(m_stream, m_version == 0 ? HASH_SIZE : (HASH_SIZE + 1)), m_hmac, CryptoStreamMode.Read), m_helper.CreateCryptoStream(m_mode), CryptoStreamMode.Read); } } /// /// Gets or sets the version number. /// Note that this can only be set when encrypting, /// and must be done before encryption has started. /// See MAX_FILE_VERSION for the maximum supported version. /// Note that version 0 requires a seekable stream. /// public byte Version { get { return m_version; } set { if (m_mode == OperationMode.Decrypt) throw new InvalidOperationException(Strings.VersionReadonlyForDecryption); if (m_mode == OperationMode.Encrypt && m_crypto != null) throw new InvalidOperationException(Strings.VersionReadonly); if (value > MAX_FILE_VERSION) throw new ArgumentOutOfRangeException(string.Format(Strings.VersionUnsupported, MAX_FILE_VERSION)); if (value == 0 && !m_stream.CanSeek) throw new InvalidOperationException(Strings.StreamMustSupportSeeking); m_version = value; } } /// /// Provides access to the extensions found in the file. /// This collection cannot be updated when decrypting, /// nor after the encryption has started. /// public IList> Extensions { get { if (m_mode == OperationMode.Decrypt || (m_mode == OperationMode.Encrypt && m_crypto != null)) return m_extensions.AsReadOnly(); else return m_extensions; } } #region Basic stream implementation stuff, all mapped directly to the cryptostream public override bool CanRead { get { return Crypto.CanRead; } } public override bool CanSeek { get { return Crypto.CanSeek; } } public override bool CanWrite { get { return Crypto.CanWrite; } } public override void Flush() { Crypto.Flush(); } public override long Length { get { return Crypto.Length; } } public override long Position { get { return Crypto.Position; } set { Crypto.Position = value; } } public override long Seek(long offset, System.IO.SeekOrigin origin) { return Crypto.Seek(offset, origin); } public override void SetLength(long value) { Crypto.SetLength(value); } #endregion /// /// Reads unencrypted data from the underlying stream /// /// The buffer to read data into /// The offset into the buffer /// The number of bytes to read /// The number of bytes read public override int Read(byte[] buffer, int offset, int count) { if (m_mode != OperationMode.Decrypt) throw new InvalidOperationException(Strings.CannotReadWhileEncrypting); if (m_hasReadFooter) return 0; count = Crypto.Read(buffer, offset, count); //TODO: If the cryptostream supporting seeking in future versions of .Net, // this counter system does not work m_readcount += count; m_length = (m_length + count) % BLOCK_SIZE; if (!m_hasReadFooter && m_readcount == m_payloadLength) { m_hasReadFooter = true; //Verify the data if (m_version >= 1) { int l = m_stream.ReadByte(); if (l < 0) throw new InvalidDataException(Strings.UnexpectedEndOfStream); m_paddingSize = (byte)l; if (m_paddingSize > BLOCK_SIZE) throw new InvalidDataException(Strings.InvalidFileLength); } if (m_paddingSize > 0) count -= (BLOCK_SIZE - m_paddingSize); if (m_length % BLOCK_SIZE != 0 || m_readcount % BLOCK_SIZE != 0) throw new InvalidDataException(Strings.InvalidFileLength); //Required because we want to read the hash, // so FlushFinalBlock need to be called. //We cannot call FlushFinalBlock directly because it may // have been called by the read operation. //The StreamHider makes sure that the underlying stream // is not closed Crypto.Close(); byte[] hmac1 = m_hmac.Hash; byte[] hmac2 = RepeatRead(m_stream, hmac1.Length); for (int i = 0; i < hmac1.Length; i++) if (hmac1[i] != hmac2[i]) throw new InvalidDataException(m_version == 0 ? Strings.DataHMACMismatch_v0 : Strings.DataHMACMismatch); } return count; } /// /// Writes unencrypted data into an encrypted stream /// /// The data to write /// The offset into the buffer /// The number of bytes to write public override void Write(byte[] buffer, int offset, int count) { if (m_mode != OperationMode.Encrypt) throw new InvalidOperationException(Strings.CannotWriteWhileDecrypting); m_length = (m_length + count) % BLOCK_SIZE; Crypto.Write(buffer, offset, count); } /// /// Flushes any remaining data to the stream /// public void FlushFinalBlock() { if (!m_hasFlushedFinalBlock) { if (m_mode == OperationMode.Encrypt) { if (!m_hasWrittenHeader) WriteEncryptionHeader(); byte lastLen = (byte)(m_length %= BLOCK_SIZE); //Apply PaddingMode.PKCS7 manually, the original AES crypt uses non-standard padding if (lastLen != 0) { byte[] padding = new byte[BLOCK_SIZE - lastLen]; for (int i = 0; i < padding.Length; i++) padding[i] = (byte)padding.Length; Write(padding, 0, padding.Length); } //Not required without padding, but throws exception if the stream is used incorrectly Crypto.FlushFinalBlock(); //The StreamHider makes sure the underlying stream is not closed. Crypto.Close(); byte[] hmac = m_hmac.Hash; if (m_version == 0) { m_stream.Write(hmac, 0, hmac.Length); long pos = m_stream.Position; m_stream.Seek(MAGIC_HEADER.Length + 1, SeekOrigin.Begin); m_stream.WriteByte(lastLen); m_stream.Seek(pos, SeekOrigin.Begin); m_stream.Flush(); } else { m_stream.WriteByte(lastLen); m_stream.Write(hmac, 0, hmac.Length); m_stream.Flush(); } } } } /// /// Releases all resources used by the instance, and flushes any data currently held, into the stream /// protected override void Dispose(bool disposing) { base.Dispose(disposing); if (disposing) { if (m_mode == OperationMode.Encrypt && !m_hasFlushedFinalBlock) FlushFinalBlock(); m_crypto.Dispose(); m_crypto = null; m_stream.Dispose(); m_stream = null; m_extensions = null; m_helper.Dispose(); m_helper = null; m_hmac = null; } } #endregion /// /// Main function, used when compiled as a standalone executable /// /// Commandline arguments public static void Main(string[] args) { if (args.Length < 4) { Console.WriteLine(Strings.CommandlineUsage); return; } try { if (args[0].StartsWith("e", StringComparison.InvariantCultureIgnoreCase)) Encrypt(args[1], args[2], args[3]); else if (args[0].StartsWith("d", StringComparison.InvariantCultureIgnoreCase)) Decrypt(args[1], args[2], args[3]); #if DEBUG else if (args[0].StartsWith("u")) Unittest(); #endif else Console.WriteLine(Strings.CommandlineUnknownMode); } catch (Exception ex) { Console.WriteLine(string.Format(Strings.CommandlineError, ex.ToString())); } } #region Unittest code #if DEBUG /// /// Performs a unittest to ensure that the program performs as expected /// private static void Unittest() { const int MIN_SIZE = 1024 * 5; const int MAX_SIZE = 1024 * 1024 * 100; //100mb const int REPETIONS = 1000; bool allpass = true; Random rnd = new Random(); Console.WriteLine("Running unittest"); //Test each supported version for (byte v = 0; v <= MAX_FILE_VERSION; v++) { SharpAESCrypt.DefaultFileVersion = v; //Test boundary 0 and around the block/keysize margins for (int i = 0; i < MIN_SIZE; i++) using (MemoryStream ms = new MemoryStream()) { byte[] tmp = new byte[i]; rnd.NextBytes(tmp); ms.Write(tmp, 0, tmp.Length); allpass &= Unittest(string.Format("Testing version {0} with length = {1} => ", v, ms.Length), ms); } } SharpAESCrypt.DefaultFileVersion = MAX_FILE_VERSION; Console.WriteLine(string.Format("Initial tests complete, running bulk tests with v{0}", SharpAESCrypt.DefaultFileVersion)); for (int i = 0; i < REPETIONS; i++) { using (MemoryStream ms = new MemoryStream()) { byte[] tmp = new byte[rnd.Next(MIN_SIZE, MAX_SIZE)]; rnd.NextBytes(tmp); ms.Write(tmp, 0, tmp.Length); allpass |= Unittest(string.Format("Testing bulk {0} of {1} with length = {2} => ", i, REPETIONS, ms.Length), ms); } } if (allpass) { Console.WriteLine(); Console.WriteLine(); Console.WriteLine("**** All unittests passed ****"); Console.WriteLine(); } } /// /// Helper function to /// /// A message printed to the console /// The stream to test with private static bool Unittest(string message, MemoryStream input) { Console.Write(message); const string PASSWORD_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\"#¤%&/()=?`*'^¨-_.:,;<>|"; const int MIN_LEN = 1; const int MAX_LEN = 25; try { Random rnd = new Random(); char[] pwdchars = new char[rnd.Next(MIN_LEN, MAX_LEN)]; for (int i = 0; i < pwdchars.Length; i++) pwdchars[i] = PASSWORD_CHARS[rnd.Next(0, PASSWORD_CHARS.Length)]; input.Position = 0; using (MemoryStream enc = new MemoryStream()) using (MemoryStream dec = new MemoryStream()) { Encrypt(new string(pwdchars), input, enc); enc.Position = 0; Decrypt(new string(pwdchars), enc, dec); dec.Position = 0; input.Position = 0; if (dec.Length != input.Length) throw new Exception(string.Format("Length differ {0} vs {1}", dec.Length, input.Length)); for (int i = 0; i < dec.Length; i++) if (dec.ReadByte() != input.ReadByte()) throw new Exception(string.Format("Streams differ at byte {0}", i)); } } catch (Exception ex) { //TODO: Hashværdien skal færdigøres før hash værdien hentes? Console.WriteLine("FAILED: " + ex.Message); return false; } Console.WriteLine("OK!"); return true; } #endif #endregion } }