Image Processing
CryptImage.jpg (948 bytes) CryptImage (BMP and JPEG Lab Report

Chinese Translation by Hector Xiang

Encrypting and Decrypting a BMP File
ScreenCryptBMPa.jpg (66141 bytes)

Purpose
The purpose of the project is to show how to encrypt/decrypt BMP and JPEG image files.

Background

The image encryption/decryption technique described here will be "good enough" for many business applications, but would not be comparable to techniques used by the U.S. Government's  National Security Agency.  There are many ways to improve on the encryption technique used here.  Here are some other sources for information about encryption:

There are many ways also to improve on the efficiency of the technique described below.  The focus of this page is to explain the technique, not optimize it.

One simple way to encrypt a string of character data or binary data is to form a "random" string of bits as long as the original "message," and "exclusive or" (XOR) this random string of bits with the original message to give an "encrypted message."  If the receiver of the message knows how to form the same "random" string of bits, a second "exclusive or" of the random string of bits with the encrypted message will decrypt the message.

Let's review the mechanics of this process.  First, recall how the XOR function works with bits (0 or 1 values):

Boolean Exclusive OR (XOR) Function

x y x XOR y
0 0 0
0 1 1
1 0 1
1 1 0

Now let's start with the single-byte message "A" and the random bit string "01111010" (or $7A in hex).  The following table shows how the encryption/decryption process works:

Step Boolean Expression Hexadecimal Binary Comments
1.  ASCII "A" a $41 0100 0001 Original "message:
2.  "Random" Bits b $7A 0111 1010 Pseudo-random value from "random" number generator
3.  XOR to encrypt a XOR b $3B 0011 1011 Encrypted "message"
4.  "Random" Bits b $7A 0111 1010 Same "Random bits" as above
5.  XOR to decrypt (a XOR b) XOR b $41 0100 0001 Decrypted "message" (same as original)

A pseudo-random number generator can be used to compute "random" numbers.    If the same "seed" is used with a random number generator for both encryption and decryption processes, the same "random" sequence will be generated.  Only the "seed" must be communicated if both sender and receiver are using the same random number generator.

Borland's Online Help Note:  " Because the implementation of the Random function may change between compiler versions, we do not recommend using Random for encryption or other purposes that require reproducible sequences of pseudo-random numbers."

A variety of random-number generators exist but Delphi's Random function is used in the programs described below.  The RandSeed variable (in the System unit) is the "seed" for the random number generator.  (See the Probability section of the efg's Delphi Math Functions for alternative random number generators.)  If long-term reproducibility is critical, you should use a random number generator for which you have the source code, as recommended by Borland.

A BMP file can be encrypted and still used as a BMP file if only the scanline pixel data is encrypted and the file header is not changed.  A JPEG file once encrypted cannot be used as a JPEG file until it is properly decrypted.   Study the BMP encryption/decryption process first, since it is easier to understand.  Then study the JPEG encryption/decryption process.


BMP Files


BMP Background

A BMP file (or a TBitmap in memory) consists of a header record of various information and the scanlines with pixel data.  If the header record of a BMP is encrypted, the BMP can no longer be treated as a bitmap.  If only the scanlines of pixel data are encrypted -- with no change in the header record --   the resulting encrypted BMP can still be displayed as a bitmap. 

Only the scanline pixel data are encrypted in this project so the resulting TBitmap/BMP file can still be used as an image.  If present, the palette could also be encrypted, but that is not covered here.

See the Scanline Tech Note for information about how to access pixel data within a TBitmap.

Materials and Equipment

Software Requirements
Windows 95/98
Delphi 3/4/5 (to recompile)
CryptBMP.EXE

Hardware Requirements
VGA display with 640-by-480 screen in high/true color display mode

Procedure

  1. Double click on the CryptBMP.EXE icon to start the program.
  2. Press the Load button and select a BMP file (not provided).  Press the Open button.
  3. If desired, uncheck the "stretch" button.  Normally, the image is stretched to fit the space available.  In some cases, such as with small images, this may not be desirable.
  4. Press the Encrypt button to display the encrypted image.  This encrypted image can be saved to a BMP file by selecting the Save button. 
  5. Experiment with the Encyrpt and Decrypt Seed Numbers.  As shown below, the decrypted image will not match the original when these two numbers are not the same.

