/*
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
*/
package com.facebook.crypto.streams;
import com.facebook.crypto.cipher.NativeGCMCipher;
import java.io.IOException;
import java.io.InputStream;
/**
* This class is used to encapsulate decryption using GCM. On reads, bytes are first read from the
* delegate input stream and decrypted before being store in the read buffer.
*/
public class NativeGCMCipherInputStream extends InputStream {
private static final int SKIP_BUFFER_SIZE = 256;
private final TailInputStream mCipherDelegate;
private final NativeGCMCipher mCipher;
private byte[] mSkipBuffer;
private boolean mTagChecked = false;
/**
* Creates a new input stream to read from.
*
* @param cipherDelegate The stream to read encrypted bytes from.
* @param cipher The cipher used to decrypt the bytes.
*/
public NativeGCMCipherInputStream(InputStream cipherDelegate, NativeGCMCipher cipher, int tagLength) {
mCipherDelegate = new TailInputStream(cipherDelegate, tagLength);
mCipher = cipher;
}
@Override
public int available() throws IOException {
return mCipherDelegate.available();
}
@Override
public void close() throws IOException {
try {
ensureTagValid();
} finally {
mCipherDelegate.close();
}
}
@Override
public void mark(int readlimit) {
throw new UnsupportedOperationException();
}
@Override
public boolean markSupported() {
return false;
}
@Override
public int read() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public int read(byte[] buffer) throws IOException {
return read(buffer, 0, buffer.length);
}
@Override
public int read(byte[] buffer, int offset, int length)
throws IOException {
if (buffer.length < offset + length) {
throw new ArrayIndexOutOfBoundsException(offset + length);
}
int read = mCipherDelegate.read(buffer, offset, length);
if (read == -1) {
// since we have reached the end of the input stream we should
// verify whether the tag of the data we've read in is valid.
ensureTagValid();
return -1;
}
read = mCipher.update(buffer, offset, read, buffer, offset);
return read;
}
private void ensureTagValid() throws IOException {
if (mTagChecked) {
return;
}
// sets it to true before executing it, since we put the cipher into a finalized
// state and destroy it, so we should not execute this again.
mTagChecked = true;
try {
byte[] tail = mCipherDelegate.getTail();
mCipher.decryptFinal(tail, tail.length);
} finally {
mCipher.destroy();
}
}
@Override
public synchronized void reset() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public long skip(long byteCount) throws IOException {
if (mSkipBuffer == null) {
mSkipBuffer = new byte[SKIP_BUFFER_SIZE];
}
// implements skip through reading
// decryption needs to process all the data anyway
// only marginal optimization would be avoiding jni to copy back plain bytes
// but that's only a problem for android that copies bytes instead of sharing
long skipped = 0;
while (byteCount > 0) {
int chunk = (int) Math.min(byteCount, SKIP_BUFFER_SIZE);
int read = read(mSkipBuffer, 0, chunk);
if (read < 0) {
break;
}
skipped += read;
byteCount -= read;
}
// if it didn't skip anything it's EOF
return skipped == 0 ? -1 : skipped;
}
}