// Copyright 2017 Google Inc.
//
// 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.google.crypto.tink;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.security.GeneralSecurityException;

An interface for streaming authenticated encryption with associated data.

Streaming encryption is typically used for encrypting large plaintexts such as large files. Tink may eventually contain multiple interfaces for streaming encryption depending on the supported properties. This interface supports a streaming interface for symmetric encryption with authentication. The underlying encryption modes are selected so that partial plaintext can be obtained fast by decrypting and authenticating just a part of the ciphertext.

Security guarantees

Instances of StreamingAead must follow the OAE2 definition as proposed in the paper "Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance" by Hoang, Reyhanitabar, Rogaway and Vizár https://eprint.iacr.org/2015/189.pdf

Restrictions

Encryption must be done in one session. There is no possibility to modify an existing ciphertext or append to it (other than reencrypt the whole file again). One reason for this restriction is the use of AES-GCM as one cipher to implement this interface. If single segments are modified then this is equivalent to reusing the same IV twice, but reusing an IV twice leaks an AES-GCM key. Another reason is that implementations of this interface have no protection against roll-back attacks: an attacker can always try to restore a previous version of the file without detection.

Blocking vs non-blocking I/O

A channel can be in a blocking mode (i.e. always waits until the requested number of bytes have been processed) or non-blocking mode (i.e. I/O operation will never block and may transfer fewer bytes than were requested or possibly no bytes at all).

If the channel provided to the streaming encryption is in blocking mode then encryption and decryption have the same property. That is, encryption always processes all the plaintext passed in, and waits until complete segments have been written to the ciphertext channel (incomplete segment, if any, is buffered). Similarly, decryption blocks until sufficiently many bytes have been read from the ciphertext channel so that all the requested plaintext can be decrypted and authenticated, or until the end of the plaintext has been reached, or an IOException occurred.

If the channel provided to the streaming encryption is in non-blocking mode, then encryption and decryption are also non-blocking. Since encryption and decryption is done in segments it is possible that for example a call attempting to read() returns no plaintext at all even if partial ciphertext was read from the underlying channel.

Sample encryption


StreamingAead s = ...
java.nio.channels.FileChannel ciphertextDestination =
    new FileOutputStream(ciphertextFile).getChannel();
byte[] aad = ...
WritableByteChannel encryptingChannel = s.newEncryptingChannel(ciphertextDestination, aad);
while ( ... ) {
  int r = encryptingChannel.write(buffer);
  ...
 }
encryptingChannel.close();

Sample full decryption


StreamingAead s = ...
java.nio.channels.FileChannel ciphertextSource =
    new FileInputStream(ciphertextFile).getChannel();
byte[] aad = ...
ReadableByteChannel decryptingChannel = s.newDecryptingChannel(ciphertextSource, aad);
int chunkSize = ...
ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
do {
  buffer.clear();
  int cnt = decryptingChannel.read(buffer);
  if (cnt > 0) {
    // Process cnt bytes of plaintext.
  } else if (read == -1) {
    // End of plaintext detected.
    break;
  } else if (read == 0) {
    // No ciphertext is available at the moment.
  }
 }
