Mobile Development 16 min read

Why Do WebP Photos Corrupt on Android? A Deep Dive into AES Encryption Bugs

An Android app that stores shop‑sign photos began corrupting WebP images at a stable 1/200 rate, leading to a thorough investigation of the storage module, AES encryption implementation, and thread safety, ultimately uncovering a faulty string‑length check that broke binary image handling.

ITPUB
ITPUB
ITPUB
Why Do WebP Photos Corrupt on Android? A Deep Dive into AES Encryption Bugs

Project Background

The author works on an Android app that captures shop‑sign photos. Over time, users reported that some photos became corrupted after being saved.

Symptoms

Corruption occurs across all task types, regardless of the number of photos.

Approximately 1 out of every 200 photos is damaged.

Only images stored in WebP format exhibit the issue; JPEG images are fine.

Initial Investigation

The team first mapped the storage workflow (see flowchart) and listed three possible failure points:

WebP compression error.

Logic error in the code path.

Faulty encryption algorithm.

Direction 1 – WebP Compression

By saving the raw, unencrypted WebP file alongside the encrypted version, it was confirmed that the original WebP image displayed correctly, ruling out the camera and compression step.

Direction 2 – Encryption Process

The app uses AES encryption on the image file before writing it to disk. The author reviewed AES basics and examined the existing C++ implementation, which encrypts/decrypts a fixed‑size buffer.

Key observations:

The decryption routine reads the encrypted file into memory, decrypts it, writes the plaintext back to the same file, then the image loader reads the file again and re‑encrypts it.

This non‑atomic sequence can leave the file in a partially decrypted state if the app crashes.

Concurrent threads could decrypt an already‑decrypted file a second time, producing random data.

Revised Decryption Logic

The decryption was changed to operate entirely in memory, returning a ByteArrayInputStream without touching the file system:

@Override
protected InputStream getImageStream(ImageDecodingInfo decodingInfo) throws IOException {
    boolean isLoaclFile = decodingInfo.getOriginalImageUri().startsWith("file://");
    if (!isLoaclFile) {
        return super.getImageStream(decodingInfo);
    }
    InputStream imageStream = super.getImageStream(decodingInfo);
    byte[] encodeByteArray = inputStreamToByteArray(imageStream);
    final int ENCRYPTED_SIZE = 1024;
    byte[] decodeBuffer = new byte[ENCRYPTED_SIZE];
    System.arraycopy(encodeByteArray, 0, decodeBuffer, 0, ENCRYPTED_SIZE);
    decodeBuffer = JniArithmetic.aesDecryptNoPadding(decodeBuffer);
    System.arraycopy(decodeBuffer, 0, encodeByteArray, 0, ENCRYPTED_SIZE);
    return new ByteArrayInputStream(encodeByteArray);
}

After removing all “decrypt‑then‑encrypt” cycles, the issue persisted, prompting further analysis.

Direction 3 – Thread‑Safety

A simple custom AES implementation (byte‑wise +1 / -1) was tested; varying its execution time never reproduced the corruption, suggesting the problem lay elsewhere.

Direction 4 – Image‑Specific Investigation

Extracted corrupted images were examined. The original WebP file starts with the RIFF header. After encryption, the first byte becomes random; after decryption, the result is often random as well.

Two test cases were performed:

Encrypting a clean image produced the expected encrypted output.

Decrypting that encrypted file sometimes yielded the original image, but often produced random data.

Server‑side Java AES decryption succeeded consistently, indicating the client‑side C++ decryption was flawed.

Direction 5 – AES Decryption Code Review

The C++ repository (historically migrated from SVN to Git) contains a custom AES implementation. The critical function strToUChar checks for an empty C‑style string using strlen(ch) == 0 and returns an error code, which the caller ignores.

int AES::strToUChar(const char *ch, unsigned char *uch, int len) {
    if (ch == NULL || uch == NULL) return -1;
    if (strlen(ch) == 0) return -2; // <-- problematic
    while (len) {
        *uch++ = (int)*ch;
        ch++; len--;
    }
    *uch = (int) '\0';
    return 0;
}

Binary image data can legitimately start with a 0x00 byte. Treating this as a null‑terminated string causes the function to abort, leaving the buffer partially processed and resulting in random output after decryption. This explains the 1/256 failure rate (matching the probability of a leading zero byte).

Root Cause and Fix

Removing the empty‑string check eliminated the false‑negative path:

int AES::strToUChar(const char *ch, unsigned char *uch, int len) {
    if (ch == NULL || uch == NULL) return -1;
    // if (strlen(ch) == 0) return -2; // removed
    while (len) {
        *uch++ = (int)*ch;
        ch++; len--;
    }
    *uch = (int) '\0';
    return 0;
}

Another similar check in charToHex was also removed. After rebuilding and testing, no further image corruption was observed.

Summary and Lessons Learned

Validate input parameters early and return clear error codes.

Design low‑level modules to be format‑agnostic; avoid assumptions that work only for text.

Be aware of hidden state and race conditions when repeatedly encrypting/decrypting the same file.

When fixing a bug, search for duplicated logic that may suffer the same issue.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

DebuggingMobile DevelopmentAndroidwebpAES encryptionImage Corruption
ITPUB
Written by

ITPUB

Official ITPUB account sharing technical insights, community news, and exciting events.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.