ScreenCryptBMPb.jpg (73691 bytes)

Note that the Decrypt button is more of a label than a button that does anything.  The decryption process is automatically called after any encryption, or when the decrypt seed number is changed.

Discussion
Consult the complete source code for all the details, but the main Encryption/Decryption routines are shown here.

The EncryptImage method looks at each scanline, regardless of PixelFormat, and XORs a random bit string with the original pixel data.  The resulting encrypted image is displayed in the ImageEncrypted TImage.

For palletized images, the original palette is copied to the encrypted image.

Encrypt BMP File
// Don't bother trying to understand structure of pixels within scanline.
// Just find length of scanline in bytes and process all bytes.
PROCEDURE TFormCrypt.EncryptImage;
  VAR
    i                :  INTEGER;
    j                :  INTEGER;
    RandomValue      :  BYTE;
    rowIn            :  pByteArray;
    rowOut           :  pByteArray;
    ScanlineByteCount:  INTEGER;
BEGIN
  IF   Assigned(BitmapEncrypted)
  THEN BitmapEncrypted.Free;
  BitmapEncrypted             := TBitmap.Create;
  BitmapEncrypted.Width       := BitmapOriginal.Width;
  BitmapEncrypted.Height      := BitmapOriginal.Height;
  BitmapEncrypted.PixelFormat := BitmapOriginal.PixelFormat;
  // Copy palette if palletized image
  IF   BitmapOriginal.PixelFormat IN [pf1bit, pf4bit, pf8bit]
  THEN BitmapEncrypted.Palette := CopyPalette(BitmapOriginal.Palette);
  // This finds the number of bytes per scanline regardless of PixelFormat
  ScanlineByteCount := ABS(Integer(BitmapOriginal.Scanline[1]) -
                           Integer(BitmapOriginal.Scanline[0]));
  TRY
    RandSeed := StrToInt(EditSeedEncrypt.Text)
  EXCEPT
    RandSeed := 79997  // use this prime number if entry is invalid
  END;
  FOR j := 0 TO BitmapOriginal.Height-1 DO
  BEGIN
    RowIn  := BitmapOriginal.Scanline[j];
    RowOut := BitmapEncrypted.Scanline[j];
    FOR i := 0 TO ScanlineByteCount-1 DO
    BEGIN
      RandomValue := Random(256);    //  0..255 value
      RowOut[i]   := RowIn[i] XOR RandomValue
    END
  END;
  ImageEncrypted.Picture.Graphic := BitmapEncrypted;
  DecryptImage;
  ButtonDecrypt.Enabled := TRUE;
  ButtonSave.Enabled := TRUE
END {EncryptImage}; 

See Andreas Filsinger's original  summary of this encryption method and an updated version for Delphi 6.01.

The DecryptImage method works much like the EncryptImage routine.  

Decrypt BMP File

PROCEDURE TFormCrypt.DecryptImage;
  VAR
    BitmapDecrypted  :  TBitmap;
    i                :  INTEGER;
    j                :  INTEGER;
    RandomValue      :  BYTE;
    rowIn            :  pByteArray;
    rowOut           :  pByteArray;
    ScanlineByteCount:  INTEGER;
BEGIN
  BitmapDecrypted             := TBitmap.Create;
  BitmapDecrypted.Width       := BitmapEncrypted.Width;
  BitmapDecrypted.Height      := BitmapEncrypted.Height;
  BitmapDecrypted.PixelFormat := BitmapEncrypted.PixelFormat;
  // Copy palette if palletized image
  IF   BitmapEncrypted.PixelFormat IN [pf1bit, pf4bit, pf8bit]
  THEN BitmapDecrypted.Palette := CopyPalette(BitmapEncrypted.Palette);
  // This finds the number of bytes per scanline regardless of PixelFormat
  ScanlineByteCount := ABS(Integer(BitmapEncrypted.Scanline[1]) -
                           Integer(BitmapEncrypted.Scanline[0]));
  TRY
    RandSeed := StrToInt(EditSeedDecrypt.Text)
  EXCEPT
    RandSeed := 79997  // use this prime number if entry is invalid
  END;
  FOR j := 0 TO BitmapEncrypted.Height-1 DO
  BEGIN
    RowIn  := BitmapEncrypted.Scanline[j];
    RowOut := BitmapDecrypted.Scanline[j];
    FOR i := 0 TO ScanlineByteCount-1 DO
    BEGIN
      RandomValue := Random(256);    //  0..255 value
      RowOut[i]  := RowIn[i]  XOR RandomValue
    END
  END;
  ImageDecrypted.Picture.Graphic := BitmapDecrypted;
  EditSeedDecrypt.Enabled := TRUE;
