org.bouncycastle.bcpg.ArmoredInputStream Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bcpg-fips Show documentation
Show all versions of bcpg-fips Show documentation
The Bouncy Castle Java APIs for the OpenPGP Protocol. The APIs are designed primarily to be used in conjunction with the BC FIPS provider. The APIs may also be used with other providers although if being used in a FIPS context it is the responsibility of the user to ensure that any other providers used are FIPS certified and used appropriately.
package org.bouncycastle.bcpg;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import org.bouncycastle.util.StringList;
import org.bouncycastle.util.Strings;
/**
* 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
extends InputStream
{
/**
* set up the decoding table.
*/
private static final byte[] decodingTable;
static
{
decodingTable = new byte[128];
for (int i = 0; i < decodingTable.length; i++)
{
decodingTable[i] = (byte)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, byte[] out)
throws IOException
{
int b1, b2, b3, b4;
if (in3 < 0)
{
throw new EOFException("unexpected end of file in armored stream.");
}
if (in2 == '=')
{
b1 = decodingTable[in0] &0xff;
b2 = decodingTable[in1] & 0xff;
if ((b1 | b2) < 0)
{
throw new ArmoredInputException("invalid armor");
}
out[2] = (byte)((b1 << 2) | (b2 >> 4));
return 2;
}
else if (in3 == '=')
{
b1 = decodingTable[in0];
b2 = decodingTable[in1];
b3 = decodingTable[in2];
if ((b1 | b2 | b3) < 0)
{
throw new ArmoredInputException("invalid armor");
}
out[1] = (byte)((b1 << 2) | (b2 >> 4));
out[2] = (byte)((b2 << 4) | (b3 >> 2));
return 1;
}
else
{
b1 = decodingTable[in0];
b2 = decodingTable[in1];
b3 = decodingTable[in2];
b4 = decodingTable[in3];
if ((b1 | b2 | b3 | b4) < 0)
{
throw new ArmoredInputException("invalid armor");
}
out[0] = (byte)((b1 << 2) | (b2 >> 4));
out[1] = (byte)((b2 << 4) | (b3 >> 2));
out[2] = (byte)((b3 << 6) | b4);
return 0;
}
}
/*
* Ignore missing CRC checksums.
* https://tests.sequoia-pgp.org/#ASCII_Armor suggests that missing CRC sums do not invalidate the message.
*/
private boolean detectMissingChecksum = false;
private final CRC24 crc;
InputStream in;
boolean start = true;
byte[] outBuf = new byte[3];
int bufPtr = 3;
boolean crcFound = false;
boolean hasHeaders = true;
String header = null;
boolean newLineFound = false;
boolean clearText = false;
boolean restart = false;
StringList headerList= Strings.newList();
int lastC = 0;
boolean isEndOfStream;
/**
* Create a stream for reading a PGP armoured message, parsing up to a header
* and then reading the data that follows.
*
* @param in
*/
public ArmoredInputStream(
InputStream in)
throws IOException
{
this(in, 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 in
* @param hasHeaders true if headers are to be looked for, false otherwise.
*/
public ArmoredInputStream(
InputStream in,
boolean hasHeaders)
throws IOException
{
this.in = in;
this.hasHeaders = hasHeaders;
this.crc = new FastCRC24();
if (hasHeaders)
{
parseHeaders();
}
start = false;
}
private ArmoredInputStream(
InputStream in,
Builder builder)
throws IOException
{
this.in = in;
this.hasHeaders = builder.hasHeaders;
this.detectMissingChecksum = builder.detectMissingCRC;
this.crc = builder.ignoreCRC ? null : new FastCRC24();
if (hasHeaders)
{
parseHeaders();
}
start = false;
}
public int available()
throws IOException
{
return in.available();
}
private boolean parseHeaders()
throws IOException
{
header = null;
int c;
int last = 0;
boolean headerFound = false;
headerList = Strings.newList();
//
// if restart we already have a header
//
if (restart)
{
headerFound = true;
}
else
{
while ((c = in.read()) >= 0)
{
if (c == '-' && (last == 0 || last == '\n' || last == '\r'))
{
headerFound = true;
break;
}
last = c;
}
}
if (headerFound)
{
boolean eolReached = false;
boolean crLf = false;
ByteArrayOutputStream buf = new ByteArrayOutputStream();
buf.write('-');
if (restart) // we've had to look ahead two '-'
{
buf.write('-');
}
while ((c = in.read()) >= 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 = Strings.fromUTF8ByteArray(buf.toByteArray());
if (line.trim().length() == 0)
{
break;
}
if (headerList.size() != 0 && line.indexOf(':') < 0)
{
throw new ArmoredInputException("invalid armor header");
}
headerList.add(line);
buf.reset();
}
if (c != '\n' && c != '\r')
{
buf.write(c);
eolReached = false;
}
else
{
if (c == '\r' || (last != '\r' && c == '\n'))
{
eolReached = true;
}
}
last = c;
}
if (crLf)
{
int nl = in.read(); // skip last \n
if (nl != '\n')
{
throw new ArmoredInputException("inconsistent line endings in headers");
}
}
}
if (headerList.size() > 0)
{
header = headerList.get(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 boolean isClearText()
{
return clearText;
}
/**
* @return true if the stream is actually at end of file.
*/
public boolean 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.size() <= 1)
{
return null;
}
return headerList.toStringArray(1, headerList.size());
}
private int readIgnoreSpace()
throws IOException
{
int c = in.read();
while (c == ' ' || c == '\t' || c == '\f' || c == '\u000B') // \u000B ~ \v
{
c = in.read();
}
if (c >= 128)
{
throw new ArmoredInputException("invalid armor");
}
return c;
}
public int read()
throws IOException
{
int c;
if (start)
{
if (hasHeaders)
{
parseHeaders();
}
if (crc != null)
{
crc.reset();
}
start = false;
}
if (clearText)
{
c = in.read();
if (c == '\r' || (c == '\n' && lastC != '\r'))
{
newLineFound = true;
}
else if (newLineFound && c == '-')
{
c = in.read();
if (c == '-') // a header, not dash escaped
{
clearText = false;
start = true;
restart = true;
}
else // a space - must be a dash escape
{
c = in.read();
}
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 == '=') // crc reached
{
bufPtr = decode(readIgnoreSpace(), readIgnoreSpace(), readIgnoreSpace(), readIgnoreSpace(), outBuf);
if (bufPtr != 0)
{
throw new ArmoredInputException("malformed crc in armored message");
}
crcFound = true;
if (crc != null)
{
int i = ((outBuf[0] & 0xff) << 16)
| ((outBuf[1] & 0xff) << 8)
| (outBuf[2] & 0xff);
if (i != crc.getValue())
{
throw new ArmoredInputException("crc check failed in armored message");
}
}
return read();
}
if (c == '-') // end of record reached
{
while ((c = in.read()) >= 0)
{
if (c == '\n' || c == '\r')
{
break;
}
}
if (!crcFound && detectMissingChecksum)
{
throw new ArmoredInputException("crc check not found");
}
crcFound = false;
start = true;
bufPtr = 3;
if (c < 0)
{
isEndOfStream = true;
}
return -1;
}
}
if (c < 0)
{
isEndOfStream = true;
return -1;
}
bufPtr = decode(c, readIgnoreSpace(), readIgnoreSpace(), readIgnoreSpace(), outBuf);
if (crc != null)
{
if (bufPtr == 0)
{
crc.update3(outBuf, 0);
}
else
{
for (int i = bufPtr; i < 3; ++i)
{
crc.update(outBuf[i] & 0xFF);
}
}
}
}
return outBuf[bufPtr++] & 0xFF;
}
/**
* 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 int read(byte[] b, int off, int len) throws IOException
{
checkIndexSize(b.length, off, len);
if (len == 0)
{
return 0;
}
int c = read();
if (c == -1)
{
return -1;
}
b[off] = (byte)c;
int i = 1;
for (; i < len ; i++)
{
c = read();
if (c == -1)
{
break;
}
b[off + i] = (byte)c;
}
return i;
}
private void checkIndexSize(int size, int off, int len)
{
if (off < 0 || len < 0)
{
throw new IndexOutOfBoundsException("Offset and length cannot be negative.");
}
if (off > size - len)
{
throw new IndexOutOfBoundsException("Invalid offset and length.");
}
}
public void close()
throws IOException
{
in.close();
}
/**
* 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 false if ignore missing CRC sums, true for exception
* @deprecated use Builder class for configuring this.
*/
public void setDetectMissingCRC(boolean detectMissing)
{
this.detectMissingChecksum = detectMissing;
}
public static Builder builder()
{
return new Builder();
}
public static class Builder
{
private boolean hasHeaders = true;
private boolean detectMissingCRC = false;
private boolean ignoreCRC = false;
private Builder()
{
}
/**
* Enable or disable header parsing (default value true).
*
* @param hasHeaders true if headers should be expected, false otherwise.
* @return the current builder instance.
*/
public Builder setParseForHeaders(boolean hasHeaders)
{
this.hasHeaders = hasHeaders;
return this;
}
/**
* 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 detectMissingCRC false if ignore missing CRC sums, true for exception
*/
public Builder setDetectMissingCRC(boolean detectMissingCRC)
{
this.detectMissingCRC = detectMissingCRC;
return this;
}
/**
* Specifically ignore the CRC if in place (this will also avoid the cost of calculation).
*
* @param ignoreCRC true if CRC should be ignored, false otherwise.
* @return the current builder instance.
*/
public Builder setIgnoreCRC(boolean ignoreCRC)
{
this.ignoreCRC = ignoreCRC;
return this;
}
public ArmoredInputStream build(InputStream inputStream)
throws IOException
{
return new ArmoredInputStream(inputStream, this);
}
}
}