/**
   * Performs decryption operation for the last time.
   *
   * <p>NOTE: For cipher feedback modes which does not perform special handling for the last few
   * blocks, this is essentially the same as <code>encrypt(...)</code>. Given most modes do not do
   * special handling, the default impl for this method is to simply call <code>decrypt(...)</code>.
   *
   * @param in the input buffer with the data to be decrypted
   * @param inOfs the offset in <code>cipher</code>
   * @param len the length of the input data
   * @param out the buffer for the decryption result
   * @param outOfs the offset in <code>plain</code>
   * @return the number of bytes placed into the <code>out</code> buffer
   */
  int decryptFinal(byte[] in, int inOfs, int len, byte[] out, int outOfs)
      throws IllegalBlockSizeException, AEADBadTagException, ShortBufferException {
    if (len < tagLenBytes) {
      throw new AEADBadTagException("Input too short - need tag");
    }
    if (out.length - outOfs < ((ibuffer.size() + len) - tagLenBytes)) {
      throw new ShortBufferException("Output buffer too small");
    }
    processAAD();
    if (len != 0) {
      ibuffer.write(in, inOfs, len);
    }

    // refresh 'in' to all buffered-up bytes
    in = ibuffer.toByteArray();
    inOfs = 0;
    len = in.length;
    ibuffer.reset();

    byte[] tag = new byte[tagLenBytes];
    // get the trailing tag bytes from 'in'
    System.arraycopy(in, len - tagLenBytes, tag, 0, tagLenBytes);
    len -= tagLenBytes;

    if (len > 0) {
      doLastBlock(in, inOfs, len, out, outOfs, false);
    }

    byte[] lengthBlock = getLengthBlock(sizeOfAAD * 8, processed * 8);
    ghashAllToS.update(lengthBlock);

    byte[] s = ghashAllToS.digest();
    byte[] sOut = new byte[s.length];
    GCTR gctrForSToTag = new GCTR(embeddedCipher, this.preCounterBlock);
    gctrForSToTag.doFinal(s, 0, s.length, sOut, 0);
    for (int i = 0; i < tagLenBytes; i++) {
      if (tag[i] != sOut[i]) {
        throw new AEADBadTagException("Tag mismatch!");
      }
    }
    return len;
  }
  /**
   * Performs encryption operation for the last time.
   *
   * @param in the input buffer with the data to be encrypted
   * @param inOfs the offset in <code>in</code>
   * @param len the length of the input data
   * @param out the buffer for the encryption result
   * @param outOfs the offset in <code>out</code>
   * @return the number of bytes placed into the <code>out</code> buffer
   */
  int encryptFinal(byte[] in, int inOfs, int len, byte[] out, int outOfs)
      throws IllegalBlockSizeException, ShortBufferException {
    if (out.length - outOfs < (len + tagLenBytes)) {
      throw new ShortBufferException("Output buffer too small");
    }

    processAAD();
    if (len > 0) {
      doLastBlock(in, inOfs, len, out, outOfs, true);
    }

    byte[] lengthBlock = getLengthBlock(sizeOfAAD * 8, processed * 8);
    ghashAllToS.update(lengthBlock);
    byte[] s = ghashAllToS.digest();
    byte[] sOut = new byte[s.length];
    GCTR gctrForSToTag = new GCTR(embeddedCipher, this.preCounterBlock);
    gctrForSToTag.doFinal(s, 0, s.length, sOut, 0);

    System.arraycopy(sOut, 0, out, (outOfs + len), tagLenBytes);
    return (len + tagLenBytes);
  }
  // Utility to process the last block; used by encryptFinal and decryptFinal
  void doLastBlock(byte[] in, int inOfs, int len, byte[] out, int outOfs, boolean isEncrypt)
      throws IllegalBlockSizeException {
    // process data in 'in'
    gctrPAndC.doFinal(in, inOfs, len, out, outOfs);
    processed += len;

    byte[] ct;
    int ctOfs;
    if (isEncrypt) {
      ct = out;
      ctOfs = outOfs;
    } else {
      ct = in;
      ctOfs = inOfs;
    }
    int lastLen = len % AES_BLOCK_SIZE;
    if (lastLen != 0) {
      ghashAllToS.update(ct, ctOfs, len - lastLen);
      byte[] padded = expandToOneBlock(ct, (ctOfs + len - lastLen), lastLen);
      ghashAllToS.update(padded);
    } else {
      ghashAllToS.update(ct, ctOfs, len);
    }
  }