END {DecryptImage};

In the pf24bit example shown at the top of this page, the encrypted image gives no hint as to what colors might be present in the original image.

For palletized images (i.e., pf1bit, pf4bit, pf8bit), the palette is copied from the original image to the encrypted image and only the scanlines are encrypted.   Because some information about the image is contained in the palette, encrypting the palette may also be a good idea, but that was not done in this project. 

For example, only the colors of the pf1bit "Smiley" (from the Single-Bit Bitmaps Lab Report) are present in the encrypted form (see below).  Hiding these colors by encrypting the palette entries may be desirable.

"Smiley" Encrypted Smiley
smiley.gif (1026 bytes) SmileyCrypt.gif (1259 bytes)

Using a different "Encrypt Seed Number" for each and every encrypted  image is quite important.  If the same Encrypt Seed Number is used for two images, some information about both images can be learned without the Encrypt Seed Number.  For example, assume you have two images, A and B:

Original Images
A B

If you encrypt both of these images with the Encrypt Seed Number 19937, the results seem to hide the images:

Images Encrypted Using Seed 19937
A19937 B19937

But now if you take both of these images, and without any knowledge of the original encryption key, perform certain operations with the images, some information about the original images can be seen.  For example, if you use XOR with corresponding color components for each pixel, (R,G,B) = (R1 XOR R2, G1 XOR G2, B1 XOR B2), some information about the original can be extracted.  In particular, many traces of image B can be seen:

A19937 XOR B19937

This image is the equivalent to XORing the original images A and B:

A XOR B

If we assume R is the random sequence of bits, this is the math that explains why the randomness does not hide the composite image, A XOR B:

(A XOR R) XOR (B XOR R) = A XOR B

The solution is to use unique random sequences with A and B:

(A XOR R1) XOR (B XOR R2)  <>  A XOR B

This emphasizes why a unique key should be used with each image.  If Image A had been encrypted with the key, 66547, the image A66547 looks much like A19937: 

A66547 A19937

With the unique key, the A XOR B operations are quite different:

A66547 XOR B19937 A19937 XOR B19937

(Thanks to Christian Berger for stressing this limitation in a posting to the Borland Community site.)


JPEG Files


JPG Background

Unlike the BMP file, manipulating the "pixel" scanlines separately from any JPG "header" information is not possible (at least without modifying the existing Delphi TJPEGImage definition).  So a different approach must be used.   With JPGs, the whole file is encrypted.  But this means that the resulting file cannot be treated as  a JPG image. 

Materials and Equipment

Software Requirements
Windows 95/98
Delphi 3/4/5 (to recompile)
CryptJPEG.EXE

Hardware Requirements
VGA display with 640-by-480 screen in high/true color display mode

Procedure

  1. Double click on the CryptJPEG.EXE icon to start the program.
  2. Press the Load button and select a JPG file (Flower.JPG is provided).  Press the Open button.
  3. If desired, uncheck the "stretch" button.  Normally, the image is stretched to fit the space available.  In some cases, such as with small images, this may not be desirable.
  4. Press the Encrypt button to display the encrypted image (see discussion below).  This encrypted image can be saved to a .Binary file by selecting the Save BIN button. 
  5. Experiment with the Encyrpt and Decrypt Seed Numbers.  When the number match, the encrypted image will be correctly decrypted.
  6. Load a previously saved .Binary file by pressing the Load BIN button.   If the Decrypt Seed is correct, this encrypted .Binary file can be decrypted and displayed.

ScreenCryptJPEG.jpg (36751 bytes)

