// 
// Decompiled by Procyon v0.6.0
// 

package com.google.crypto.tink.subtle;

import java.security.spec.AlgorithmParameterSpec;
import java.security.Key;
import javax.crypto.spec.IvParameterSpec;
import java.nio.channels.WritableByteChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.io.OutputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteOrder;
import javax.crypto.Mac;
import javax.crypto.Cipher;
import java.security.InvalidAlgorithmParameterException;
import com.google.crypto.tink.StreamingAead;
import com.google.crypto.tink.streamingaead.AesCtrHmacStreamingParameters;
import com.google.crypto.tink.InsecureSecretKeyAccess;
import com.google.crypto.tink.streamingaead.AesCtrHmacStreamingKey;
import java.util.Arrays;
import java.security.GeneralSecurityException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import com.google.crypto.tink.config.internal.TinkFipsUtil;
import com.google.crypto.tink.AccessesPartialKey;

@AccessesPartialKey
public final class AesCtrHmacStreaming extends NonceBasedStreamingAead
{
    public static final TinkFipsUtil.AlgorithmFipsCompatibility FIPS;
    private static final int NONCE_SIZE_IN_BYTES = 16;
    private static final int NONCE_PREFIX_IN_BYTES = 7;
    private static final int HMAC_KEY_SIZE_IN_BYTES = 32;
    private final int keySizeInBytes;
    private final String tagAlgo;
    private final int tagSizeInBytes;
    private final int ciphertextSegmentSize;
    private final int plaintextSegmentSize;
    private final int firstSegmentOffset;
    private final String hkdfAlgo;
    private final byte[] ikm;
    
    private static Buffer toBuffer(final ByteBuffer b) {
        return b;
    }
    
    public AesCtrHmacStreaming(final byte[] ikm, final String hkdfAlgo, final int keySizeInBytes, final String tagAlgo, final int tagSizeInBytes, final int ciphertextSegmentSize, final int firstSegmentOffset) throws GeneralSecurityException {
        if (!AesCtrHmacStreaming.FIPS.isCompatible()) {
            throw new GeneralSecurityException("Can not use AES-CTR-HMAC streaming in FIPS-mode.");
        }
        validateParameters(ikm.length, keySizeInBytes, tagAlgo, tagSizeInBytes, ciphertextSegmentSize, firstSegmentOffset);
        this.ikm = Arrays.copyOf(ikm, ikm.length);
        this.hkdfAlgo = hkdfAlgo;
        this.keySizeInBytes = keySizeInBytes;
        this.tagAlgo = tagAlgo;
        this.tagSizeInBytes = tagSizeInBytes;
        this.ciphertextSegmentSize = ciphertextSegmentSize;
        this.firstSegmentOffset = firstSegmentOffset;
        this.plaintextSegmentSize = ciphertextSegmentSize - tagSizeInBytes;
    }
    
    private AesCtrHmacStreaming(final AesCtrHmacStreamingKey key) throws GeneralSecurityException {
        if (!AesCtrHmacStreaming.FIPS.isCompatible()) {
            throw new GeneralSecurityException("Can not use AES-CTR-HMAC streaming in FIPS-mode.");
        }
        this.ikm = key.getInitialKeyMaterial().toByteArray(InsecureSecretKeyAccess.get());
        String hkdfAlgString = "";
        if (key.getParameters().getHkdfHashType().equals(AesCtrHmacStreamingParameters.HashType.SHA1)) {
            hkdfAlgString = "HmacSha1";
        }
        else if (key.getParameters().getHkdfHashType().equals(AesCtrHmacStreamingParameters.HashType.SHA256)) {
            hkdfAlgString = "HmacSha256";
        }
        else if (key.getParameters().getHkdfHashType().equals(AesCtrHmacStreamingParameters.HashType.SHA512)) {
            hkdfAlgString = "HmacSha512";
        }
        this.hkdfAlgo = hkdfAlgString;
        this.keySizeInBytes = key.getParameters().getDerivedKeySizeBytes();
        String tagAlgString = "";
        if (key.getParameters().getHmacHashType().equals(AesCtrHmacStreamingParameters.HashType.SHA1)) {
            tagAlgString = "HmacSha1";
        }
        else if (key.getParameters().getHmacHashType().equals(AesCtrHmacStreamingParameters.HashType.SHA256)) {
            tagAlgString = "HmacSha256";
        }
        else if (key.getParameters().getHmacHashType().equals(AesCtrHmacStreamingParameters.HashType.SHA512)) {
            tagAlgString = "HmacSha512";
        }
        this.tagAlgo = tagAlgString;
        this.tagSizeInBytes = key.getParameters().getHmacTagSizeBytes();
        this.ciphertextSegmentSize = key.getParameters().getCiphertextSegmentSizeBytes();
        this.firstSegmentOffset = 0;
        this.plaintextSegmentSize = this.ciphertextSegmentSize - this.tagSizeInBytes;
    }
    
