/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.apksig.internal.asn1.ber;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
/**
* {@link BerDataValueReader} which reads from an {@link InputStream} returning BER-encoded data
* values. See {@code X.690} for the encoding.
*/
public class InputStreamBerDataValueReader implements BerDataValueReader {
private final InputStream mIn;
public InputStreamBerDataValueReader(InputStream in) {
if (in == null) {
throw new NullPointerException("in == null");
}
mIn = in;
}
@Override
public BerDataValue readDataValue() throws BerDataValueFormatException {
return readDataValue(mIn);
}
/**
* Returns the next data value or {@code null} if end of input has been reached.
*
* @throws BerDataValueFormatException if the value being read is malformed.
*/
@SuppressWarnings("resource")
private static BerDataValue readDataValue(InputStream input)
throws BerDataValueFormatException {
RecordingInputStream in = new RecordingInputStream(input);
try {
int firstIdentifierByte = in.read();
if (firstIdentifierByte == -1) {
// End of input
return null;
}
int tagNumber = readTagNumber(in, firstIdentifierByte);
int firstLengthByte = in.read();
if (firstLengthByte == -1) {
throw new BerDataValueFormatException("Missing length");
}
boolean constructed = BerEncoding.isConstructed((byte) firstIdentifierByte);
int contentsLength;
int contentsOffsetInDataValue;
if ((firstLengthByte & 0x80) == 0) {
// short form length
contentsLength = readShortFormLength(firstLengthByte);
contentsOffsetInDataValue = in.getReadByteCount();
skipDefiniteLengthContents(in, contentsLength);
} else if ((firstLengthByte & 0xff) != 0x80) {
// long form length
contentsLength = readLongFormLength(in, firstLengthByte);
contentsOffsetInDataValue = in.getReadByteCount();
skipDefiniteLengthContents(in, contentsLength);
} else {
// indefinite length
contentsOffsetInDataValue = in.getReadByteCount();
contentsLength =
constructed
? skipConstructedIndefiniteLengthContents(in)
: skipPrimitiveIndefiniteLengthContents(in);
}
byte[] encoded = in.getReadBytes();
ByteBuffer encodedContents =
ByteBuffer.wrap(encoded, contentsOffsetInDataValue, contentsLength);
return new BerDataValue(
ByteBuffer.wrap(encoded),
encodedContents,
BerEncoding.getTagClass((byte) firstIdentifierByte),
constructed,
tagNumber);
} catch (IOException e) {
throw new BerDataValueFormatException("Failed to read data value", e);
}
}
private static int readTagNumber(InputStream in, int firstIdentifierByte)
throws IOException, BerDataValueFormatException {
int tagNumber = BerEncoding.getTagNumber((byte) firstIdentifierByte);
if (tagNumber == 0x1f) {
// high-tag-number form
return readHighTagNumber(in);
} else {
// low-tag-number form
return tagNumber;
}
}
private static int readHighTagNumber(InputStream in)
throws IOException, BerDataValueFormatException {
// Base-128 big-endian form, where each byte has the highest bit set, except for the last
// byte where the highest bit is not set
int b;
int result = 0;
do {
b = in.read();
if (b == -1) {
throw new BerDataValueFormatException("Truncated tag number");
}
if (result > Integer.MAX_VALUE >>> 7) {
throw new BerDataValueFormatException("Tag number too large");
}
result <<= 7;
result |= b & 0x7f;
} while ((b & 0x80) != 0);
return result;
}
private static int readShortFormLength(int firstLengthByte) {
return firstLengthByte & 0x7f;
}
private static int readLongFormLength(InputStream in, int firstLengthByte)
throws IOException, BerDataValueFormatException {
// The low 7 bits of the first byte represent the number of bytes (following the first
// byte) in which the length is in big-endian base-256 form
int byteCount = firstLengthByte & 0x7f;
if (byteCount > 4) {
throw new BerDataValueFormatException("Length too large: " + byteCount + " bytes");
}
int result = 0;
for (int i = 0; i < byteCount; i++) {
int b = in.read();
if (b == -1) {
throw new BerDataValueFormatException("Truncated length");
}
if (result > Integer.MAX_VALUE >>> 8) {
throw new BerDataValueFormatException("Length too large");
}
result <<= 8;
result |= b & 0xff;
}
return result;
}
private static void skipDefiniteLengthContents(InputStream in, int len)
throws IOException, BerDataValueFormatException {
long bytesRead = 0;
while (len > 0) {
int skipped = (int) in.skip(len);
if (skipped <= 0) {
throw new BerDataValueFormatException(
"Truncated definite-length contents: " + bytesRead + " bytes read"
+ ", " + len + " missing");
}
len -= skipped;
bytesRead += skipped;
}
}
private static int skipPrimitiveIndefiniteLengthContents(InputStream in)
throws IOException, BerDataValueFormatException {
// Contents are terminated by 0x00 0x00
boolean prevZeroByte = false;
int bytesRead = 0;
while (true) {
int b = in.read();
if (b == -1) {
throw new BerDataValueFormatException(
"Truncated indefinite-length contents: " + bytesRead + " bytes read");
}
bytesRead++;
if (bytesRead < 0) {
throw new BerDataValueFormatException("Indefinite-length contents too long");
}
if (b == 0) {
if (prevZeroByte) {
// End of contents reached -- we've read the value and its terminator 0x00 0x00
return bytesRead - 2;
}
prevZeroByte = true;
continue;
} else {
prevZeroByte = false;
}
}
}
private static int skipConstructedIndefiniteLengthContents(RecordingInputStream in)
throws BerDataValueFormatException {
// Contents are terminated by 0x00 0x00. However, this data value is constructed, meaning it
// can contain data values which are indefinite length encoded as well. As a result, we
// must parse the direct children of this data value to correctly skip over the contents of
// this data value.
int readByteCountBefore = in.getReadByteCount();
while (true) {
// We can't easily peek for the 0x00 0x00 terminator using the provided InputStream.
// Thus, we use the fact that 0x00 0x00 parses as a data value whose encoded form we
// then check below to see whether it's 0x00 0x00.
BerDataValue dataValue = readDataValue(in);
if (dataValue == null) {
throw new BerDataValueFormatException(
"Truncated indefinite-length contents: "
+ (in.getReadByteCount() - readByteCountBefore) + " bytes read");
}
if (in.getReadByteCount() <= 0) {
throw new BerDataValueFormatException("Indefinite-length contents too long");
}
ByteBuffer encoded = dataValue.getEncoded();
if ((encoded.remaining() == 2) && (encoded.get(0) == 0) && (encoded.get(1) == 0)) {
// 0x00 0x00 encountered
return in.getReadByteCount() - readByteCountBefore - 2;
}
}
}
private static class RecordingInputStream extends InputStream {
private final InputStream mIn;
private final ByteArrayOutputStream mBuf;
private RecordingInputStream(InputStream in) {
mIn = in;
mBuf = new ByteArrayOutputStream();
}
public byte[] getReadBytes() {
return mBuf.toByteArray();
}
public int getReadByteCount() {
return mBuf.size();
}
@Override
public int read() throws IOException {
int b = mIn.read();
if (b != -1) {
mBuf.write(b);
}
return b;
}
@Override
public int read(byte[] b) throws IOException {
int len = mIn.read(b);
if (len > 0) {
mBuf.write(b, 0, len);
}
return len;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
len = mIn.read(b, off, len);
if (len > 0) {
mBuf.write(b, off, len);
}
return len;
}
@Override
public long skip(long n) throws IOException {
if (n <= 0) {
return mIn.skip(n);
}
byte[] buf = new byte[4096];
int len = mIn.read(buf, 0, (int) Math.min(buf.length, n));
if (len > 0) {
mBuf.write(buf, 0, len);
}
return (len < 0) ? 0 : len;
}
@Override
public int available() throws IOException {
return super.available();
}
@Override
public void close() throws IOException {
super.close();
}
@Override
public synchronized void mark(int readlimit) {}
@Override
public synchronized void reset() throws IOException {
throw new IOException("mark/reset not supported");
}
@Override
public boolean markSupported() {
return false;
}
}
}