Encrypted messages - SATSA MIDlet

This section provides the source code for the SATSA MIDlet. For the complete Eclipse project ZIP file , see Forum Nokia.

The example includes the following classes:

SATSAMIDlet

package satsa;

import java.security.GeneralSecurityException;
import javax.crypto.BadPaddingException;
import javax.microedition.lcdui.Display;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;

// Main SATSA MIDlet class
// It controls the state of the user interface
// and handles the encryption and decryption process
public class SATSAMIDlet extends MIDlet {
  private Display display;    
  private MessageStore messageStore;
  private ListScreen listScreen;
  private EncryptScreen encryptScreen;
  private PasswordScreen passwordScreen;
  private DecryptScreen decryptScreen;
  private InfoScreen infoScreen;
  private NewScreen newScreen;
  private Codec codec;

  public SATSAMIDlet() {}

  protected void destroyApp(boolean unconditional)
    throws MIDletStateChangeException
  {
    exitMIDlet();
  }

  protected void pauseApp()
  {
  }

  protected void startApp()
    throws MIDletStateChangeException
  {
    if (display == null)
    {
      initMIDlet();
    }
  }

  // Initialize all the internal fields
  private void initMIDlet()
  {
    // Init the user interface
    display = Display.getDisplay(this);
    listScreen = new ListScreen(this);
    encryptScreen = new EncryptScreen(this);
    passwordScreen = new PasswordScreen(this);
    decryptScreen = new DecryptScreen(this);
    infoScreen = new InfoScreen();
    newScreen = new NewScreen(this);
    
    // Initialize the Cryptographic Codec and the message store
    codec = new Codec(this);
    messageStore = new MessageStore(this);
    
    // Startup the UI
    showMessageList();
  }

  // Shows the list of existing messages
  void showMessageList()
  {
    messageStore.fillList(listScreen);
    display.setCurrent(listScreen);
  }

  // Shows an encrypted message in HEX format
  void showEncryptedMessage(int index)
  {
    encryptScreen.setIndex(index);
    encryptScreen.setMessage(codec
        .byteToHex(messageStore.getMessage(index+1)));
    display.setCurrent(encryptScreen);
  }

  // Displays the password screen
  void showPasswordScreen(int index)
  {
    passwordScreen.setIndex(index);
    display.setCurrent(passwordScreen);
  }

  // Shows a decrypted messages
  void showDecryptedMessage(int index, String password)
  {
    // 128bit (16 characters) key is needed for AES
    // just fill with blanks if too short
    while (password.length() <16 )
    {
      password = password.concat(" ");
    }
    decryptScreen.setIndex(index);
    byte message[] = messageStore.getMessage(index+1);
    // Do the decryption and validation in a separate thread
    new Thread(new CryptoRunnable(message,
        password.getBytes(),
        false)).start();
  }

  // Display an error message
  void showError(String messageString)
  {
    infoScreen.showError(messageString, Display.getDisplay(this));
  }

  // Delete a message
  void deleteMessage(int index)
  {
    messageStore.deleteMessage(index+1);
    messageStore.fillList(listScreen);
  }

  // Shows the new message
  void showNewMessage()
  {
    newScreen.createForm();
    display.setCurrent(newScreen);
  }

  // adds a new message encrypting it an storing in the record store
  void addNewMessage(String message, String password)
  {
    // 128bit (16 characters) key is needed for AES
    // just fill with blanks if too short
    while (password.length() <16 )
    {
      password = password.concat(" ");
    }
    // Do the encryption in a separate thread
    new Thread(new CryptoRunnable(message.getBytes(),
        password.getBytes(),
        true)).start();      
  }

  void exitMIDlet()
  {
    if (messageStore != null) {
  	messageStore.close();
    }   
    notifyDestroyed();
  }
  
  // Internal class to handle the decryption and encryption
  // process in a separate thread as recommended in SATSA-CRYPTO
  private class CryptoRunnable implements Runnable {
    private byte[] message, password;
    private boolean encryption;
    
    CryptoRunnable(byte[] message, byte[] password, boolean encryption) {
      this.message = message;
      this.password = password;
      this.encryption = encryption;
    }
    
    public void run() {
      if (encryption) {
        try
        {
          // Encrypt the message
          byte[] encryptedMessage = codec.encrypt(password, message);
          // Calculate the digest of the encrypted message
          byte[] digest = codec.digest(encryptedMessage);
          // Compose the overall message
          byte[] messageBytes = 
            new byte[encryptedMessage.length + digest.length];
          System.arraycopy(digest, 0, messageBytes, 0, digest.length);
          System.arraycopy(encryptedMessage,
              0,
              messageBytes,
              digest.length,
              encryptedMessage.length);
          // Store the encoded message
          messageStore.addMessage(messageBytes);
        }
        catch (GeneralSecurityException gse)
        {
          // Use of generic Exception type since mutiple
          // exceptions may be thrown at this point
          showError("General Security Exception while encrypting: "
              + gse.toString());
        }
        showMessageList();
      } else {
        try
        {
          // Get the cipher text and the digest
          // SHA-1 digest is 160 bits long
          byte[] digest = new byte[20];
          byte[] cipherText = new byte[message.length-digest.length];
          // Decompose the message
          System.arraycopy(message, 0, digest, 0, digest.length);
          System.arraycopy(message,
              digest.length,
              cipherText,
              0,
              cipherText.length);
          
          // Verify the cipher's text digest
          if (codec.isDigestValid(cipherText, digest)) {
            // If digest is ok, let's decrypt
            byte[] plainText = codec.decrypt(password, cipherText);        
             
            // Display the message on screen
            decryptScreen.setMessage(new String(plainText));
            display.setCurrent(decryptScreen);
          } else {
            showError("Digest of message is not valid");
          }            
        }
        catch (BadPaddingException bpe)
        {
          // This is a particular exception when the password is incorrect
          showError("Incorrect password.");
        }
        catch (GeneralSecurityException gse)
        {
          // Handles all other exceptions
          showError("General Security Exception while decrypting: "
              + gse.toString());
        }
      }
    }
  }
}

Codec

package satsa;

import java.security.DigestException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.SecretKeySpec;

// Utility class that encapsulates the calls to the SATSA API
class Codec
{
  // we can have a global instance of the algorithms
  // as long as we synchronize access
  private MessageDigest digest = null;
  private Cipher cipher = null;
  private boolean operative = true;
  
  private SATSAMIDlet midlet;

  // Builds the Codec object and initializes the
  // cipher and digest 
  Codec(SATSAMIDlet midlet)
  {
    this.midlet = midlet;
    try {
      digest = MessageDigest.getInstance("SHA-1");
    } catch (NoSuchAlgorithmException e) {
      // Basically this should not happen since we have to know 
      // if SHA-1 is availabe otherwise the whole design 
      // cannot work
      operative = false;
    }
    try {
      cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    } catch (NoSuchAlgorithmException e) {
      // This should not happen since we know the target platform
      // but we set the operative flag to false just in case
      operative = false;
    } catch (NoSuchPaddingException e) {
      // This should not happen since we know the target platform
      // but we set the operative flag to false just in case
      operative = false;
    }
  }  

  // Encrypt text with given AES key. It encodes the message
  // including the length in two bytes and the plaintext
  synchronized byte[] encrypt (byte[] keyBits, byte[] plaintext)
    throws InvalidKeySpecException, InvalidKeyException,
      IllegalStateException, ShortBufferException,
      IllegalBlockSizeException, BadPaddingException,
      InvalidAlgorithmParameterException
  {
    if (operative) {
      // Initialize the key from  the password
      Key key = new SecretKeySpec(keyBits, 0, keyBits.length, "AES");
      // add 2 bytes to encode the length of the plaintext
      // as a short value
      byte[] plaintextAndLength = new byte[plaintext.length + 2];
      plaintextAndLength[0] = (byte)(0xff & (plaintext.length >> 8));
      plaintextAndLength[1] = (byte)(0xff & plaintext.length);
      // build the new plaintext
      System.arraycopy(plaintext,
          0,
          plaintextAndLength,
          2,
          plaintext.length);   
    
      // calculate the size of the ciperthext considering
      // the padding
      int blocksize = 16;
      int ciphertextLength = 0;
      int remainder = plaintextAndLength.length % blocksize;
      if (remainder == 0) {
        ciphertextLength = plaintextAndLength.length;
      } else {
        ciphertextLength = plaintextAndLength.length - remainder
          + blocksize;
      }
      byte[] cipherText = new byte[ciphertextLength];
    
      // reinitialize the cipher in encryption mode with the given key
      cipher.init(Cipher.ENCRYPT_MODE, key);
      // do the encryption
      cipher.doFinal(plaintextAndLength,
          0,
          plaintextAndLength.length,
          cipherText,
          0);
      
      return cipherText;
    } else {
      throw new IllegalStateException("Codec not initialized");
    }
  }
  
