|
Tech Tips archive
December 17, 2001
WELCOME to the Java Developer Connection (JDC)
Java 2 Platform, Micro Edition
(J2ME) Tech Tips, for April 16, 2001.
This issue covers:
The J2ME Tech Tips are written by Eric Giguere
(http://www.ericgiguere.com), an engineer at iAnywhere Solutions, inc. Eric is the author of the book "Java 2 Micro Edition: Professional Developer's Guide" and co-author of the book "Mobile Information Device Profile for Java 2 Micro Edition," both books in John Wiley & Sons' Professional Developer's Guide series.
DATA ENCRYPTION FOR J2ME PROFILES
If your J2ME applications store or transmit sensitive or confidential data, you can make them more secure using data encryption. Devices that support the Mobile Information Device Profile (MIDP), for example, are only required (in version 1.0) to support HTTP, not the secure HTTP protocol, HTTPS. The security of your data therefore depends on the security of the network. Even applications that don't transmit confidential data can benefit from data encryption. When run on a Palm OS-based device, for example, the record stores used by a MIDP application are actually Palm OS record databases. The MIDP runtime system controls access to the underlying record databases so that only applications running the same MIDlet suite can see each other's data. However, it's a trivial matter for an experienced Palm OS user to view and copy the contents of those databases independently from the MIDP runtime system. Encrypting the contents of a record store can defeat all but the most determined attackers.
Unfortunately, encryption is not a standard part of either the Connected Device Configuration (CDC) or the Connected Limited Device Configuration (CLDC). You must either write your own encryption/decryption code or else use a class library. Writing your own code is not a recommended approach because of the complexity involved in selecting and implementing the appropriate algorithms. Instead, it's better to take advantage of existing code. That's where the Legion of the Bouncy Castle comes in.
The Legion of the Bouncy Castle is an open source Java encryption project hosted at http://www.bouncycastle.org. Although primarily geared towards providing alternative encryption algorithms for J2SE, the Legion has adapted some of its code to work with J2ME. Specifically, parts of the Bouncy Castle lightweight cryptography API work with both the CLDC and the CDC. The lightweight API supports all the common block and stream encryptions, such as DES, Blowfish, IDEA, Rijndael, and RC4, as well as digest generation and key exchange, although not all these features are available for all J2ME platforms.
To use the cryptography API, go to the Bouncy Castle web site and download the latest release of the lightweight API for J2ME. Extract the archive into a suitable directory. Although a midp_classes.zip file is provided with classes compiled and preverified, you're better off copying the source files you need directly into your project. That's because some of the Bouncy Castle classes refer to classes that are not found in the CLDC or the MIDP. Second, you get a better idea of how much of a space penalty you pay for adding encryption support to your application.
Here is an example of a simple encryption class built using the Bouncy Castle lightweight API:
import org.bouncycastle.crypto.*;
import org.bouncycastle.crypto.engines.*;
import org.bouncycastle.crypto.modes.*;
import org.bouncycastle.crypto.params.*;
// A simple example that uses the Bouncy Castle
// lightweight cryptography API to perform DES
// encryption of arbitrary data.
public class Encryptor {
private BufferedBlockCipher cipher;
private KeyParameter key;
// Initialize the cryptographic engine.
// The key array should be at least 8 bytes long.
public Encryptor( byte[] key ){
cipher = new PaddedBlockCipher(
new CBCBlockCipher(
new DESEngine() ) );
this.key = new KeyParameter( key );
}
// Initialize the cryptographic engine.
// The string should be at least 8 chars long.
public Encryptor( String key ){
this( key.getBytes() );
}
// Private routine that does the gritty work.
private byte[] callCipher( byte[] data )
throws CryptoException {
int size =
cipher.getOutputSize( data.length );
byte[] result = new byte[ size ];
int olen = cipher.processBytes( data, 0,
data.length, result, 0 );
olen += cipher.doFinal( result, olen );
if( olen < size ){
byte[] tmp = new byte[ olen ];
System.arraycopy(
result, 0, tmp, 0, olen );
result = tmp;
}
return result;
}
// Encrypt arbitrary byte array, returning the
// encrypted data in a different byte array.
public synchronized byte[] encrypt( byte[] data )
throws CryptoException {
if( data == null || data.length == 0 ){
return new byte[0];
}
cipher.init( true, key );
return callCipher( data );
}
// Encrypts a string.
public byte[] encryptString( String data )
throws CryptoException {
if( data == null || data.length() == 0 ){
return new byte[0];
}
return encrypt( data.getBytes() );
}
// Decrypts arbitrary data.
public synchronized byte[] decrypt( byte[] data )
throws CryptoException {
if( data == null || data.length == 0 ){
return new byte[0];
}
cipher.init( false, key );
return callCipher( data );
}
// Decrypts a string that was previously encoded
// using encryptString.
public String decryptString( byte[] data )
throws CryptoException {
if( data == null || data.length == 0 ){
return "";
}
return new String( decrypt( data ) );
}
}
|
The constructor defines the encryption algorithm to use by instantiating the appropriate encryption engine, in this case DESEngine, which represents the 56-bit DES algorithm:
public Encryptor( byte[] key ){
cipher = new PaddedBlockCipher(
new CBCBlockCipher(
new DESEngine() ) );
DES works with 8-byte blocks. In order to be able to encrypt or decrypt data of arbitrary length, the constructor wraps the engine using PaddedBlockCipher and CDCBlockCipher. This produces a "cipher" object that does the encryption or decryption. The cipher object must be initialized before it is used so that it knows whether it should encrypt or decrypt, and what key to use.
The callCipher routine is where the encryption or decryption actually occurs.
private byte[] callCipher( byte[] data )
throws CryptoException {
int size =
cipher.getOutputSize( data.length ); ...
To encrypt or decrypt data in your application, create an instance of this class, passing in the secret key. The secret key must be at least eight bytes or characters long:
Encryptor encryptor =
new Encryptor( "ghk23rTX" );
Note that because the encryption algorithm used in the example Encryptor class is the DES 56-bit algorithm, only the first eight characters or bytes of the key are used. If you change the class to use one of the other engines, different limits apply.
Call the encrypt and decrypt methods to encrypt or decrypt arbitrary binary data. The convenience methods encryptString and decryptString make it easy to encrypt or decrypt strings by automatically converting them to and from byte arrays.
Here is a simple MIDP application that uses the Encryptor class to encrypt and decrypt a string stored in a record store. The application prompts the user for an eight-character key. It then encrypts a user-defined message. The user can then enter different keys and see the result: either gibberish or an exception. Only the correct key will decrypt and display the message.
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.rms.*;
import org.bouncycastle.crypto.*;
// Simple test of encryption/decryption routines.
public class CryptoTest extends MIDlet {
private Display display;
private Command exitCommand =
new Command( "Exit",
Command.EXIT, 1 );
private Command okCommand =
new Command( "OK",
Command.OK, 1 );
private Encryptor encryptor;
private RecordStore rs;
public CryptoTest(){
}
protected void destroyApp( boolean unconditional )
throws MIDletStateChangeException {
exitMIDlet();
}
protected void pauseApp(){
}
protected void startApp()
throws MIDletStateChangeException {
if( display == null ){ // first time called...
initMIDlet();
}
}
private void initMIDlet(){
display = Display.getDisplay( this );
// Open a record store here
try {
rs = RecordStore.openRecordStore( "test",
true );
}
catch( RecordStoreException e ){
// put in error handling here
}
display.setCurrent( new AskForKey() );
}
public void exitMIDlet(){
try {
if( rs != null ){
rs.closeRecordStore();
}
}
catch( RecordStoreException e ){
}
notifyDestroyed();
}
private void displayException( Exception e ){
Alert a = new Alert( "Exception" );
a.setString( e.toString() );
a.setTimeout( Alert.FOREVER );
display.setCurrent( a, new AskForKey() );
}
class AskForKey extends TextBox
implements CommandListener {
public AskForKey(){
super( "Enter a secret key:", "", 8, 0 );
setCommandListener( this );
addCommand( okCommand );
addCommand( exitCommand );
}
public void commandAction( Command c,
Displayable d ){
if( c == exitCommand ){
exitMIDlet();
}
String key = getString();
if( key.length() < 8 ){
Alert a = new Alert( "Key too short" );
a.setString( "The key must be " +
"8 characters long" );
setString( "" );
display.setCurrent( a, this );
return;
}
encryptor = new Encryptor( key );
try {
if( rs.getNextRecordID() == 1 ){
display.setCurrent(
new EnterMessage() );
} else {
byte[] data = rs.getRecord( 1 );
String str =
encryptor.decryptString( data );
Alert a =
new Alert( "Decryption" );
a.setTimeout( Alert.FOREVER );
a.setString(
"The decrypted string is '" +
str + "'" );
display.setCurrent( a, this );
}
}
catch( RecordStoreException e ){
displayException( e );
}
catch( CryptoException e ){
displayException( e );
}
}
}
class EnterMessage extends TextBox
implements CommandListener {
public EnterMessage(){
super( "Enter a message to encrypt:", "",
100, 0 );
setCommandListener( this );
addCommand( okCommand );
}
public void commandAction( Command c,
Displayable d ){
String msg = getString();
try {
byte[] data =
encryptor.encryptString( msg );
rs.addRecord( data, 0, data.length );
}
catch( RecordStoreException e ){
displayException( e );
}
catch( CryptoException e ){
displayException( e );
}
display.setCurrent( new AskForKey() );
}
}
}
|
 |
VALIDATING INPUT USING THE ITEMSTATELISTENER INTERFACE
The Mobile Information Device Profile's high-level user interface API defines several components for placement on Form objects. These components are referred to as "items" because they all extend the javax.microedition.lcdui.Item class. Some of the items are editable by the user, the include the ChoiceGroup, Gauge, DateField and TextField items.
Sometimes the application needs to know when an editable item's value or selection changes. For example, the application might want to update the text in a label based on the current value of a gauge. Or an application might want to do some preliminary validation of input from a text field. This is where the ItemStateListener interface is important. The interface is very simple, consisting of single method:
package javax.microedition.lcdui;
public interface ItemStateListener {
void itemStateChanged( Item item );
}
|
If you want to be notified of item state changes, simply register (with the form) an object that implements the ItemStateListener interface:
Form f = new Form();
ItemStateListener listener = ....; //
form.setItemStateListener( listener );
Whenever an editable item on the form changes state because of user interaction, it invokes the listener's itemStateChanged method. The item whose state changed is passed as the only argument. Note that the notification occurs after the item's state has changed -- if you need to know the old state you must keep track of it separately.
Let's use the ItemStateListener to perform some validation of numeric input. As discussed in the November 14, 2001 Tech Tip, "Developing Custom Components for the Mobile Information Device Profile", the type of input accepted by a TextField component can be constrained in simple ways. A text field can be limited to accepting numeric input by setting the TextField.NUMERIC constraint. Unfortunately, there is no validation beyond these simple constraints. For example, suppose your application wants the user to enter a phone number. Although the TextField class supports a PHONENUMBER constraint, it might be too limited or somehow inappropriate for your purposes. Using ItemStateListener, you can build a phone number entry form that allows only specific kinds of phone numbers. In fact, in this tip you'll see a more general form, called NumericInputForm, that can be used for arbitrary numeric input, not just phone numbers.
A NumericInputForm has two items on it: a labeled TextField and a StringItem. The TextField is where the user enters the phone number. It has a NUMERIC constraint -- in other words, the user enters an unformatted number. (This works quite well on MIDP devices because it means users can use the keypad to directly enter numbers without having to cycle through letters and symbols, as with general text input.)
The StringItem displays the formatted form of the number if it matches one of the input masks. The form has two Command objects associated with it: a "Done" command and a "Cancel" command. The "Cancel" command is always active, but the "Done" command is only enabled when the user's input matches one of the input masks. This means that the user can proceed to the next form only by canceling the current form or by entering a value that matches one of the masks. Here's the code for NumericInputForm:
import javax.microedition.lcdui.*;
// Defines a form that prompts the user to
// enter a numeric value and checks it against
// a set of input masks. If the number matches
// a mask, the "Done" command is made visible
// and the user can then proceed to the next
// form.
public class NumericInputForm extends Form
implements ItemStateListener {
public static final Command DONE_COMMAND =
new Command( "Done", Command.OK, 1 );
public static final Command CANCEL_COMMAND =
new Command(
"Cancel", Command.CANCEL, 1 );
private String[] masks;
private int[] digits;
private Command done;
private Command cancel;
private TextField field;
private StringItem match;
public NumericInputForm( String title,
String label,
String[] masks,
String value ){
this( title, label, masks, value,
null, null );
}
public NumericInputForm( String title,
String label,
String[] masks,
String value,
Command done,
Command cancel ){
super( title );
this.masks = masks;
this.done = ( done != null ? done
: DONE_COMMAND );
this.cancel = ( cancel != null ? cancel
: CANCEL_COMMAND );
digits = new int[ masks.length ];
int maxlen = 0;
for( int i = 0; i < masks.length; ++i ){
int mlen = countDigitsInMask( masks[i] );
if( mlen > maxlen ) maxlen = mlen;
digits[i] = mlen;
}
field = new TextField( label, value,
maxlen,
TextField.PHONENUMBER );
append( field );
match = new StringItem( null, "" );
append( match );
adjustState();
addCommand( this.cancel );
setItemStateListener( this );
}
// Adjust the state of the form based on changes
// made to the input field. Adds or removes the
// "done" command as appropriate, and also adjusts
// the value of the label showing the masked
// input that matches, if any.
protected void adjustState(){
String val = field.getString();
String applied = null;
for( int i = 0; i < masks.length; ++i ){
applied = matches(
val, digits[i], masks[i] );
if( applied != null ){
break;
}
}
if( applied != null ){
match.setText( applied );
addCommand( done );
} else {
match.setText( "" );
removeCommand( done );
}
}
// Figure out how many digits need to be entered
// for a particular mask.
private int countDigitsInMask( String mask ){
int count = 0;
for( int i = 0; i < mask.length(); ++i ){
char ch = mask.charAt( i );
if( ch == '#' || Character.isDigit( ch ) ){
++count;
}
}
return count;
}
// Return the masked value.
public String getMaskedValue(){
return match.getText();
}
// Return the raw value (numbers only).
public String getRawValue(){
return field.getString();
}
// Our callback, just calls adjustState.
public void itemStateChanged( Item item ){
adjustState();
}
// Check to see if the given string matches
// the given mask.
protected String matches( String value,
int digits,
String mask ){
if( value.equals( mask ) ) return value;
int vlen = value.length();
if( vlen != digits ) return null;
int mlen = mask.length();
int vindex = 0;
StringBuffer b = new StringBuffer( mlen );
for( int i = 0; i < mlen; ++i ){
char mch = mask.charAt( i );
char vch = mch;
if( mch == '#' ){
vch = value.charAt( vindex++ );
} else if( Character.isDigit( mch ) ){
vch = value.charAt( vindex++ );
if( mch != vch ) return null;
}
b.append( vch );
}
return b.toString();
}
// Adjust the field's value.
public void setRawValue( String value ){
field.setString( value );
adjustState();
}
}
|
Notice that the constructor takes an array of strings. These strings are the masks that are accepted by the form. Use the '#' character to indicate a placeholder for a number. For example, to accept North American style phone numbers, declare the masks like this:
String[] masks = new String[]{ "###-####",
"(###) ###-####" };
Here's a simple MIDlet that uses the NumericInputForm class to
prompt the user for a phone number.
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.rms.*;
// A simple test of the NumericInputForm class
// that prompts the user to enter a phone number
// in one of two acceptable formats.
public class NumericInputTest extends MIDlet {
private Display display;
private Command exitCommand =
new Command( "Exit",
Command.EXIT, 1 );
private Command okCommand =
new Command( "OK",
Command.OK, 1 );
public NumericInputTest(){
}
protected void destroyApp( boolean unconditional )
throws MIDletStateChangeException {
exitMIDlet();
}
protected void pauseApp(){
}
protected void startApp()
throws MIDletStateChangeException {
if( display == null ){
initMIDlet();
}
}
private void initMIDlet(){
display = Display.getDisplay( this );
display.setCurrent( new PromptForPhone() );
}
public void exitMIDlet(){
notifyDestroyed();
}
private String[] phoneMasks =
new String[]{ "###-####",
"(###) ###-####" };
// A subclass of NumericInputForm that does nothing
// but set the masks and the initial values and
// registers itself as a command listener.
class PromptForPhone extends NumericInputForm
implements CommandListener {
public PromptForPhone(){
super( "Test", "Enter a phone number:",
phoneMasks, "" );
setCommandListener( this );
}
// Called when the user presses either the
// "done" or "cancel" commands on the
// numeric input form.
public void commandAction( Command c,
Displayable d ){
if( c == CANCEL_COMMAND ){
exitMIDlet();
} else {
Alert a = new Alert( "Result" );
a.setString( "You entered the number " +
getMaskedValue() +
" from the string " +
getRawValue() );
a.setTimeout( Alert.FOREVER );
display.setCurrent( a, this );
setRawValue( "" );
}
}
}
}
|
Several improvements can be made to the NumericInputForm class. It could allow for (and display) partial matches, for example, or it could dynamically adjust the constraints of the underlying TextField to allow the user to enter more than just numeric characters.
IMPORTANT: Please read our Terms of Use and Privacy policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
- FEEDBACK
Comments? Send your feedback on the J2ME Tech Tips to:
jdc-webmaster@sun.com
- SUBSCRIBE/UNSUBSCRIBE
- To subscribe, go to the subscriptions page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".
- To use our one-click unsubscribe facility, see the link at the end of this email:
- ARCHIVES
You'll find the J2ME Tech Tips archives at: http://java.sun.com/jdc/J2METechTips/index.html
- COPYRIGHT
Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html
J2ME Tech Tips December 17, 2001
Sun, Sun Microsystems, Java, Java Developer Connection, J2ME, and J2SE are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.
|