blob: 454d24e02592ecb6ddfdb57e8ef8cb7ab2f828fd [file] [log] [blame]
// 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.subtle;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
/**
* Abstract base class for ChaCha20 and XChaCha20.
*
* <p>ChaCha20 and XChaCha20 have two differences: the size of the nonce and the initial state of
* the block function that produces a key stream block from a key, a nonce, and a counter.
*
* <p>Concrete implementations of this class are meant to be used to construct an {@link
* com.google.crypto.tink.Aead} with {@link com.google.crypto.tink.subtle.Poly1305}.
*/
abstract class ChaCha20Base implements IndCpaCipher {
public static final int BLOCK_SIZE_IN_INTS = 16;
public static final int BLOCK_SIZE_IN_BYTES = BLOCK_SIZE_IN_INTS * 4;
public static final int KEY_SIZE_IN_INTS = 8;
public static final int KEY_SIZE_IN_BYTES = KEY_SIZE_IN_INTS * 4;
private static final int[] SIGMA =
toIntArray(
new byte[] {
'e', 'x', 'p', 'a', 'n', 'd', ' ', '3', '2', '-', 'b', 'y', 't', 'e', ' ', 'k'
});
int[] key;
private final int initialCounter;
ChaCha20Base(final byte[] key, int initialCounter) throws InvalidKeyException {
if (key.length != KEY_SIZE_IN_BYTES) {
throw new InvalidKeyException("The key length in bytes must be 32.");
}
this.key = toIntArray(key);
this.initialCounter = initialCounter;
}
/** Returns the initial state from {@code nonce} and {@code counter}. */
abstract int[] createInitialState(final int[] nonce, int counter);
/**
* The size of the randomly generated nonces.
*
* <p>ChaCha20 uses 12-byte nonces, but XChaCha20 use 24-byte nonces.
*/
abstract int nonceSizeInBytes();
@Override
public byte[] encrypt(final byte[] plaintext) throws GeneralSecurityException {
if (plaintext.length > Integer.MAX_VALUE - nonceSizeInBytes()) {
throw new GeneralSecurityException("plaintext too long");
}
ByteBuffer ciphertext = ByteBuffer.allocate(nonceSizeInBytes() + plaintext.length);
encrypt(ciphertext, plaintext);
return ciphertext.array();
}
void encrypt(ByteBuffer output, final byte[] plaintext) throws GeneralSecurityException {
if (output.remaining() - nonceSizeInBytes() < plaintext.length) {
throw new IllegalArgumentException("Given ByteBuffer output is too small");
}
byte[] nonce = Random.randBytes(nonceSizeInBytes());
output.put(nonce);
process(nonce, output, ByteBuffer.wrap(plaintext));
}
@Override
public byte[] decrypt(final byte[] ciphertext) throws GeneralSecurityException {
return decrypt(ByteBuffer.wrap(ciphertext));
}
byte[] decrypt(ByteBuffer ciphertext) throws GeneralSecurityException {
if (ciphertext.remaining() < nonceSizeInBytes()) {
throw new GeneralSecurityException("ciphertext too short");
}
byte[] nonce = new byte[nonceSizeInBytes()];
ciphertext.get(nonce);
ByteBuffer plaintext = ByteBuffer.allocate(ciphertext.remaining());
process(nonce, plaintext, ciphertext);
return plaintext.array();
}
private void process(final byte[] nonce, ByteBuffer output, ByteBuffer input)
throws GeneralSecurityException {
int length = input.remaining();
int numBlocks = (length / BLOCK_SIZE_IN_BYTES) + 1;
for (int i = 0; i < numBlocks; i++) {
ByteBuffer keyStreamBlock = chacha20Block(nonce, i + initialCounter);
if (i == numBlocks - 1) {
// last block
Bytes.xor(output, input, keyStreamBlock, length % BLOCK_SIZE_IN_BYTES);
} else {
Bytes.xor(output, input, keyStreamBlock, BLOCK_SIZE_IN_BYTES);
}
}
}
// https://tools.ietf.org/html/rfc8439#section-2.3.
ByteBuffer chacha20Block(final byte[] nonce, int counter) {
int[] state = createInitialState(toIntArray(nonce), counter);
int[] workingState = state.clone();
shuffleState(workingState);
for (int i = 0; i < state.length; i++) {
state[i] += workingState[i];
}
ByteBuffer out = ByteBuffer.allocate(BLOCK_SIZE_IN_BYTES).order(ByteOrder.LITTLE_ENDIAN);
out.asIntBuffer().put(state, 0, BLOCK_SIZE_IN_INTS);
return out;
}
static void setSigmaAndKey(int[] state, final int[] key) {
System.arraycopy(SIGMA, 0, state, 0, SIGMA.length);
System.arraycopy(key, 0, state, SIGMA.length, KEY_SIZE_IN_INTS);
}
static void shuffleState(final int[] state) {
for (int i = 0; i < 10; i++) {
quarterRound(state, 0, 4, 8, 12);
quarterRound(state, 1, 5, 9, 13);
quarterRound(state, 2, 6, 10, 14);
quarterRound(state, 3, 7, 11, 15);
quarterRound(state, 0, 5, 10, 15);
quarterRound(state, 1, 6, 11, 12);
quarterRound(state, 2, 7, 8, 13);
quarterRound(state, 3, 4, 9, 14);
}
}
static void quarterRound(int[] x, int a, int b, int c, int d) {
x[a] += x[b];
x[d] = rotateLeft(x[d] ^ x[a], 16);
x[c] += x[d];
x[b] = rotateLeft(x[b] ^ x[c], 12);
x[a] += x[b];
x[d] = rotateLeft(x[d] ^ x[a], 8);
x[c] += x[d];
x[b] = rotateLeft(x[b] ^ x[c], 7);
}
static int[] toIntArray(final byte[] input) {
IntBuffer intBuffer = ByteBuffer.wrap(input).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
int[] ret = new int[intBuffer.remaining()];
intBuffer.get(ret);
return ret;
}
private static int rotateLeft(int x, int y) {
return (x << y) | (x >>> -y);
}
}