2010-02-23

Delaying security dialogs in Java until you need them

Delaying security dialogs in Java until you need them, is a bit harder than it should be. By default, the dialog appears before the first line of code is executed, scaring off your casual visitor.

If you only occasionally need to perform actions that require elevated privileges, you can delay the security dialog to the absolute last moment (for example: right before reading/writing a file). The trick is to keep most of your code in an unsigned JAR, and the code that requires elevated privileges into a signed JAR. Use Class.forName(String) to load the signed class, which will prompt the security dialog.

Using an interface in the unsigned code, and the implementation in the signed code, you can keep your code tidy.


Note:

The browser will remember the choice of the user, until a *restart* of the browser. To workaround this (when people declined), create a dozen tiny signed JARs (with a dozen different certificates, mind you) and use a roundrobin algorithm, using serverside code or javascript that generates the applet-archive attribute. After a dozen hits and rejections, you can be sure your visitor will never grant access to his system anyway.

Update:

To make it work in MSIE, both classes MUST be in separate packages.


IMPORTANT: Since 6u19 this doesn't work anymore. You not only get a rather confusing security dialog (clicking [YES] means deny access, clicking [NO] means allow access), the two classes end up in different classloaders that cannot access eachother, resulting in ClassNotFoundException / NoClassDefFoundException. Thanks Oracle, for making Java's user experience even more secure and crap at the same time.
http://java.sun.com/javase/6/docs/technotes/guides/jweb/mixed_code.html



On to the code, which is reasonably simple.

Unsigned JAR:
package some.unsigned.stuff;

public interface SecureAccess
{
   public byte[] loadFile(File file);
   public void storeFile(File file, byte[] data);
}

     // Usage:

     File file = new File("/home/silly/image.jpg");
     Class< ? > clazz = Class.forName("some.signed.stuff.LocalSecureAccess");
     SecureAccess access = (SecureAccess) clazz.newInstance();
     byte[] data = access.loadFile(file);

Signed JAR:
package some.signed.stuff;

public class LocalSecureAccess implements SecureAccess
{
   public byte[] loadFile(final File file)
   {
      return AccessController.doPrivileged(new PrivilegedAction<byte[]>()
      {
         @Override
         public byte[] run()
         {
            return loadFileImpl(file);
         }
      });
   }

   @Override
   public void storeFile(final File file, final byte[] data)
   {
      AccessController.doPrivileged(new PrivilegedAction<Object>()
      {
         @Override
         public Object run()
         {
            storeFileImpl(file, data);

            return null;
         }
      });
   }

   // implementation

   static final int MAX_FILE_SIZE = 8 * 1024 * 1024; // prevent applet running out of memory

   byte[] loadFileImpl(File file)
   {
      DataInputStream input = null;

      try
      {
         long len = file.length();
         if (len > MAX_FILE_SIZE)
            throw new IllegalStateException("file too big: " + file);

         byte[] data = new byte[(int) len];
         input = new DataInputStream(new FileInputStream(file));
         input.readFully(data);
         input.close();
         return data;
      }
      catch (IOException exc)
      {
         throw new IllegalStateException(exc);
      }
      finally
      {
         try { if(input!=null) input.close(); } catch(IOException exc) {}
      }
   }

   void storeFileImpl(File file, byte[] data)
   {
      OutputStream output = null;

      try
      {
         output = new FileOutputStream(file);
         output.write(data);
         output.flush();
      }
      catch (IOException exc)
      {
         throw new IllegalStateException(exc);
      }
      finally
      {
         try { if(output!=null) output.close(); } catch(IOException exc) {}
      }
   }
}

Jar signing 101: (using DSA keys instead of RSA for Java 1.4 compatibility)
PATH=%PATH%;path\to\JDK\bin
SET ALIAS=MY_ALIAS
SET PASS=MY_PASSWORD
SET JAR=my.jar

keytool -delete -storepass %PASS% -alias %ALIAS%
keytool -genkey -storepass %PASS% -keypass %PASS% -keyalg DSA -alias %ALIAS%
   -dname "CN=full.domainname.com, OU=Your unit, O=Your Company,
           L=Your city, ST=Your state, C=CA,
           EMAILADDRESS=your@server.com DC=server, DC=com"
   -validity 999 (put all of this on one line)
keytool -selfcert -storepass %PASS% -alias %ALIAS% -validity 999
keytool -exportcert -storepass %PASS% -alias %ALIAS% -rfc -file %ALIAS%.cer
jarsigner -storepass %PASS% -keypass %PASS% %JAR% %ALIAS%
pause

Applet code: (nothing special)
    <applet
      code="package/of/YourApplet.class"
      archive="unsigned.jar,signed.jar"
      width="640"
      height="480">
      no applet?
    </applet>  

1 comment: