Lamport's one-time password algorithm (or, don't talk to complete strangers!)

A design pattern for securing client/service interactions with OTP

1 2 3 4 5 6 Page 6
Page 6 of 6

The initial implementation of the PassKeySequencerFactory has a notion of what the default PassKeySequencer implementation (that is, the shared sequence algorithm) is, but it can also be directed to create a specific implementation based on a fully qualified class name.

Leveraging a common factory pattern design leaves all other components oblivious to the details of determining the why, what, and how of the sequence-algorithm selection. Listing 7 shows a PassKeySequencerFactory class that provides a PassKeySequencer based on the following precedence order:

  1. The implementation requested by the client
  2. The implementation set in the environment (property bundle)
  3. The default implementation in the code (HashSequencer)

Listing 7. Constructing a PassKeySequencer implementor

package com.opensolutionspace.otp.lib;

import java.lang.*;
import java.util.*;
import com.opensolutionspace.common.*;

/**
 * Provides ease of constructing an implementor of PassKeySequencer ...
 * Clients of PassKeySequencer's need not concern themselves with
 *  selecting a specific sequence algorithm (i.e., implementation),
 *  the details of creation/initialization and the finite set of implementors.
 *
 * @author lji
 *  02/02/2009
 */
public class PassKeySequencerFactory {

    /**
     * Create and return a Sequence instance based on the "classpath" value ...
     */
    static public PassKeySequencer createSequence(String classPath) {

        // Use Hash if what user asks for is NA
        PassKeySequencer passKeySequence = new HashSequencer();

        //
        try{
            Class theClass = Class.forName( classPath );
            passKeySequence = (PassKeySequencer)theClass.newInstance();
            //SimpleLogger.info( "PassKeySequenceFactory: successfully created: " + passKeySequence.getClass().getName() );
        }
        catch( InstantiationException e ){
                SimpleLogger.error( "PassKeySequenceFactory:" + e );
        }
        catch( IllegalAccessException e ){
                SimpleLogger.error( "PassKeySequenceFactory:" + e );
        }
        catch( ClassCastException e ){
                SimpleLogger.error( "PassKeySequenceFactory:" + e );
        }
        catch( ClassNotFoundException e ){
                SimpleLogger.error( "PassKeySequenceFactory:" + e );
        }


        if( passKeySequence == null ) {
            passKeySequence = new HashSequencer();
            SimpleLogger.error(  "PassKeySequenceFactory: Seq Class Was Not Instantiated - Still null" );
        }


        return passKeySequence;
    }

    /**
     * Returns the "default/current" Sequence Type
     */
    static public PassKeySequencer createSequence() {

        String seqType;
        ObjectAttributes attributes;
        attributes = ObjectAttributes.createObjectAttributes("PassKeySequencerFactory");
        attributes.loadAttributes();
        seqType = attributes.getString("sequenceType");
        //System.out.println("client attributes: " + attributes.toString());
        if(seqType == null) {
            seqType = PassKeySequencer.DEFAULT_SEQUENCE_TYPE;
        }

        return createSequence(seqType);
    }


}

The implementation of SimpleOtpAuthority and its base class -- BaseOtpAuthority -- is also worth reviewing. This is where all client requests are validated, providing OtpService instances with a single point for all validation, including passkey validation. Instances of PassKeySequencer are not directly used by OtpService implementations. Listing 8 shows the implementation of the two classes.

Listing 8. SimpleOtpAuthority and BaseOtpAuthority

package com.opensolutionspace.otp.poc;

import java.util.*;
import com.opensolutionspace.otp.lib.*;



/**
 *
 * 02/01/2009
 * Used by the SecureService class
 * It's only validation check on the clientId: it cannot be empty
 * @author lji
 */
public class SimpleOtpAuthority extends BaseOtpAuthority implements OtpAuthority {

    public SimpleOtpAuthority() { }

