package io.kaif.token; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.Key; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Base64; import java.util.Base64.Encoder; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.ShortBufferException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; /** * iOS use AES/CBC/PKCS7Padding instead of PKCS#5, for key length <= 256bit, PKCS#7 and PKCS#5 are * identical */ class Encryptor { private static final int KEY_LENGTH = 16; final static Encoder URL_SAFE_BASE64_ENCODER = Base64.getUrlEncoder().withoutPadding(); /** * key are 16 bytes, with 16 bytes-zero iv */ public static Encryptor create(final byte[] key) { final byte[] iv = new byte[KEY_LENGTH]; Arrays.fill(iv, (byte) 0x00); return create(key, iv); } /** * both key and iv are 16 bytes * * @param key * @param iv * 16 bytes initial vector */ public static Encryptor create(final byte[] key, final byte[] iv) { try { final IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); return new Encryptor(key, "AES/CBC/PKCS5Padding", ivParameterSpec); } catch (final GeneralSecurityException e) { e.printStackTrace(); throw new RuntimeException("unsupported type of encryption: AES"); } } private final Cipher encryptCipher; private final Key key; private Encryptor(final byte[] rawKeyData, final String algorithm, final IvParameterSpec ivParameterSpec) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException { key = new SecretKeySpec(rawKeyData, "AES"); encryptCipher = Cipher.getInstance(algorithm); encryptCipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec); } public byte[] encrypt(final byte[] rawData) throws GeneralSecurityException { if (rawData == null) { return null; } return encryptCipher.doFinal(rawData); } public String encrypt(final String rawData) throws GeneralSecurityException { if (rawData == null) { return null; } try { return URL_SAFE_BASE64_ENCODER.encodeToString(encrypt(rawData.getBytes("UTF-8"))); } catch (final UnsupportedEncodingException e) { throw new RuntimeException(e); } } public void encryptInto(final byte[] rawData, final int inputLength, final ByteBuffer out) throws ShortBufferException, GeneralSecurityException { if (rawData == null) { out.clear(); return; } final int outSize = encryptCipher.doFinal(rawData, 0, inputLength, out.array()); out.limit(outSize); } public int estimateByteSize(final int inputByteSize) { return encryptCipher.getOutputSize(inputByteSize); } /** * reset cipher to initialized state, with updated IV * * @param iv * 16 bytes initial vector */ public void resetIvParameter(final byte[] iv, final int offset) { try { encryptCipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv, offset, KEY_LENGTH)); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { throw new RuntimeException("unexpected", e); } } }