Discussion
Consult the complete source code for all the details, but the main Encryption/Decryption routines are shown here.

The technique shown here for a JPEG file could be used with any graphics file, such as GIFs, or any other type of file.  Unlike the BMP encryption/decryption process described above, once a JPEG file is encrypted, it cannot be displayed as an image file.  The encrypted JPEG file is a file of binary data that must be decrypted before it can be used in any way.

The processing of loading a BMP file wasn't explained above since it was so straightforward.  However, in addition to the "normal" process of loading and displaying a TJPEGImage in a TImage, the JPEG file is loaded in to a JPEGOriginalBinary TMemoryStream for later processing by the encryption routine.

Load JPEG Image

procedure TFormCrypt.ButtonLoadJPGClick(Sender: TObject);
  VAR
    JPEGOriginal:  TJPEGImage;
begin
  IF   OpenPictureDialog.Execute
  THEN BEGIN
    // Load JPEG Image for Display
    JPEGOriginal := TJPEGImage.Create;
    TRY
      JPEGOriginal.LoadFromFile(OpenPictureDialog.Filename);
      ImageOriginal.Picture.Graphic := JPEGOriginal
    FINALLY
      JPEGOriginal.Free
    END;
    // Load JPEG Image as Binary Stream
    IF   Assigned(JPEGOriginalBinary)
    THEN JPEGOriginalBinary.Free;
    JPEGOriginalBinary := TMemoryStream.Create;
    JPEGOriginalBinary.LoadFromFile(OpenPictureDialog.Filename);
    ButtonEncrypt.Enabled := TRUE;
    EditSeedEncrypt.Enabled := TRUE;
  END
end;

With the BMP file, the EncryptImage method looked at each scanline.  Here with a JPEG, the whole file is treated as a binary stream of data and encrypted byte-by-byte to form a new binary stream, JPEGEncryptedBinary (a TMemoryStream).

The binary data in a TMemoryStream is processed by obtaining a pointer to the beginning of the stream, such as in

    pIn  := JPEGOriginalBinary.Memory;

The pointer is incremented, INC(pIn), as each byte is processed by XORing it with a byte from a sequence of "random" bytes.

Encrypt JPEG File

// Encrypt bytes in JPEGOriginalBinary TMemoryStream to give JPEGEncryptedBinary
PROCEDURE TFormCrypt.EncryptImage;
  VAR
    BitmapDisplay:  TBitmap;
    i            :  INTEGER;
    pIn          :  pByte;
    pOut         :  pByte;
    RandomValue  :  BYTE;
BEGIN
  TRY
    RandSeed := StrToInt(EditSeedEncrypt.Text)
  EXCEPT
    RandSeed := 67547  // use this prime number if entry is invalid
  END;
  IF   Assigned(JPEGEncryptedBinary)
  THEN JPEGEncryptedBinary.Free;
  JPEGEncryptedBinary := TMemoryStream.Create;
  // Encrypted version same size as the original version
  JPEGEncryptedBinary.Size := JPEGOriginalBinary.Size;
  pIn  := JPEGOriginalBinary.Memory;
  pOut := JPEGEncryptedBinary.Memory;
  FOR i := 1 TO JPEGOriginalBinary.Size DO
  BEGIN
    RandomValue := Random(256);       //  0..255
    pOut^ := pIn^ XOR RandomValue;
    INC(pIn);
    INC(pOut)
  END;
  // JPEGEncryptedBinary cannot be displayed as an image.  So, let's just
  // create a "noise" bitmap to display instead.  The "seed" for this noise
  // image will be the RandSeed left over from the JPEG encryption, so the
  // same noise image will be created for a given JPEG.
  BitmapDisplay := CreateNoiseImage(ImageEncrypted.Width,
                                    ImageEncrypted.Height);
  TRY
    ImageEncrypted.Picture.Graphic := BitmapDisplay;
  FINALLY
    BitmapDisplay.Free
  END;
  DecryptImage;
  ButtonDecrypt.Enabled := TRUE;
  ButtonSaveBIN.Enabled := TRUE
END {EncryptImage};

Unlike the encrypted BMP file, the encrypted JPEG binary stream cannot be displayed as an image.  To display something for this encrypted file, a "noise" image was displayed instead, which was created as shown next:

