2
votes

I am trying to implement client side encryption/decryption on Android platform by referencing IOS implementations. I am wrestling with the question that encryption and decryption on Android and IOS platform are different even they have used the same algorithm. Let`s say, when Android device encrypt and upload a file to server, the IOS device couldn`t download and decrypt it correctly.

The algorithm I am using

  1. Encrypt the file key with the user provided password. We first use PBKDF2 algorithm (1000 iteratioins of SHA256) to derive a key/iv pair from the password, then use AES 256/CBC to encrypt the file key. The result is called the "encrypted file key". This encrypted file key will be sent to and stored on the server. When you need to access the data, you can decrypt the file key from the encrypted file key.
  2. All file data is encrypted by the file key with AES 256/CBC. We use PBKDF2 algorithm (1000 iterations of SHA256) to derive key/iv pair from the file key.
  3. store the derived key/iv pair locally and use them to encrypt files. After encryption, the data is uploaded to the server. The same with decrypt files when downloading files.

Android code

    private static final String TAG = Crypto.class.getSimpleName();

    private static final String CIPHER_ALGORITHM = "AES/CBC/NoPadding";

    private static int KEY_LENGTH = 32;
    private static int KEY_LENGTH_SHORT = 16;
    // minimum values recommended by PKCS#5, increase as necessary
    private static int ITERATION_COUNT = 1000;
    // Should generate random salt for each repo
    private static byte[] salt = {(byte) 0xda, (byte) 0x90, (byte) 0x45, (byte) 0xc3, (byte) 0x06, (byte) 0xc7, (byte) 0xcc, (byte) 0x26};

    private Crypto() {
    }

    /**
     * decrypt repo encKey
     *
     * @param password
     * @param randomKey
     * @param version
     * @return
     * @throws UnsupportedEncodingException
     * @throws NoSuchAlgorithmException
     */
    public static String deriveKeyPbkdf2(String password, String randomKey, int version) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        if (TextUtils.isEmpty(password) || TextUtils.isEmpty(randomKey)) {
            return null;
        }

        PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA256Digest());
        gen.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password.toCharArray()), salt, ITERATION_COUNT);
        byte[] keyBytes;

        if (version == 2) {
            keyBytes = ((KeyParameter) gen.generateDerivedMacParameters(KEY_LENGTH * 8)).getKey();
        } else
            keyBytes = ((KeyParameter) gen.generateDerivedMacParameters(KEY_LENGTH_SHORT * 8)).getKey();

        SecretKey realKey = new SecretKeySpec(keyBytes, "AES");

        final byte[] iv = deriveIVPbkdf2(realKey.getEncoded());

        return seafileDecrypt(fromHex(randomKey), realKey, iv);
    }

    public static byte[] deriveIVPbkdf2(byte[] key) throws UnsupportedEncodingException {
        PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA256Digest());
        gen.init(key, salt, 10);
        return ((KeyParameter) gen.generateDerivedMacParameters(KEY_LENGTH_SHORT * 8)).getKey();
    }

    /**
     * All file data is encrypted by the file key with AES 256/CBC.
     *
     * We use PBKDF2 algorithm (1000 iterations of SHA256) to derive key/iv pair from the file key.
     * After encryption, the data is uploaded to the server.
     *
     * @param plaintext
     * @param key
     * @return
     */
    private static byte[] seafileEncrypt(byte[] plaintext, SecretKey key, byte[] iv) {
        try {
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);

            IvParameterSpec ivParams = new IvParameterSpec(iv);
            cipher.init(Cipher.ENCRYPT_MODE, key, ivParams);

            return cipher.doFinal(plaintext);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            Log.e(TAG, "NoSuchAlgorithmException " + e.getMessage());
            return null;
        } catch (InvalidKeyException e) {
            e.printStackTrace();
            Log.e(TAG, "InvalidKeyException " + e.getMessage());
            return null;
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
            Log.e(TAG, "InvalidAlgorithmParameterException " + e.getMessage());
            return null;
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
            Log.e(TAG, "NoSuchPaddingException " + e.getMessage());
            return null;
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
            Log.e(TAG, "IllegalBlockSizeException " + e.getMessage());
            return null;
        } catch (BadPaddingException e) {
            e.printStackTrace();
            Log.e(TAG, "BadPaddingException " + e.getMessage());
            return null;
        }
    }

    /**
     * All file data is encrypted by the file key with AES 256/CBC.
     *
     * We use PBKDF2 algorithm (1000 iterations of SHA256) to derive key/iv pair from the file key.
     * After encryption, the data is uploaded to the server.
     *
     * @param plaintext
     * @param key
     * @return
     */
    public static byte[] encrypt(byte[] plaintext, String key, byte[] iv, int version) throws NoSuchAlgorithmException {
        PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(new SHA256Digest());
        gen.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(key.toCharArray()), salt, ITERATION_COUNT);
        byte[] keyBytes;

        if (version == 2) {
            keyBytes = ((KeyParameter) gen.generateDerivedMacParameters(KEY_LENGTH * 8)).getKey();
        } else
            keyBytes = ((KeyParameter) gen.generateDerivedMacParameters(KEY_LENGTH_SHORT * 8)).getKey();

        SecretKey realKey = new SecretKeySpec(keyBytes, "AES");
        return seafileEncrypt(plaintext, realKey , iv);
    }