  synchronized byte[] digest(byte message[]) throws DigestException {
    if (operative) {
      // Reset the digest and update with the data
      digest.reset();
      digest.update(message, 0, message.length);
      // SHA-1 produces 160-bit long digests
      byte[] output = new byte[20];
      digest.digest(output, 0, output.length);
      return output;
    } else {
      throw new IllegalStateException("Codec not initialized");
    }

  }
  
  synchronized boolean isDigestValid(byte message[], byte[] digest)
    throws DigestException {
    if (operative) {
      byte[] calculatedDigest = digest(message);
      if (calculatedDigest.length != digest.length) {
        return false;
      }
      // compare byte per byte
      for (int i=0;i<digest.length;i++) {
        if (calculatedDigest[i] != digest[i]) {
          return false;
        }
      }
      return true;
    } else {
      throw new IllegalStateException("Codec not initialized");
    }    
  }

  // Decrypt text with given AES key. It decodes the message
  // reading the message length and then the message itself
  synchronized byte[] decrypt (byte[] keyBits, byte[] cipherText)
    throws InvalidKeySpecException, InvalidKeyException,
      IllegalStateException, ShortBufferException,
      IllegalBlockSizeException, BadPaddingException,
      InvalidAlgorithmParameterException
  {
    if (operative) {
      // create a key from the keyBits
      Key key = new SecretKeySpec(keyBits, 0, keyBits.length, "AES");
    
      // Initialize the cipher in decrypt mode
      cipher.init(Cipher.DECRYPT_MODE, key);
  
      byte[] decrypted = new byte[cipherText.length];
      // Decrypt the cipher text
      cipher.doFinal(cipherText, 0, cipherText.length, decrypted, 0);
      // Calculate the length of the plaintext
      int plainTextLength = (decrypted[0] << 8)  |
        (decrypted[1] & 0xff);
      byte[] finalText = new byte[plainTextLength];
      // Decode the final text
      System.arraycopy(decrypted, 2, finalText, 0, plainTextLength);
  
      return finalText;
    } else {
      throw new IllegalStateException("Codec not initialized");
    }  
  }

  // Displays ecrypted data in hex
  String byteToHex(byte[] data)
  { 
    StringBuffer hexString = new StringBuffer();
    String hexCodes = "0123456789ABCDEF";

    for (int i=0; i < data.length; i++) 
    {
      hexString.append( hexCodes.charAt( (data[i] >> 4) & 0x0f) );
      hexString.append( hexCodes.charAt( data[i] & 0x0f) );
      if (i< data.length - 1)
      {
        hexString.append(":");
      }
      if ( ((i+1)%8) == 0)
        hexString.append("\n");
    }
    return hexString.toString();
  }
}

DecryptScreen

package satsa;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.StringItem;

class DecryptScreen
  extends Form
  implements CommandListener
{
  private final SATSAMIDlet midlet;
  private final Command commandBack =
    new Command("Back", Command.BACK, 1);
  private int index;
  private String decryptedMessage;

  DecryptScreen(SATSAMIDlet midlet)
  {
    super(null);

    this.midlet = midlet;
    createForm();
  }

  public void commandAction(Command c, Displayable d)
  {
    if (c==commandBack)
    {
      midlet.showEncryptedMessage(index);
    }
  }
  
  void setIndex(int index)
  {
    this.index = index;
  }

  void setMessage(String decryptedMessage)
  {
    this.decryptedMessage = decryptedMessage.trim();
    createForm();
  }

  private void createForm()
  {
    deleteAll();
    setTitle("Decrypted Message ");
    StringItem messageItem = new StringItem(null, decryptedMessage);
    append(messageItem);
    addCommand(commandBack);
    setCommandListener(this);
  }
}

