GordianSimonEngine.java

/*
 * GordianKnot: Security Suite
 * Copyright 2012-2026. Tony Washer
 *
 * 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 io.github.tonywasher.joceanus.gordianknot.impl.ext.engines;

import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.Pack;

/**
 * Simon Cipher engine.
 * <p>Cut down version of Tim Whittington's implementation available at
 * https://github.com/timw/bc-java/blob/feature/simon-speck/core/src/main/java/org/bouncycastle/crypto/engines/SimonEngine.java
 * </p>
 */
public class GordianSimonEngine
        implements BlockCipher {
    /**
     * Number of rounds.
     */
    private static final byte[] ROUNDS = {68, 69, 72};

    /**
     * Pre-computed z0...z4 round constants.
     */
    private static final byte[][] Z = {
            {1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0,
                    0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1,
                    0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0},
            {1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0,
                    0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1,
                    1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0},
            {1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0,
                    1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0,
                    0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1},
            {1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0,
                    1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0,
                    1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1},
            {1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0,
                    1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0,
                    1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 1}
    };

    /**
     * Number of words in state.
     */
    private static final int NUMWORDS = 2;

    /**
     * BlockSize.
     */
    private static final int BLOCKSIZE = NUMWORDS * Long.BYTES;

    /**
     * Number of words in 128-bit key.
     */
    private static final int NUMWORDS128 = 2;

    /**
     * Number of words in 192-bit key.
     */
    private static final int NUMWORDS192 = 3;

    /**
     * Number of words in 256-bit key.
     */
    private static final int NUMWORDS256 = 4;

    /**
     * Rotate1.
     */
    private static final int ROT1 = 1;

    /**
     * Rotate2.
     */
    private static final int ROT2 = 2;

    /**
     * Rotate3.
     */
    private static final int ROT3 = 3;

    /**
     * Rotate8.
     */
    private static final int ROT8 = 8;

    /**
     * The # of rounds.
     */
    private int theRounds;

    /**
     * The expanded key schedule.
     */
    private long[] theRoundKeys;

    /**
     * Are we encrypting?
     */
    private boolean forEncryption;

    @Override
    public void init(final boolean pEncrypt,
                     final CipherParameters pParams) {
        /* Reject invalid parameters */
        if (!(pParams instanceof KeyParameter)) {
            throw new IllegalArgumentException("Invalid parameter passed to Speck init - "
                    + pParams.getClass().getName());
        }

        /* Validate keyLength */
        final byte[] myKey = ((KeyParameter) pParams).getKey();
        final int myKeyLen = myKey.length;
        if ((((myKeyLen << 1) % BLOCKSIZE) != 0)
                || myKeyLen < BLOCKSIZE
                || myKeyLen > (BLOCKSIZE << 1)) {
            throw new IllegalArgumentException("KeyBitSize must be 128, 192 or 256");
        }

        /* Generate the round keys */
        forEncryption = pEncrypt;
        generateRoundKeys(myKey);
    }

    @Override
    public void reset() {
        /* NoOp */
    }

    @Override
    public String getAlgorithmName() {
        return "Simon";
    }

    @Override
    public int getBlockSize() {
        return BLOCKSIZE;
    }

    @Override
    public int processBlock(final byte[] pInput,
                            final int pInOff,
                            final byte[] pOutput,
                            final int pOutOff) {
        /* Check buffers */
        if (pInput == null || pInput.length - pInOff < BLOCKSIZE) {
            throw new IllegalArgumentException("Invalid input buffer");
        }
        if (pOutput == null || pOutput.length - pOutOff < BLOCKSIZE) {
            throw new IllegalArgumentException("Invalid output buffer");
        }

        /* Perform the encryption/decryption */
        return forEncryption
                ? encryptBlock(pInput, pInOff, pOutput, pOutOff)
                : decryptBlock(pInput, pInOff, pOutput, pOutOff);
    }

    /**
     * Encrypt a block.
     *
     * @param pInput  the input buffer
     * @param pInOff  the input offset
     * @param pOutput the output offset
     * @param pOutOff the output offset
     * @return the bytes processed
     */
    private int encryptBlock(final byte[] pInput,
                             final int pInOff,
                             final byte[] pOutput,
                             final int pOutOff) {
        /* Load the bytes into the block */
        long myX = Pack.bigEndianToLong(pInput, pInOff);
        long myY = Pack.bigEndianToLong(pInput, pInOff + Long.BYTES);

        /* Loop through the rounds */
        for (int i = 0; i < theRounds; i++) {
            /* Perform the encryption round */
            final long myTmp = myX;
            myX = myY ^ (rol64(myX, ROT1) & rol64(myX, ROT8)) ^ rol64(myX, ROT2) ^ theRoundKeys[i];
            myY = myTmp;
        }

        /* Output the bytes from the block */
        Pack.longToBigEndian(myX, pOutput, pOutOff);
        Pack.longToBigEndian(myY, pOutput, pOutOff + Long.BYTES);

        /* Return # of bytes processed */
        return BLOCKSIZE;
    }

    /**
     * Decrypt a block.
     *
     * @param pInput  the input buffer
     * @param pInOff  the input offset
     * @param pOutput the output offset
     * @param pOutOff the output offset
     * @return the bytes processed
     */
    private int decryptBlock(final byte[] pInput,
                             final int pInOff,
                             final byte[] pOutput,
                             final int pOutOff) {
        /* Load the bytes into the block */
        long myX = Pack.bigEndianToLong(pInput, pInOff);
        long myY = Pack.bigEndianToLong(pInput, pInOff + Long.BYTES);

        /* Loop through the rounds */
        for (int i = theRounds - 1; i >= 0; i--) {
            /* Perform the decryption round */
            final long myTmp = myY;
            myY = myX ^ (rol64(myY, ROT1) & rol64(myY, ROT8)) ^ rol64(myY, ROT2) ^ theRoundKeys[i];
            myX = myTmp;
        }

        /* Output the bytes from the block */
        Pack.longToBigEndian(myX, pOutput, pOutOff);
        Pack.longToBigEndian(myY, pOutput, pOutOff + Long.BYTES);

        /* Return # of bytes processed */
        return BLOCKSIZE;
    }

    /**
     * Generate the round keys.
     *
     * @param pKey the key
     */
    private void generateRoundKeys(final byte[] pKey) {
        /* Determine number of key words */
        final int numWords = pKey.length / Long.BYTES;
        final byte[] myConstants = Z[numWords];

        /* Determine # of rounds and allocate round keys */
        theRounds = ROUNDS[numWords - NUMWORDS128];
        theRoundKeys = new long[theRounds];

        /* Load the key */
        for (int i = 0; i < numWords; i++) {
            theRoundKeys[i] = Pack.bigEndianToLong(pKey, (numWords - i - 1) * Long.BYTES);
        }

        /* Key expansion */
        for (int i = numWords; i < theRounds; i++) {
            long tmp = ror64(theRoundKeys[i - 1], ROT3);
            if (numWords == NUMWORDS256) {
                tmp ^= theRoundKeys[i - NUMWORDS192];
            }
            tmp = tmp ^ ror64(tmp, ROT1);
            theRoundKeys[i] = tmp ^ theRoundKeys[i - numWords]
                    ^ myConstants[(i - numWords) % myConstants.length] ^ ~NUMWORDS192;
        }
    }

    /**
     * rotate left.
     *
     * @param pValue the value to rotate
     * @param pBits  the # of bits to rotate
     * @return the rotated value
     */
    private static long rol64(final long pValue,
                              final long pBits) {
        return (pValue << pBits) | (pValue >>> (Long.SIZE - pBits));
    }

    /**
     * rotate right.
     *
     * @param pValue the value to rotate
     * @param pBits  the # of bits to rotate
     * @return the rotated value
     */
    private static long ror64(final long pValue,
                              final long pBits) {
        return (pValue >>> pBits) | (pValue << (Long.SIZE - pBits));
    }
}