    /**
    * Defined as abstract in BaseOtpAuthority
    */
    protected boolean validRequester(String id) {
      if(id == null || id.length() == 0)
          return false;
        return true;
    }

}


///////////////////////////////////////////////////////////////////////////////////////////////////

package com.opensolutionspace.otp.lib;

import java.util.Hashtable;
import java.util.Map;

import com.opensolutionspace.common.SimpleLogger;

/**
 * 02/03/2009
 *
 * Convenience base OTP Service Class - carries data structures and
 * default key management and authentication methods.
 * Concrete classes that implement the OtpService would be well served
 * by extending this and modifying/extending behavior as needed.
 * @author lji
 *
 */
public abstract class BaseOtpAuthority {

    private Map<String,String> requesterMap;
    private PassKeySequencer passKeySequence;


    public BaseOtpAuthority() {
      init();
    }


    protected void init() {
      requesterMap = new Hashtable<String,String>(83);
      passKeySequence = PassKeySequencerFactory.createSequence();
      return;
    }


    // required to be implemented by derived classes
    protected abstract boolean validRequester(String id) ;


    protected void update(String id, String password) {
      requesterMap.put(id, password);
    }


    protected void purgeId(String id) {
      requesterMap.remove(id);
    }


    protected boolean knownClient(String id) {
      if( requesterMap.containsKey(id) == false) {
        return false;
      }
      return true;
    }

    protected boolean authenticate(String id, String password) {
      /**
      * Reject empty s!
      */
      if( password == null || password.length() == 0 )
        return false;

      // perform operation on to obtain what should be stored
      // on behalf of this client ...
      String real = passKeySequence.encode(password);
      // stored value ...
      String found = requesterMap.get(id);

      if(real.equals(found) == true ) {
        update(id, password) ;
        return true;
      }

      return false;
    }


    ////////////////////////////////////////////////////
    ///// Public methods
    /**
    * 1st interaction between client and server "conversation"
    */
    public void hello(String id, String info ) throws SecureServiceException {

      if(this.validRequester(id) == true)
        update(id, info);
      else
        throw new SecureServiceException(
            SecureServiceException.Code.AUTHORIZATION_EXCEPTION);

    }


    /**
    * 2 opportunities for failure - 'unknown/untrusted client', or bad password ...
    */
    public int authorizeRequest(
       String id, String info, String request, String... args ) {

       // Are we satisfied with the way the client is identifying himself?
       if( knownClient(id) == false) {
         SimpleLogger.error("Unknown Client - NOT Allowing " + request + " on behalf of " + id + "\n");
         return OtpService.UNKNOWN_CLIENT;
       }

      // Is the passkey valid?
      if( authenticate(id, info ) == false) {
         SimpleLogger.error(" PassKey Rejected - NOT Allowing " + request + " on behalf of " + id + "\n");
         return OtpService.INVALID_PASSWORD;
      }

      // Ok, go ahead and allow service
      SimpleLogger.info("Allowing " + request + " on behalf of " + id );
      return OtpService.OK;
    }


    public void goodbye(String id, String info ) throws SecureServiceException {

      // validate the request to end this client conversation
      // Is the password valid
      if( authenticate(id, info ) == false) {
        SimpleLogger.error("PassKey Rejected - NOT terminating conversation on behalf of " + id + "\n");

        throw new SecureServiceException(
        SecureServiceException.Code.AUTHORIZATION_EXCEPTION);

      }

      // Don't even bother checking if 'id' already exists - just purge
      purgeId(id);
      SimpleLogger.info("Terminating conversation on behalf of " + id + "\n");

    }


}

A note about packaging