    public static StreamingAead create(final AesCtrHmacStreamingKey key) throws GeneralSecurityException {
        return new AesCtrHmacStreaming(key);
    }
    
    private static void validateParameters(final int ikmSize, final int keySizeInBytes, final String tagAlgo, final int tagSizeInBytes, final int ciphertextSegmentSize, final int firstSegmentOffset) throws InvalidAlgorithmParameterException {
        if (ikmSize < 16 || ikmSize < keySizeInBytes) {
            throw new InvalidAlgorithmParameterException("ikm too short, must be >= " + Math.max(16, keySizeInBytes));
        }
        if (firstSegmentOffset < 0) {
            throw new InvalidAlgorithmParameterException("firstSegmentOffset must not be negative");
        }
        Validators.validateAesKeySize(keySizeInBytes);
        if (tagSizeInBytes < 10) {
            throw new InvalidAlgorithmParameterException("tag size too small " + tagSizeInBytes);
        }
        if ((tagAlgo.equals("HmacSha1") && tagSizeInBytes > 20) || (tagAlgo.equals("HmacSha256") && tagSizeInBytes > 32) || (tagAlgo.equals("HmacSha512") && tagSizeInBytes > 64)) {
            throw new InvalidAlgorithmParameterException("tag size too big");
        }
        final int firstPlaintextSegment = ciphertextSegmentSize - firstSegmentOffset - tagSizeInBytes - keySizeInBytes - 7 - 1;
        if (firstPlaintextSegment <= 0) {
            throw new InvalidAlgorithmParameterException("ciphertextSegmentSize too small");
        }
    }
    
    @Override
    public AesCtrHmacStreamEncrypter newStreamSegmentEncrypter(final byte[] aad) throws GeneralSecurityException {
        return new AesCtrHmacStreamEncrypter(aad);
    }
    
    @Override
    public AesCtrHmacStreamDecrypter newStreamSegmentDecrypter() throws GeneralSecurityException {
        return new AesCtrHmacStreamDecrypter();
    }
    
    @Override
    public int getCiphertextSegmentSize() {
        return this.ciphertextSegmentSize;
    }
    
    @Override
    public int getPlaintextSegmentSize() {
        return this.plaintextSegmentSize;
    }
    
    @Override
    public int getHeaderLength() {
        return 1 + this.keySizeInBytes + 7;
    }
    
    @Override
    public int getCiphertextOffset() {
        return this.getHeaderLength() + this.firstSegmentOffset;
    }
    
    @Override
    public int getCiphertextOverhead() {
        return this.tagSizeInBytes;
    }
    
    public int getFirstSegmentOffset() {
        return this.firstSegmentOffset;
    }
    
    public long expectedCiphertextSize(final long plaintextSize) {
        final long offset = this.getCiphertextOffset();
        final long fullSegments = (plaintextSize + offset) / this.plaintextSegmentSize;
        long ciphertextSize = fullSegments * this.ciphertextSegmentSize;
        final long lastSegmentSize = (plaintextSize + offset) % this.plaintextSegmentSize;
        if (lastSegmentSize > 0L) {
            ciphertextSize += lastSegmentSize + this.tagSizeInBytes;
        }
        return ciphertextSize;
    }
    
    private static Cipher cipherInstance() throws GeneralSecurityException {
        return EngineFactory.CIPHER.getInstance("AES/CTR/NoPadding");
    }
    
    private Mac macInstance() throws GeneralSecurityException {
        return EngineFactory.MAC.getInstance(this.tagAlgo);
    }
    
