using System; using System.Collections; using System.IO; using System.Text; using Org.BouncyCastle.Utilities; using Org.BouncyCastle.Utilities.IO; namespace Org.BouncyCastle.Bcpg { /** * reader for Base64 armored objects - read the headers and then start returning * bytes when the data is reached. An IOException is thrown if the CRC check * is detected and fails. *

* By default a missing CRC will not cause an exception. To force CRC detection use: *

     *     ArmoredInputStream aIn = ...
     *
     *     aIn.setDetectMissingCRC(true);
     * 
*

*/ public class ArmoredInputStream : BaseInputStream { /* * set up the decoding table. */ private readonly static byte[] decodingTable; static ArmoredInputStream() { decodingTable = new byte[128]; Arrays.Fill(decodingTable, 0xff); for (int i = 'A'; i <= 'Z'; i++) { decodingTable[i] = (byte)(i - 'A'); } for (int i = 'a'; i <= 'z'; i++) { decodingTable[i] = (byte)(i - 'a' + 26); } for (int i = '0'; i <= '9'; i++) { decodingTable[i] = (byte)(i - '0' + 52); } decodingTable['+'] = 62; decodingTable['/'] = 63; } /** * decode the base 64 encoded input data. * * @return the offset the data starts in out. */ private static int Decode(int in0, int in1, int in2, int in3, int[] result) { if (in3 < 0) throw new EndOfStreamException("unexpected end of file in armored stream."); int b1, b2, b3, b4; if (in2 == '=') { b1 = decodingTable[in0]; b2 = decodingTable[in1]; if ((b1 | b2) >= 128) throw new IOException("invalid armor"); result[2] = ((b1 << 2) | (b2 >> 4)) & 0xff; return 2; } else if (in3 == '=') { b1 = decodingTable[in0]; b2 = decodingTable[in1]; b3 = decodingTable[in2]; if ((b1 | b2 | b3) >= 128) throw new IOException("invalid armor"); result[1] = ((b1 << 2) | (b2 >> 4)) & 0xff; result[2] = ((b2 << 4) | (b3 >> 2)) & 0xff; return 1; } else { b1 = decodingTable[in0]; b2 = decodingTable[in1]; b3 = decodingTable[in2]; b4 = decodingTable[in3]; if ((b1 | b2 | b3 | b4) >= 128) throw new IOException("invalid armor"); result[0] = ((b1 << 2) | (b2 >> 4)) & 0xff; result[1] = ((b2 << 4) | (b3 >> 2)) & 0xff; result[2] = ((b3 << 6) | b4) & 0xff; return 0; } } /* * Ignore missing CRC checksums. * https://tests.sequoia-pgp.org/#ASCII_Armor suggests that missing CRC sums do not invalidate the message. */ private bool detectMissingChecksum = false; Stream input; bool start = true; int[] outBuf = new int[3]; int bufPtr = 3; Crc24 crc = new Crc24(); bool crcFound = false; bool hasHeaders = true; string header = null; bool newLineFound = false; bool clearText = false; bool restart = false; IList headerList = Platform.CreateArrayList(); int lastC = 0; bool isEndOfStream; /** * Create a stream for reading a PGP armoured message, parsing up to a header * and then reading the data that follows. * * @param input */ public ArmoredInputStream(Stream input) : this(input, true) { } /** * Create an armoured input stream which will assume the data starts * straight away, or parse for headers first depending on the value of * hasHeaders. * * @param input * @param hasHeaders true if headers are to be looked for, false otherwise. */ public ArmoredInputStream(Stream input, bool hasHeaders) { this.input = input; this.hasHeaders = hasHeaders; if (hasHeaders) { ParseHeaders(); } start = false; } private bool ParseHeaders() { header = null; int c; int last = 0; bool headerFound = false; headerList = Platform.CreateArrayList(); // // if restart we already have a header // if (restart) { headerFound = true; } else { while ((c = input.ReadByte()) >= 0) { if (c == '-' && (last == 0 || last == '\n' || last == '\r')) { headerFound = true; break; } last = c; } } if (headerFound) { StringBuilder buf = new StringBuilder("-"); bool eolReached = false; bool crLf = false; if (restart) // we've had to look ahead two '-' { buf.Append('-'); } while ((c = input.ReadByte()) >= 0) { if (last == '\r' && c == '\n') { crLf = true; } if (eolReached && (last != '\r' && c == '\n')) { break; } if (eolReached && c == '\r') { break; } if (c == '\r' || (last != '\r' && c == '\n')) { string line = buf.ToString(); if (line.Trim().Length < 1) break; if (headerList.Count > 0 && line.IndexOf(':') < 0) throw new IOException("invalid armor header"); headerList.Add(line); buf.Length = 0; } if (c != '\n' && c != '\r') { buf.Append((char)c); eolReached = false; } else { if (c == '\r' || (last != '\r' && c == '\n')) { eolReached = true; } } last = c; } if (crLf) { input.ReadByte(); // skip last \n } } if (headerList.Count > 0) { header = (string)headerList[0]; } clearText = "-----BEGIN PGP SIGNED MESSAGE-----".Equals(header); newLineFound = true; return headerFound; } /** * @return true if we are inside the clear text section of a PGP * signed message. */ public bool IsClearText() { return clearText; } /** * @return true if the stream is actually at end of file. */ public bool IsEndOfStream() { return isEndOfStream; } /** * Return the armor header line (if there is one) * @return the armor header line, null if none present. */ public string GetArmorHeaderLine() { return header; } /** * Return the armor headers (the lines after the armor header line), * @return an array of armor headers, null if there aren't any. */ public string[] GetArmorHeaders() { if (headerList.Count <= 1) return null; string[] hdrs = new string[headerList.Count - 1]; for (int i = 0; i != hdrs.Length; i++) { hdrs[i] = (string)headerList[i + 1]; } return hdrs; } private int ReadIgnoreSpace() { int c; do { c = input.ReadByte(); } while (c == ' ' || c == '\t' || c == '\f' || c == '\u000B') ; // \u000B ~ \v if (c >= 128) throw new IOException("invalid armor"); return c; } public override int ReadByte() { if (start) { if (hasHeaders) { ParseHeaders(); } crc.Reset(); start = false; } int c; if (clearText) { c = input.ReadByte(); if (c == '\r' || (c == '\n' && lastC != '\r')) { newLineFound = true; } else if (newLineFound && c == '-') { c = input.ReadByte(); if (c == '-') // a header, not dash escaped { clearText = false; start = true; restart = true; } else // a space - must be a dash escape { c = input.ReadByte(); } newLineFound = false; } else { if (c != '\n' && lastC != '\r') { newLineFound = false; } } lastC = c; if (c < 0) { isEndOfStream = true; } return c; } if (bufPtr > 2 || crcFound) { c = ReadIgnoreSpace(); if (c == '\r' || c == '\n') { c = ReadIgnoreSpace(); while (c == '\n' || c == '\r') { c = ReadIgnoreSpace(); } if (c < 0) // EOF { isEndOfStream = true; return -1; } if (c == '=') // crc reached { bufPtr = Decode(ReadIgnoreSpace(), ReadIgnoreSpace(), ReadIgnoreSpace(), ReadIgnoreSpace(), outBuf); if (bufPtr == 0) { int i = ((outBuf[0] & 0xff) << 16) | ((outBuf[1] & 0xff) << 8) | (outBuf[2] & 0xff); crcFound = true; if (i != crc.Value) { throw new IOException("crc check failed in armored message."); } return ReadByte(); } else { if (detectMissingChecksum) { throw new IOException("no crc found in armored message"); } } } else if (c == '-') // end of record reached { while ((c = input.ReadByte()) >= 0) { if (c == '\n' || c == '\r') { break; } } if (!crcFound && detectMissingChecksum) { throw new IOException("crc check not found"); } crcFound = false; start = true; bufPtr = 3; if (c < 0) { isEndOfStream = true; } return -1; } else // data { bufPtr = Decode(c, ReadIgnoreSpace(), ReadIgnoreSpace(), ReadIgnoreSpace(), outBuf); } } else { if (c >= 0) { bufPtr = Decode(c, ReadIgnoreSpace(), ReadIgnoreSpace(), ReadIgnoreSpace(), outBuf); } else { isEndOfStream = true; return -1; } } } c = outBuf[bufPtr++]; crc.Update(c); return c; } /** * Reads up to len bytes of data from the input stream into * an array of bytes. An attempt is made to read as many as * len bytes, but a smaller number may be read. * The number of bytes actually read is returned as an integer. * * The first byte read is stored into element b[off], the * next one into b[off+1], and so on. The number of bytes read * is, at most, equal to len. * * NOTE: We need to override the custom behavior of Java's {@link InputStream#read(byte[], int, int)}, * as the upstream method silently swallows {@link IOException IOExceptions}. * This would cause CRC checksum errors to go unnoticed. * * @see Related BC bug report * @param b byte array * @param off offset at which we start writing data to the array * @param len number of bytes we write into the array * @return total number of bytes read into the buffer * * @throws IOException if an exception happens AT ANY POINT */ public override int Read(byte[] b, int off, int len) { CheckIndexSize(b.Length, off, len); int pos = 0; while (pos < len) { int c = ReadByte(); if (c < 0) break; b[off + pos++] = (byte)c; } return pos; } private void CheckIndexSize(int size, int off, int len) { if (off < 0 || len < 0) throw new IndexOutOfRangeException("Offset and length cannot be negative."); if (off > size - len) throw new IndexOutOfRangeException("Invalid offset and length."); } #if PORTABLE protected override void Dispose(bool disposing) { if (disposing) { Platform.Dispose(input); } base.Dispose(disposing); } #else public override void Close() { Platform.Dispose(input); base.Close(); } #endif /** * Change how the stream should react if it encounters missing CRC checksum. * The default value is false (ignore missing CRC checksums). If the behavior is set to true, * an {@link IOException} will be thrown if a missing CRC checksum is encountered. * * @param detectMissing ignore missing CRC sums */ public virtual void SetDetectMissingCrc(bool detectMissing) { this.detectMissingChecksum = detectMissing; } } }