The interfaces and classes in the reference implementation are partitioned across five packages. Here's a brief synopsis of what each package contains, with the packages listed in roughly least-to-most dependent order:

  • *.otp.test contains The test driver class: TestDriver
  • *.appl contains the original client/service classes: SecureSampleClient and SecureSampleService
  • *.common contains several generally useful "utility classes" that are not specific to any particular application: ObjectAttributes, SimpleLogger, ServiceException,, and Utility.
  • *.otp.lib contains core interfaces and base-class implementations specific to OTP generation, maintenance, and validation: BaseOtpAuthority, PassKeySequencer, BaseSequencer, PassKeySequencerFactory, HashSequencer, SecureServiceException, OtpAuthority, and OtpService
  • *.otp.poc contains proof-of-concept implementations or class stubs that extend base classes in *.otp.lib: FiboSequencer, SimpleOtpAuthority, StringSequencer, FicaOtpAuthority, and SimpleSequencer. Note: If a developer wanted to utilize this reference implementation, the contents of common, otp.lib, and otp.poc would be provided in one or more JARs.
  • *.otp.appl contains the client/service classes that rely on OTP -- extensions of the*.appl components SecureSampleClient and SecureSampleService.

Figure 5 shows the package interdependencies.

Figure 5. Package interdependencies (Click to enlarge.)

In conclusion

Leveraging tried and tested algorithms often proves to be the main ingredient in a software solution recipe. Incorporating single-usage, or one-time, passwords into requests passed between cooperating entities is not a novel idea, but it is a practical one. Lamport's approach focuses on synchronizing client/service entities through a common sequencing algorithm used both to generate and to validate passkey values sent along with every interaction. It might not be the only security precaution you take, but it's worth considering between distributed applications if you would like to:

  • Tighten up casual, clear-text, unauthenticated interactions.
  • Simplify needlessly complicated interactions that use certificate-authority and encryption overhead, without completely disregarding security concerns.
  • Authenticate client requests without necessarily implementing user-registration and password-assignment use cases. That is, it lets you allow anonymous interactions but ensure that a service conversation is being initiated by a trusted or recognized client.

Next steps

As published, the Lamport algorithm focuses on sequence-value generation and management, within an OTP context. The solution details I've offered in this article (relating to client/service interactions) are beyond its scope. The reference implementation shows how you can apply an OTP framework to an existing client/service collaboration with minimal coding, and it can be extended to generate alternate sequence algorithms and stricter client-identifier validation. However, this is a proof-of-concept-level code -- a baseline. Here are some suggestions for taking this implementation to the next level:

  • Use a more realistic scenario that OTP would generally be applied to, such as a client/service pair that's distributed across machine or JVM boundaries. A service deployed as an HTTP servlet, or an remote method invocation (RMI) service would be a much more practical example, but wouldn't be much more of a challenge than extending our original SampleClient and SampleService implementations.
  • Demonstrate additional, less trivial PassKeySequencer implementations.
  • Demonstrate additional, less trivial OtpAuthority implementations.
  • Provide more-detailed error checking and exception handling around the OTP authentication and core service usage.
  • Since the client is not obligated to say "goodbye" (not really enforceable in this model) the OtpAuthority implementations should probably "age out" the client-identifier records after a period of idleness and/or fixed period.
  • Demonstrate that core library classes can be used in a thread-safe manner.

Acknowledgments

Leslie Lamport is credited with first suggesting that using a one-way function to generate a sequence of values could be useful for securing distributed systems through one-time (expiring) passwords. Most of Lamport's career has revolved around solution algorithms for distributed computing.

Special acknowledgments are owed to two trusted peers and friends: Jeff Kroll of HP Software and Tim Ihde of DoubleCheck, Inc. -- Jeff for noticing the practical usefulness of Lamport's algorithm and sharing his quick study with me, and Tim for long ago introducing me to the multifaceted world of application security.

Louis J. Iacona has been designing and developing software since 1982, mainly on UNIX/Linux platforms. Most recently, his efforts have focused on Java/J2EE-implemented solutions for enterprise-scoped applications and leveraging virtualization platforms. Louis is currently on assignment at HP Software in Paramus, New Jersey, and can be reached at louis.iacona@verizon.net.

Learn more about this topic

More from JavaWorld

1 2 3 4 5 6 Page 6
Page 6 of 6