    private byte[] randomSalt() {
        return Random.randBytes(this.keySizeInBytes);
    }
    
    private byte[] nonceForSegment(final byte[] prefix, final long segmentNr, final boolean last) throws GeneralSecurityException {
        final ByteBuffer nonce = ByteBuffer.allocate(16);
        nonce.order(ByteOrder.BIG_ENDIAN);
        nonce.put(prefix);
        SubtleUtil.putAsUnsigedInt(nonce, segmentNr);
        nonce.put((byte)(last ? 1 : 0));
        nonce.putInt(0);
        return nonce.array();
    }
    
    private byte[] randomNonce() {
        return Random.randBytes(7);
    }
    
    private byte[] deriveKeyMaterial(final byte[] salt, final byte[] aad) throws GeneralSecurityException {
        final int keyMaterialSize = this.keySizeInBytes + 32;
        return Hkdf.computeHkdf(this.hkdfAlgo, this.ikm, salt, aad, keyMaterialSize);
    }
    
    private SecretKeySpec deriveKeySpec(final byte[] keyMaterial) throws GeneralSecurityException {
        return new SecretKeySpec(keyMaterial, 0, this.keySizeInBytes, "AES");
    }
    
    private SecretKeySpec deriveHmacKeySpec(final byte[] keyMaterial) throws GeneralSecurityException {
        return new SecretKeySpec(keyMaterial, this.keySizeInBytes, 32, this.tagAlgo);
    }
    
    static {
        FIPS = TinkFipsUtil.AlgorithmFipsCompatibility.ALGORITHM_NOT_FIPS;
    }
    
    class AesCtrHmacStreamEncrypter implements StreamSegmentEncrypter
    {
        private final SecretKeySpec keySpec;
        private final SecretKeySpec hmacKeySpec;
        private final Cipher cipher;
        private final Mac mac;
        private final byte[] noncePrefix;
        private ByteBuffer header;
        private long encryptedSegments;
        
        public AesCtrHmacStreamEncrypter(final byte[] aad) throws GeneralSecurityException {
            this.encryptedSegments = 0L;
            this.cipher = cipherInstance();
            this.mac = AesCtrHmacStreaming.this.macInstance();
            this.encryptedSegments = 0L;
            final byte[] salt = AesCtrHmacStreaming.this.randomSalt();
            this.noncePrefix = AesCtrHmacStreaming.this.randomNonce();
            (this.header = ByteBuffer.allocate(AesCtrHmacStreaming.this.getHeaderLength())).put((byte)AesCtrHmacStreaming.this.getHeaderLength());
            this.header.put(salt);
            this.header.put(this.noncePrefix);
            toBuffer(this.header).flip();
            final byte[] keymaterial = AesCtrHmacStreaming.this.deriveKeyMaterial(salt, aad);
            this.keySpec = AesCtrHmacStreaming.this.deriveKeySpec(keymaterial);
            this.hmacKeySpec = AesCtrHmacStreaming.this.deriveHmacKeySpec(keymaterial);
        }
        
        @Override
        public ByteBuffer getHeader() {
            return this.header.asReadOnlyBuffer();
        }
        
        @Override
        public synchronized void encryptSegment(final ByteBuffer plaintext, final boolean isLastSegment, final ByteBuffer ciphertext) throws GeneralSecurityException {
            final int position = toBuffer(ciphertext).position();
            final byte[] nonce = AesCtrHmacStreaming.this.nonceForSegment(this.noncePrefix, this.encryptedSegments, isLastSegment);
            this.cipher.init(1, this.keySpec, new IvParameterSpec(nonce));
            ++this.encryptedSegments;
            this.cipher.doFinal(plaintext, ciphertext);
            final ByteBuffer ctCopy = ciphertext.duplicate();
            toBuffer(ctCopy).flip();
            toBuffer(ctCopy).position(position);
            this.mac.init(this.hmacKeySpec);
            this.mac.update(nonce);
            this.mac.update(ctCopy);
            final byte[] tag = this.mac.doFinal();
            ciphertext.put(tag, 0, AesCtrHmacStreaming.this.tagSizeInBytes);
        }
        