IOS code

+ (int)deriveKey:(const char *)data_in inlen:(int)in_len version:(int)version key:(unsigned char *)key iv:(unsigned char *)iv
{
    unsigned char salt[8] = { 0xda, 0x90, 0x45, 0xc3, 0x06, 0xc7, 0xcc, 0x26 };
    if (version == 2) {
        PKCS5_PBKDF2_HMAC (data_in, in_len,
                           salt, sizeof(salt),
                           1000,
                           EVP_sha256(),
                           32, key);
        PKCS5_PBKDF2_HMAC ((char *)key, 32,
                           salt, sizeof(salt),
                           10,
                           EVP_sha256(),
                           16, iv);
        return 0;
    } else if (version == 1)
        return EVP_BytesToKey (EVP_aes_128_cbc(), /* cipher mode */
                               EVP_sha1(),        /* message digest */
                               salt,              /* salt */
                               (unsigned char*)data_in,
                               in_len,
                               1 << 19,   /* iteration times */
                               key, /* the derived key */
                               iv); /* IV, initial vector */
    else
        return EVP_BytesToKey (EVP_aes_128_ecb(), /* cipher mode */
                               EVP_sha1(),        /* message digest */
                               NULL,              /* salt */
                               (unsigned char*)data_in,
                               in_len,
                               3,   /* iteration times */
                               key, /* the derived key */
                               iv); /* IV, initial vector */
}

+(int)seafileEncrypt:(char **)data_out outlen:(int *)out_len datain:(const char *)data_in inlen:(const int)in_len version:(int)version key:(uint8_t *)key iv:(uint8_t *)iv
{
    int ret, blks;
    EVP_CIPHER_CTX ctx;
    EVP_CIPHER_CTX_init (&ctx);
    if (version == 2)
        ret = EVP_EncryptInit_ex (&ctx,
                                  EVP_aes_256_cbc(), /* cipher mode */
                                  NULL, /* engine, NULL for default */
                                  key,  /* derived key */
                                  iv);  /* initial vector */
    else if (version == 1)
        ret = EVP_EncryptInit_ex (&ctx,
                                  EVP_aes_128_cbc(), /* cipher mode */
                                  NULL, /* engine, NULL for default */
                                  key,  /* derived key */
                                  iv);  /* initial vector */
    else
        ret = EVP_EncryptInit_ex (&ctx,
                                  EVP_aes_128_ecb(), /* cipher mode */
                                  NULL, /* engine, NULL for default */
                                  key,  /* derived key */
                                  iv);  /* initial vector */
    if (ret == DEC_FAILURE)
        return -1;

    blks = (in_len / BLK_SIZE) + 1;
    *data_out = (char *)malloc (blks * BLK_SIZE);
    if (*data_out == NULL) {
        Debug ("failed to allocate the output buffer.\n");
        goto enc_error;
    }
    int update_len, final_len;

    /* Do the encryption. */
    ret = EVP_EncryptUpdate (&ctx,
                             (unsigned char*)*data_out,
                             &update_len,
                             (unsigned char*)data_in,
                             in_len);

    if (ret == ENC_FAILURE)
        goto enc_error;


    /* Finish the possible partial block. */
    ret = EVP_EncryptFinal_ex (&ctx,
                               (unsigned char*)*data_out + update_len,
                               &final_len);

    *out_len = update_len + final_len;

    /* out_len should be equal to the allocated buffer size. */
    if (ret == ENC_FAILURE || *out_len != (blks * BLK_SIZE))
        goto enc_error;

    EVP_CIPHER_CTX_cleanup (&ctx);

    return 0;

enc_error:
    EVP_CIPHER_CTX_cleanup (&ctx);
    *out_len = -1;

    if (*data_out != NULL)
        free (*data_out);

    *data_out = NULL;

    return -1;
}

+ (void)generateKey:(NSString *)password version:(int)version encKey:(NSString *)encKey key:(uint8_t *)key iv:(uint8_t *)iv
{
    unsigned char key0[32], iv0[16];
    char passwordPtr[256] = {0}; // room for terminator (unused)
    [password getCString:passwordPtr maxLength:sizeof(passwordPtr) encoding:NSUTF8StringEncoding];
    if (version < 2) {
        [NSData deriveKey:passwordPtr inlen:(int)password.length version:version key:key iv:iv];
        return;
    }
    [NSData deriveKey:passwordPtr inlen:(int)password.length version:version key:key0 iv:iv0];
    char enc_random_key[48], dec_random_key[48];
    int outlen;
    hex_to_rawdata(encKey.UTF8String, enc_random_key, 48);
    [NSData seafileDecrypt:dec_random_key outlen:&outlen datain:(char *)enc_random_key inlen:48 version:2 key:key0 iv:iv0];
    [NSData deriveKey:dec_random_key inlen:32 version:2 key:key iv:iv];
}