EncryptScreen

package satsa;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.StringItem;

// Screen used to display a decrypted message
class EncryptScreen
  extends Form
  implements CommandListener
{

  private final SATSAMIDlet midlet;
  private final Command commandBack =
    new Command("Back", Command.BACK, 1);
  private final Command commandShowDecrypted =
    new Command("Decrypt", Command.ITEM, 1);
  private int index;
  private String encryptedMessage;

  EncryptScreen(SATSAMIDlet midlet)
  {
    super(null);
    this.midlet = midlet;
    createForm();
  }
  
  public void commandAction(Command c, Displayable d)
  {
    if (c==commandBack)
    {
      midlet.showMessageList();
    }
    if (c==commandShowDecrypted)
    {
      midlet.showPasswordScreen(index);
    }
  }

  void setIndex(int index)
  {
    this.index = index;
  }

  void setMessage(String encryptedMessage)
  {
    this.encryptedMessage = encryptedMessage;
    createForm();
  }

  private void createForm()
  {
    deleteAll();
    setTitle("Encrypted Message ");
    StringItem messageItem = new StringItem(null, encryptedMessage);
    append(messageItem);
    addCommand(commandBack);
    addCommand(commandShowDecrypted);
    setCommandListener(this);
  }
}

InfoScreen

package satsa;

import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Display;

class InfoScreen
  extends Alert
{
  InfoScreen()
  {
    super("InfoScreen");
  }

  void showError(String message, Display display)
  {
    setTitle("Error");
    setType(AlertType.ERROR);
    setTimeout(5000);
    setString(message);
    display.setCurrent(this);
  }

}

ListScreen

package satsa;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.List;

// Main UI screen showing the list of available messages
class ListScreen
  extends List
  implements CommandListener
{
  private final SATSAMIDlet midlet;
  private final Command commandExit =
    new Command("Exit", Command.EXIT, 1);
  private final Command commandShow =
    new Command("Show", Command.ITEM, 1);
  private final Command commandNew =
    new Command("Create new", Command.ITEM, 2);
  private final Command commandDelete =
    new Command("Delete this", Command.ITEM, 3);

  ListScreen(SATSAMIDlet midlet)
  {
    super("Messages", List.IMPLICIT);
    this.midlet = midlet;
    createList();
  }
  
  public void commandAction(Command c, Displayable d)
  {
    if (c==commandExit)
    {
      midlet.exitMIDlet();
    }
    if (c==commandShow)
    {
      midlet.showEncryptedMessage(getSelectedIndex());
    }
    if (c==commandDelete)
    {
      midlet.deleteMessage(getSelectedIndex());
    }
    if (c==commandNew)
    {
      midlet.showNewMessage();
    }
  }

  void setNumber(int number)
  {
    deleteAll();
    for(int i=1; i<=number; i++)
    {
      append("Message "+i, null);
    }
  }

  private void createList()
  {
    addCommand(commandExit);
    addCommand(commandDelete);
    addCommand(commandNew);
    addCommand(commandShow);
    setCommandListener(this);
  }
}

MessageStore

package satsa;

import javax.microedition.lcdui.List;
import javax.microedition.rms.RecordEnumeration;
import javax.microedition.rms.RecordStore;
import javax.microedition.rms.RecordStoreException;

// Stores the messages as byte arrays in the RMS
class MessageStore
{
  private final SATSAMIDlet midlet;
  private RecordStore recordStore;
  private RecordEnumeration recordEnumeration;
  private int[] messageIdArray;
  private int numberOfMessages;

  MessageStore(SATSAMIDlet midlet)
  {
    this.midlet = midlet;
    openMessageStore();
  }

  void close()
  {
    try 
    {
      recordStore.closeRecordStore();
    }
    catch (RecordStoreException rse)
    {
      // Ignore, since the application is closing anyway.
    }
  }