        @Override
        public synchronized void encryptSegment(final ByteBuffer part1, final ByteBuffer part2, final boolean isLastSegment, final ByteBuffer ciphertext) throws GeneralSecurityException {
            final int position = toBuffer(ciphertext).position();
            final byte[] nonce = AesCtrHmacStreaming.this.nonceForSegment(this.noncePrefix, this.encryptedSegments, isLastSegment);
            this.cipher.init(1, this.keySpec, new IvParameterSpec(nonce));
            ++this.encryptedSegments;
            this.cipher.update(part1, ciphertext);
            this.cipher.doFinal(part2, ciphertext);
            final ByteBuffer ctCopy = ciphertext.duplicate();
            toBuffer(ctCopy).flip();
            toBuffer(ctCopy).position(position);
            this.mac.init(this.hmacKeySpec);
            this.mac.update(nonce);
            this.mac.update(ctCopy);
            final byte[] tag = this.mac.doFinal();
            ciphertext.put(tag, 0, AesCtrHmacStreaming.this.tagSizeInBytes);
        }
    }
    
    class AesCtrHmacStreamDecrypter implements StreamSegmentDecrypter
    {
        private SecretKeySpec keySpec;
        private SecretKeySpec hmacKeySpec;
        private Cipher cipher;
        private Mac mac;
        private byte[] noncePrefix;
        
        @Override
        public synchronized void init(final ByteBuffer header, final byte[] aad) throws GeneralSecurityException {
            if (header.remaining() != AesCtrHmacStreaming.this.getHeaderLength()) {
                throw new InvalidAlgorithmParameterException("Invalid header length");
            }
            final byte firstByte = header.get();
            if (firstByte != AesCtrHmacStreaming.this.getHeaderLength()) {
                throw new GeneralSecurityException("Invalid ciphertext");
            }
            this.noncePrefix = new byte[7];
            final byte[] salt = new byte[AesCtrHmacStreaming.this.keySizeInBytes];
            header.get(salt);
            header.get(this.noncePrefix);
            final byte[] keymaterial = AesCtrHmacStreaming.this.deriveKeyMaterial(salt, aad);
            this.keySpec = AesCtrHmacStreaming.this.deriveKeySpec(keymaterial);
            this.hmacKeySpec = AesCtrHmacStreaming.this.deriveHmacKeySpec(keymaterial);
            this.cipher = cipherInstance();
            this.mac = AesCtrHmacStreaming.this.macInstance();
        }
        
        @Override
        public synchronized void decryptSegment(final ByteBuffer ciphertext, final int segmentNr, final boolean isLastSegment, final ByteBuffer plaintext) throws GeneralSecurityException {
            final int position = toBuffer(ciphertext).position();
            final byte[] nonce = AesCtrHmacStreaming.this.nonceForSegment(this.noncePrefix, segmentNr, isLastSegment);
            final int ctLength = ciphertext.remaining();
            if (ctLength < AesCtrHmacStreaming.this.tagSizeInBytes) {
                throw new GeneralSecurityException("Ciphertext too short");
            }
            final int ptLength = ctLength - AesCtrHmacStreaming.this.tagSizeInBytes;
            final int startOfTag = position + ptLength;
            final ByteBuffer ct = ciphertext.duplicate();
            toBuffer(ct).limit(startOfTag);
            final ByteBuffer tagBuffer = ciphertext.duplicate();
            toBuffer(tagBuffer).position(startOfTag);
            assert this.mac != null;
            assert this.hmacKeySpec != null;
            this.mac.init(this.hmacKeySpec);
            this.mac.update(nonce);
            this.mac.update(ct);
            byte[] tag = this.mac.doFinal();
            tag = Arrays.copyOf(tag, AesCtrHmacStreaming.this.tagSizeInBytes);
            final byte[] expectedTag = new byte[AesCtrHmacStreaming.this.tagSizeInBytes];
            assert tagBuffer.remaining() == AesCtrHmacStreaming.this.tagSizeInBytes;
            tagBuffer.get(expectedTag);
            assert expectedTag.length == tag.length;
            if (!Bytes.equal(expectedTag, tag)) {
                throw new GeneralSecurityException("Tag mismatch");
            }
            toBuffer(ciphertext).limit(startOfTag);
            this.cipher.init(1, this.keySpec, new IvParameterSpec(nonce));
            this.cipher.doFinal(ciphertext, plaintext);
        }
    }
}