- (NSData *)encrypt:(NSString *)password encKey:(NSString *)encKey version:(int)version
{
    uint8_t key[kCCKeySizeAES256+1] = {0}, iv[kCCKeySizeAES128+1];
    [NSData generateKey:password version:version encKey:encKey key:key iv:iv];
    char *data_out;
    int outlen;
    int ret = [NSData seafileEncrypt:&data_out outlen:&outlen datain:self.bytes inlen:(int)self.length version:version key:key iv:iv];
    if (ret < 0) return nil;
    return [NSData dataWithBytesNoCopy:data_out length:outlen];
}

Here is the complete project for anyone who found this useful.

  1. Support client side encryption #487
  2. How does an encrypted library work?
1
"couldn't decrypt correctly". Super vague. Give more information, exceptions, etc. - Luke Joshua Park
@LukePark the exception is Android device only can encrypt/decrypt the file which was processed by itself before, not the file processed by IOS device. I didn`t find any clue yet, because I am not familiar with Objective-C (or C) language crypto. - Logan Guo
You're unlikely to get a useful answer as your question contains too much code. Please do some more analysis. Your code consists of several steps. Use the same input data and compare the output of each step. That way you can figure out where the differences between iOS and Android really occur. Once you have better isolated the problem, post a more specific question. - Codo
@Codo that makes sense. - Logan Guo
Recently, I posted an acceptable answer in stackoverflow.com/questions/40421146/…. - Julian Corrêa

1 Answers

3
votes

Hi me too had the same problem.

I found 2 things causing mismatch for my code: 1. ios and android encryption algorithms was not same(i have requested PKCS7Padding algorithm in ios and i tried NoPadding and AES/CBC/PKCS5Padding algorithms in android. 2. The key generated in ios is different than in Android.

Please see my worked code both gives same output in ios and android:

IOS:

- (NSString *) encryptString:(NSString*)plaintext withKey:(NSString*)key {
NSData *data = [[plaintext dataUsingEncoding:NSUTF8StringEncoding] AES256EncryptWithKey:key];
return [data base64EncodedStringWithOptions:kNilOptions];
}
- (NSString *) decryptString:(NSString *)ciphertext withKey:(NSString*)key {
if ([ciphertext isKindOfClass:[NSString class]]) { 

    NSData *data = [[NSData alloc] initWithBase64EncodedString:ciphertext options:kNilOptions];
    return [[NSString alloc] initWithData:[data AES256DecryptWithKey:key] encoding:NSUTF8StringEncoding];
}
return nil;
}

Android:(We modified aes w.r.t iOS default method)

private static String Key = "your key"; 
public static String encryptString(String stringToEncode) throws NullPointerException {

    try {
        SecretKeySpec skeySpec = getKey(Key);
        byte[] clearText = stringToEncode.getBytes("UTF8");
        final byte[] iv = new byte[16];
        Arrays.fill(iv, (byte) 0x00);
        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
        cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivParameterSpec);
        String encrypedValue = Base64.encodeToString(cipher.doFinal(clearText), Base64.DEFAULT);
        return encrypedValue;

    } catch (Exception e) {
        e.printStackTrace();
    }
    return "";
}
public static String encryptString(String stringToEncode) throws NullPointerException {

    try {
        SecretKeySpec skeySpec = getKey(Key);
        byte[] clearText = stringToEncode.getBytes("UTF8");
        final byte[] iv = new byte[16];
        Arrays.fill(iv, (byte) 0x00);
        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
        cipher.init(Cipher.DECRYPT_MODE, skeySpec, ivParameterSpec);
        byte[] cipherData = cipher.doFinal(Base64.decode(stringToEncode.getBytes("UTF-8"), Base64.DEFAULT));
        String decoded = new String(cipherData, "UTF-8");
        return decoded;

    } catch (Exception e) {
        e.printStackTrace();
    }
    return "";
}


private static SecretKeySpec getKey(String password) throws UnsupportedEncodingException {
    int keyLength = 256;
    byte[] keyBytes = new byte[keyLength / 8];
    Arrays.fill(keyBytes, (byte) 0x0);
    byte[] passwordBytes = password.getBytes("UTF-8");
    int length = passwordBytes.length < keyBytes.length ? passwordBytes.length : keyBytes.length;
    System.arraycopy(passwordBytes, 0, keyBytes, 0, length);
    SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
    return key;
}

Hope this help u guys Thanks