  // List all messages in Record Store
  void fillList(List list)
  {
    list.deleteAll();

    try
    {
      numberOfMessages = recordStore.getNumRecords();
      messageIdArray = new int[numberOfMessages+1];

      recordEnumeration = recordStore.enumerateRecords(null,
          null,
          false);
      int i=0;

      while (recordEnumeration.hasNextElement())
      {
        i++;
        int id=recordEnumeration.nextRecordId();

        list.append("Message " + id, null);
        messageIdArray[i]=id;
      }
    }
    catch (RecordStoreException rse)
    {
      midlet.showError("Record Store Exception. " + rse.getMessage());
    }
  }

  // Add a record to Record Store
  void addMessage(byte[] messageData)
  {
    try
    {
      recordStore.addRecord(messageData, 0, messageData.length);
    }
    catch (RecordStoreException rse)
    {
      midlet.showError("Record Store Exception. " + rse.getMessage());
    }
  }

  // Get message from Record Store
  byte[] getMessage(int index)
  {
    try
    {
      byte[] messageData = recordStore.getRecord(messageIdArray[index]);
      return messageData;
    }
    catch (RecordStoreException rse)
    {
      midlet.showError("Record Store Exception. " + rse.getMessage());
      return null;
    }
  }

  // Delete a record from Record Store
  void deleteMessage(int index)
  {
    try
    {
      recordStore.deleteRecord(messageIdArray[index]);
    }
    catch (RecordStoreException rse)
    {
      midlet.showError("Record Store Exception. " + rse.getMessage());
    }
  }
  
  private void openMessageStore()
  {
    try 
    {
      recordStore = RecordStore.openRecordStore("SATSA_test", true);
    }
    catch (RecordStoreException rse)
    {
      midlet.showError("Record Store Exception. " + rse.getMessage());
    }
  }
}

NewScreen

package satsa;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.TextField;

// Displays a form to enter new messages
class NewScreen
  extends Form
  implements CommandListener
{

  private final SATSAMIDlet midlet;
  private final Command commandBack =
    new Command("Back", Command.BACK, 1);
  private final Command commandSave =
    new Command("Save", Command.ITEM, 1);
  TextField messageField;
  TextField passwordField;

  NewScreen(SATSAMIDlet midlet)
  {
    super(null);
    this.midlet = midlet;
  }
  
  public void commandAction(Command c, Displayable d)
  {
    if (c==commandBack)
    {
      midlet.showMessageList();
    }
    if (c==commandSave)
    {
      midlet.addNewMessage(messageField.getString(),
          passwordField.getString());
    }
  }

  void createForm()
  {
    deleteAll();
    setTitle("Add new message ");
    messageField = new TextField("Message",
        "",
        80,
        TextField.ANY);
    // Notice that the max length is 16 as indicated for the AES key
    passwordField = new TextField("Password",
        "",
        16,
        TextField.PASSWORD);
    append(messageField);
    append(passwordField);
    addCommand(commandBack);
    addCommand(commandSave);
    setCommandListener(this);
  }
}

PasswordScreen

package satsa;

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.TextField;

// Shows a password field used to decrypt a message
class PasswordScreen
  extends Form
  implements CommandListener
{
  private final SATSAMIDlet midlet;
  private final Command commandBack =
    new Command("Back", Command.BACK, 1);
  private final Command commandDecrypt =
    new Command("Decrypt", Command.ITEM, 1);
  private int index;
  TextField passwordField;

  PasswordScreen(SATSAMIDlet midlet)
  {
    super(null);
    this.midlet = midlet;
    createForm();
  }
  
  public void commandAction(Command c, Displayable d)
  {
    if (c==commandBack)
    {
      midlet.showEncryptedMessage(index);
    }
    if (c==commandDecrypt)
    {
      midlet.showDecryptedMessage(index,
          passwordField.getString());
    }
  }

  void setIndex(int index)
  {
    this.index = index;
    createForm();
  }

  private void createForm()
  {
    deleteAll();
    setTitle("Enter password to decrypt.");
    passwordField = new TextField("Password",
        "",
        16,
        TextField.PASSWORD);
    append(passwordField);
    addCommand(commandBack);
    addCommand(commandDecrypt);
    setCommandListener(this);
  }

}