Since:1.1.0
/** * An interface for streaming authenticated encryption with associated data. * * <p>Streaming encryption is typically used for encrypting large plaintexts such as large files. * Tink may eventually contain multiple interfaces for streaming encryption depending on the * supported properties. This interface supports a streaming interface for symmetric encryption with * authentication. The underlying encryption modes are selected so that partial plaintext can be * obtained fast by decrypting and authenticating just a part of the ciphertext. * * <h3>Security guarantees</h3> * * <p>Instances of StreamingAead must follow the OAE2 definition as proposed in the paper "Online * Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance" by Hoang, Reyhanitabar, Rogaway * and Vizár https://eprint.iacr.org/2015/189.pdf * * <h3>Restrictions</h3> * * <p>Encryption must be done in one session. There is no possibility to modify an existing * ciphertext or append to it (other than reencrypt the whole file again). One reason for this * restriction is the use of AES-GCM as one cipher to implement this interface. If single segments * are modified then this is equivalent to reusing the same IV twice, but reusing an IV twice leaks * an AES-GCM key. Another reason is that implementations of this interface have no protection * against roll-back attacks: an attacker can always try to restore a previous version of the file * without detection. * * <h3>Blocking vs non-blocking I/O</h3> * * <p>A channel can be in a blocking mode (i.e. always waits until the requested number of bytes * have been processed) or non-blocking mode (i.e. I/O operation will never block and may transfer * fewer bytes than were requested or possibly no bytes at all). * * <p>If the channel provided to the streaming encryption is in blocking mode then encryption and * decryption have the same property. That is, encryption always processes all the plaintext passed * in, and waits until complete segments have been written to the ciphertext channel (incomplete * segment, if any, is buffered). Similarly, decryption blocks until sufficiently many bytes have * been read from the ciphertext channel so that all the requested plaintext can be decrypted and * authenticated, or until the end of the plaintext has been reached, or an IOException occurred. * * <p>If the channel provided to the streaming encryption is in non-blocking mode, then encryption * and decryption are also non-blocking. Since encryption and decryption is done in segments it is * possible that for example a call attempting to read() returns no plaintext at all even if partial * ciphertext was read from the underlying channel. * * <h3>Sample encryption</h3> * * <pre>{@code * StreamingAead s = ... * java.nio.channels.FileChannel ciphertextDestination = * new FileOutputStream(ciphertextFile).getChannel(); * byte[] aad = ... * WritableByteChannel encryptingChannel = s.newEncryptingChannel(ciphertextDestination, aad); * while ( ... ) { * int r = encryptingChannel.write(buffer); * ... * } * encryptingChannel.close(); * }</pre> * * <h3>Sample full decryption</h3> * * <pre>{@code * StreamingAead s = ... * java.nio.channels.FileChannel ciphertextSource = * new FileInputStream(ciphertextFile).getChannel(); * byte[] aad = ... * ReadableByteChannel decryptingChannel = s.newDecryptingChannel(ciphertextSource, aad); * int chunkSize = ... * ByteBuffer buffer = ByteBuffer.allocate(chunkSize); * do { * buffer.clear(); * int cnt = decryptingChannel.read(buffer); * if (cnt > 0) { * // Process cnt bytes of plaintext. * } else if (read == -1) { * // End of plaintext detected. * break; * } else if (read == 0) { * // No ciphertext is available at the moment. * } * } * }</pre> * * @since 1.1.0 */
public interface StreamingAead {
Returns a WritableByteChannel for plaintext. Any data written to the returned channel will be encrypted and the resulting ciphertext written to the provided ciphertextDestination
Params:
  • ciphertextDestination – the channel to which the ciphertext is written.
  • associatedData – data associated with the plaintext. This data is authenticated but not encrypted. It must be passed into the decryption.
/** * Returns a WritableByteChannel for plaintext. Any data written to the returned * channel will be encrypted and the resulting ciphertext written to the provided * {@code ciphertextDestination} * * @param ciphertextDestination the channel to which the ciphertext is written. * @param associatedData data associated with the plaintext. This data is authenticated * but not encrypted. It must be passed into the decryption. */
WritableByteChannel newEncryptingChannel( WritableByteChannel ciphertextDestination, byte[] associatedData) throws GeneralSecurityException, IOException;
Returns a SeekableByteChannel that allows to access the plaintext.

This method does not work on Android Marshmallow (API level 23) or older because these Android versions don't have the java.nio.channels.SeekableByteChannel interface.

Params:
  • ciphertextSource – the ciphertext
  • associatedData – the data associated with the ciphertext.
Throws:
  • GeneralSecurityException – if the header of the ciphertext is corrupt or if associatedData is not correct.
  • IOException – if an IOException occurred while reading from ciphertextDestination.
Returns:SeekableByteChannel that allows random read access to the plaintext. The following methods of SeekableByteChannel are implemented:
  • long position() Returns the channel's position in the plaintext.
  • SeekableByteChannel position(long newPosition) Sets the channel's position. Setting the position to a value greater than the plaintext size is legal. A later attempt to read byte will immediately return an end-of-file indication.
  • int read(ByteBuffer dst) Bytes are read starting at the channel's position, and then the position is updated with the number of bytes actually read. All bytes returned have been authenticated. If the end of the stream has been reached -1 is returned. A result of -1 is authenticated (e.g. by checking the MAC of the last ciphertext chunk.) A call to this function attempts to fill dst, but it may return fewer bytes than requested, e.g. if the underlying ciphertextSource does not provide the requested number of bytes or if the plaintext ended.

    Throws IOException if a MAC verification failed. TODO: Should we extend the interface with read(ByteBuffer dst, long position) to avoid race conditions?

