/// <summary> /// Encrypts a stream to another stream. /// </summary> /// <param name="source">The source stream.</param> /// <param name="target">The target stream.</param> /// <param name="passwordName">Identifies the password.</param> /// <exception cref="CryptographicException">Thrown if the password was not found or for other encryption problems.</exception> public void Encrypt(Stream source, Stream target, string passwordName) { Covenant.Requires <ArgumentNullException>(source != null, nameof(source)); Covenant.Requires <ArgumentException>(source.CanRead && source.CanSeek, nameof(source)); Covenant.Requires <ArgumentNullException>(target != null, nameof(target)); var key = GetKeyFromPassword(passwordName); passwordName = passwordName.ToLowerInvariant(); using (var cipher = new AesCipher(key, maxPaddingBytes: 128)) { using (var encrypted = new MemoryStream()) { cipher.EncryptStream(source, encrypted); // Write the header line to the target. var header = $"{MagicString}1.0;AES256;{passwordName}{lineEnding}"; target.Write(Encoding.ASCII.GetBytes(header)); // Write the encrypted data as HEX (80 characters per line). var buffer = new byte[1]; var lineLength = 0; encrypted.Position = 0; while (true) { if (encrypted.Read(buffer, 0, 1) == 0) { break; } target.Write(Encoding.ASCII.GetBytes(NeonHelper.ToHex(buffer[0], uppercase: true))); lineLength += 2; if (lineLength == 80) { target.Write(Encoding.ASCII.GetBytes(lineEnding)); lineLength = 0; } } } } }
/// <summary> /// Decrypts a stream to another stream. /// </summary> /// <param name="source">The source stream.</param> /// <param name="target">The target stream.</param> /// <exception cref="CryptographicException">Thrown if the password was not found or for other decryption problems.</exception> public void Decrypt(Stream source, Stream target) { // Read the first 512 bytes of the source stream to detect whether // this is a [NeonVault] encrypted file. We'll simply copy the // source stream to the target if it isn't one of our files. var isVaultFile = true; var startPos = source.Position; var buffer = new byte[512]; var cb = source.Read(buffer, 0, buffer.Length); var afterBOMPos = 0; // Skip over any UTF-8 byte order marker (BOM) bytes. if (cb > 3 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF) { afterBOMPos = 3; } if (cb - afterBOMPos < MagicBytes.Length) { isVaultFile = false; } for (int i = 0; i < MagicBytes.Length; i++) { if (buffer[i + afterBOMPos] != MagicBytes[i]) { isVaultFile = false; break; } } if (!isVaultFile) { source.Position = startPos; source.CopyTo(target); return; } // This is a [NeonVault] file. We need to validate the remaining parameters // on the header line, extract the password name. var lfPos = -1; for (int i = afterBOMPos; i < buffer.Length; i++) { if (buffer[i] == (char)'\n') { lfPos = i + 1; break; } } if (lfPos == -1) { throw new CryptographicException($"Invalid [{nameof(NeonVault)}] file: Invalid header line."); } var headerLine = Encoding.UTF8.GetString(buffer, 0, lfPos).Trim(); var fields = headerLine.Split(';'); if (fields.Length != 5) { throw new CryptographicException($"Invalid [{nameof(NeonVault)}] file: Unexpected number of header line parameters."); } if (fields[2] != "1.0") { throw new CryptographicException($"Unsupported [{nameof(NeonVault)}] file: Unexpected version number: {fields[2]}"); } if (fields[3] != "AES256") { throw new CryptographicException($"Unsupported [{nameof(NeonVault)}] file: Unexpected cipher: {fields[3]}"); } var passwordName = fields[4].ToLowerInvariant(); var key = GetKeyFromPassword(passwordName); // Read the HEX lines and convert them into bytes and then write then // to a MemoryStream to be decrypted. source.Position = startPos + lfPos; using (var encrypted = new MemoryStream()) { while (true) { if (source.Read(buffer, 0, 1) == 0) { break; } var firstHex = char.ToUpperInvariant((char)buffer[0]); if (char.IsWhiteSpace(firstHex)) { continue; } if (source.Read(buffer, 0, 1) == 0) { throw new CryptographicException($"Invalid [{nameof(NeonVault)}] file: Odd numer of HEX digits on a line."); } var secondHex = char.ToUpperInvariant((char)buffer[0]); if (char.IsWhiteSpace(firstHex)) { throw new CryptographicException($"Invalid [{nameof(NeonVault)}] file: Odd numer of HEX digits on a line."); } if (!NeonHelper.IsHex(firstHex) || !NeonHelper.IsHex(secondHex)) { throw new CryptographicException($"Invalid [{nameof(NeonVault)}] file: Invalid HEX digit."); } byte value; if ('0' <= firstHex && firstHex <= '9') { value = (byte)(firstHex - '0'); } else { value = (byte)(firstHex - 'A' + 10); } value <<= 4; if ('0' <= secondHex && secondHex <= '9') { value |= (byte)(secondHex - '0'); } else { value |= (byte)(secondHex - 'A' + 10); } encrypted.WriteByte(value); } encrypted.Position = 0; using (var cipher = new AesCipher(key)) { cipher.DecryptStream(encrypted, target); } } }