Create Noise Image

// Create pf24bit "noise" image using random numbers
FUNCTION CreateNoiseImage(CONST Width, Height:  INTEGER):  TBitmap;
  VAR
    i  :  INTEGER;
    j  :  INTEGER;
    row:  pByteArray;
BEGIN
  RESULT             := TBitmap.Create;
  RESULT.Width       := Width;
  RESULT.Height      := Height;
  RESULT.PixelFormat := pf24bit;
  FOR j := 0 TO Height-1 DO
  BEGIN
    row := RESULT.Scanline[j];
    FOR i := 0 TO 3*Width-1 DO   // 3 bytes per pixel
    BEGIN
      row[i] := Random(256)
    END
  END
END {CreateNoiseImage};

The DecryptImage method works much like the EncryptImage routine.   After  the decrypted binary stream is formed, JPEGDecryptedBinary, this stream is loaded into a new TJPEGImage.  If there is any exception in this process (e.g., the decrypted file never was a JPEG file, or the decryption did no result in a JPEG file), then a "noise" image is displayed.

Decrypt JPEG File

PROCEDURE TFormCrypt.DecryptImage;
  VAR
    BitmapDisplay      :  TBitmap;
    i                  :  INTEGER;
    JPEGDecrypted      :  TJPEGImage;
    JPEGDecryptedBinary:  TMemoryStream;
    pIn                :  pByte;
    pOut               :  pByte;
    RandomValue        :  BYTE;
BEGIN
  TRY
    RandSeed := StrToInt(EditSeedDecrypt.Text)
  EXCEPT
    RandSeed := 67547  // use this prime number if entry is invalid
  END;
  JPEGDecryptedBinary := TMemoryStream.Create;
  TRY
    // Decrypted version same size as the encrypted version
    JPEGDecryptedBinary.Size := JPEGEncryptedBinary.Size;
    pIn  := JPEGEncryptedBinary.Memory;
    pOut := JPEGDecryptedBinary.Memory;
    FOR i := 1 TO JPEGEncryptedBinary.Size DO
    BEGIN
      RandomValue := Random(256);       //  0..255
      pOut^ := pIn^ XOR RandomValue;
      INC(pIn);
      INC(pOut)
    END;
    // At this point the decrypted JPEG binary stream is in the
    // JPEGDecryptedBinary stream.  Load this memory stream into the
    // JPEGDecrypted TJPEGImage.
    JPEGDecrypted := TJPEGImage.Create;
    TRY
      TRY
        JPEGDecrypted.LoadFromStream(JPEGDecryptedBinary);
        ImageDecrypted.Picture.Graphic := JPEGDecrypted;
      EXCEPT
        // If any error occurs in the conversion, just show a noise image
        BitmapDisplay := CreateNoiseImage(ImageEncrypted.Width,
                                          ImageEncrypted.Height);
        TRY
          ImageDecrypted.Picture.Graphic := BitmapDisplay;
        FINALLY
          BitmapDisplay.Free
        END;
      END
    FINALLY
      JPEGDecrypted.Free
    END
  FINALLY
    JPEGDecryptedBinary.Free
  END;
  EditSeedDecrypt.Enabled := TRUE;
END {DecryptImage};

Conclusions
Encryption using a sequence of random bytes and XOR is a fairly simple process.  A similar technique can be applied to other types of files.

An encrypted BMP file can still be displayed as a BMP file. An encrypted JPEG file is a binary stream that cannot be treated as a JPEG image until (and unless) it is correctly decrypted.


Thanks to Morten Jacobsen of Norway for asking how an image could be encrypted.


Keywords
XOR, Random, RandSeed, BMP, JPEG, TBitmap, TJEGImage, Scanline, PixelFormat, Palette, CopyPalette, TMemoryStream, Noise Image, pByte, pByteArray,   TMemoryStream.Memory, LoadFromFile, LoadFromStream, ShellExecute, EditNumericKeyPress

Download
Delphi 3/4/5 Source and EXE:  CryptBMP.ZIP (138 KB) and CryptJPEG.ZIP (191 KB)


Updated 18 Feb 2002


since 12 Dec 1999