  • long size() Returns the size of the plaintext. TODO: Decide whether the result should be authenticated)
  • SeekableByteChannel truncate(long size) throws NonWritableChannelException because the channel is read-only.
  • int write(ByteBuffer src) throws NonWritableChannelException because the channel is read-only.
  • close() closes the channel
  • isOpen()
/** * Returns a SeekableByteChannel that allows to access the plaintext. * * <p>This method does not work on Android Marshmallow (API level 23) or older because these * Android versions don't have the java.nio.channels.SeekableByteChannel interface. * * @param ciphertextSource the ciphertext * @param associatedData the data associated with the ciphertext. * @return {@link SeekableByteChannel} that allows random read access to the plaintext. The * following methods of SeekableByteChannel are implemented: * <ul> * <li>{@code long position()} Returns the channel's position in the plaintext. * <li>{@code SeekableByteChannel position(long newPosition)} Sets the channel's position. * Setting the position to a value greater than the plaintext size is legal. A later * attempt to read byte will immediately return an end-of-file indication. * <li>{@code int read(ByteBuffer dst)} Bytes are read starting at the channel's position, * and then the position is updated with the number of bytes actually read. All bytes * returned have been authenticated. If the end of the stream has been reached -1 is * returned. A result of -1 is authenticated (e.g. by checking the MAC of the last * ciphertext chunk.) A call to this function attempts to fill dst, but it may return * fewer bytes than requested, e.g. if the underlying ciphertextSource does not provide * the requested number of bytes or if the plaintext ended. * <p>Throws {@link IOException} if a MAC verification failed. TODO: Should we extend * the interface with read(ByteBuffer dst, long position) to avoid race conditions? * <li>{@code long size()} Returns the size of the plaintext. TODO: Decide whether the * result should be authenticated) * <li>{@code SeekableByteChannel truncate(long size)} throws {@link * NonWritableChannelException} because the channel is read-only. * <li>{@code int write(ByteBuffer src)} throws {@link NonWritableChannelException} because * the channel is read-only. * <li>{@code close()} closes the channel * <li>{@code isOpen()} * </ul> * * @throws GeneralSecurityException if the header of the ciphertext is corrupt or if * associatedData is not correct. * @throws IOException if an IOException occurred while reading from ciphertextDestination. */
SeekableByteChannel newSeekableDecryptingChannel( SeekableByteChannel ciphertextSource, byte[] associatedData) throws GeneralSecurityException, IOException; ReadableByteChannel newDecryptingChannel( ReadableByteChannel ciphertextSource, byte[] associatedData) throws GeneralSecurityException, IOException;
Returns a wrapper around ciphertextDestination, such that any write-operation via the wrapper results in AEAD-encryption of the written data, using associatedData as associated authenticated data. The associated data is not included in the ciphertext and has to be passed in as parameter for decryption.
/** * Returns a wrapper around {@code ciphertextDestination}, such that any write-operation via * the wrapper results in AEAD-encryption of the written data, using {@code associatedData} * as associated authenticated data. The associated data is not included in the ciphertext * and has to be passed in as parameter for decryption. */
OutputStream newEncryptingStream(OutputStream ciphertextDestination, byte[] associatedData) throws GeneralSecurityException, IOException;
Returns a wrapper around ciphertextSource, such that any read-operation via the wrapper results in AEAD-decryption of the underlying ciphertext, using associatedData as associated authenticated data.

The returned InputStream may support mark()/reset(), but does not have to do it -- markSupported() provides the corresponding info.

The returned InputStream supports skip(), yet possibly in an inefficient way, i.e. by reading a sequence of blocks until the desired position. If a more efficient skip()-functionality is needed, the Channel-based API can be used.

/** * Returns a wrapper around {@code ciphertextSource}, such that any read-operation * via the wrapper results in AEAD-decryption of the underlying ciphertext, * using {@code associatedData} as associated authenticated data. * * <p>The returned InputStream may support {@code mark()}/{@code reset()}, * but does not have to do it -- {@code markSupported()} provides the corresponding info. * * <p>The returned InputStream supports {@code skip()}, yet possibly in an inefficient way, * i.e. by reading a sequence of blocks until the desired position. If a more efficient * {@code skip()}-functionality is needed, the Channel-based API can be used. */
InputStream newDecryptingStream(InputStream ciphertextSource, byte[] associatedData) throws GeneralSecurityException, IOException; }