In this blog post I will be explaining how to create a RESTful service that returns a .pkpass file in Java with Google App-Engine and Eclipse. I will also point out some traps you may fall into when using Google's App-Engine. For more information on what a .pkpass file is and Passbook please refer to my collegue Jonathan Tang's blog posts Part 1 , Part 2

I'm going to break the blog post into the four parts to creating the .pkpass archive:

1. Create pass.json

2. Create manifest.json

3. Sign the manifest and create the signature file

4. Zip up all the files and static .png files to create the .pkpass archive.

Tools you will need

jPassKit - great open source API for creating a pkpass file in Java

Jersey - create RESTful web services with annotations on Java classes

Bouncy Castle - for signing the .pkpass file or your preferred security Java API

Potential problems you may encounter:

a) Google's App-Engine is a distributed server so the file system the app-engine runs on is read-only so that it can avoid the overhead of propigating created files across multiple instances of a single server. Therefore the when using app-engine the pkpass file has to be created in memory and sent across the wire immediately.

b) Google's App-Engine does not allow the use of signed JAR's (as of this post) which makes it a little tricky to use your own security classes, such as BouncyCastle JAR's. You'll need some kind of security classes to create the 'signature' file for your pkpass file. I got around this by removing any signing information from the JAR's I needed and JARing everything back up again. I knew I was getting the JAR's from the right place because they were signed correctly when I downloaded them.

Intro to jPassKit API

Most seasoned developers should be familiar with the tools I listed above minus the jPassKit API so I'll list out here some interesting classes and leave the rest of the API exploring to you:

a) PKPass.java - contains all the information needed to create the pass.json file and implements an interface that allows you to place some validation on the pass.json file before it is created!

b) PKSigningUtil.java - contains some static methods the most important being the createSignedAndZippedPkPassArchive method. If you look at this method signature you will realize you need to call theloadSigningInformationFromPKCS12FileAndIntermediateCertificateFile method first so you can have a PKSigningInformation object created to pass in.

Getting Started

The four steps I mentioned above boil down into Java code like this:

public static byte[] createSignedAndZippedPkPassArchive(MyLowesPass pass, PKSigningInformation signingInformation) throws Exception {
 
 
 
		byte[] passOut = createPassJSONFile(pass); // step 1
 
		byte[] manifestOut = createManifestJSONFile(passOut, staticImgDir); // step 2
 
		byte[] signatureBytes = signManifestFile(manifestOut, signingInformation); // step 3
 
		return createPkPassArchive(passOut, manifestOut, signatureBytes); //step 4
 
}

The only problem with simply using the jPassKit API is that it relies on a writable file system. To work in Google's read-only file system we are going to have to create everything in memory using byte[]'s. The good news is that Google let's us include static files so we can keep a directory on the App-Engine that contains all the image files we need and just include those which we do in the code above with staticImgDir. Let's take the each step and look more closely at what I'm doing.

Create pass.json - Step 1

Let's look at just creating the pass.json from the PKPass object. I start out by setting up the jsonObjectMapper that will take care of taking the PKPass object and writing it to a byte array. That is all that is going on in the method below and this is the easiest step.

private static byte[] createPassJSONFile(final PKPass pass) throws IOException,
 
	 JsonGenerationException, JsonMappingException { 
	 ObjectMapper jsonObjectMapper = new ObjectMapper();
	 jsonObjectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false);
 
	 jsonObjectMapper.setDateFormat(new ISO8601DateFormat());
 
 
		FilterProvider filters = new SimpleFilterProvider().addFilter("pkPassFilter", SimpleBeanPropertyFilter.serializeAllExcept("valid",
 
		 "validationErrors", "foregroundColorAsObject", "backgroundColorAsObject", "labelColorAsObject"));
 
		jsonObjectMapper.setSerializationInclusion(Inclusion.NON_NULL);
 
		jsonObjectMapper.getSerializationConfig().addMixInAnnotations(Object.class, PropertyFilterMixIn.class);
 
		ByteArrayOutputStream out = new ByteArrayOutputStream();	
 
		ObjectWriter objectWriter = jsonObjectMapper.writer(filters);
 
		objectWriter.writeValue(out, pass);		
 
		return out.toByteArray();
 
	}
Create manifest.json - Step 2

For the manifest.json we need to grab all the static image files and the pass.json we've create and the pass.json file, get their file name and include them in the manifest with their hash code. I used java.net.URL to reference the static image files. Apple wants the static image files to contain the '@' symbol but Google App-Engine does not like '@' in the URL. So I had to store the files on the App-Engine without the '@' and add it back in before adding the file to the manifest and then in the pkpass file.

private static byte[] createManifestJSONFile(byte[] outForPassFile, File imgDir, String passName) throws IOException,
	 JsonGenerationException, JsonMappingException, NoSuchAlgorithmException {
 
		ObjectMapper jsonObjectMapper = new ObjectMapper();
	 jsonObjectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false);
	 jsonObjectMapper.setDateFormat(new ISO8601DateFormat());
 
		MapString, String> fileWithHashMap = new HashMapString, String>();
 
		ByteArrayOutputStream out = new ByteArrayOutputStream();
 
		File[] imgFiles = imgDir.listFiles();
		HashFunction hashFunction = Hashing.sha1();
		hashFilesInDirectory(imgFiles, fileWithHashMap, hashFunction); // fiddle the name to include the @ then hash
 
		// finally include the pass.json and its hash
		HashCode hash = hashFunction.hashBytes(outForPassFile);
		fileWithHashMap.put(passName, encodeHexString(hash.asBytes()));
 
		jsonObjectMapper.writeValue(out, fileWithHashMap);
 
		return out.toByteArray();
	}
Sign the manifest and create signature file - Step 3

In the method below you will see a lot of BouncyCastle specific code. The signingInformation parameter is an object from the jPassKit API that contains any certification that you may need as well as the private key you have to sign the manifest file. Feel free to use whatever security package you are comfortable with.

private static byte[] signManifestFile(byte[] manifestOut, final PKSigningInformation signingInformation) throws Exception {
 
 if (manifestOut == null || signingInformation == null || !signingInformation.isValid()) {
 throw new IllegalArgumentException("Null params are not supported");
 }
 addBCProvider();
 
 CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
 ContentSigner sha1Signer = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(
 signingInformation.getSigningPrivateKey());
 
 generator.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider(
 BouncyCastleProvider.PROVIDER_NAME).build()).build(sha1Signer, signingInformation.getSigningCert()));
 
 ListX509Certificate> certList = new ArrayListX509Certificate>();
 certList.add(signingInformation.getAppleWWDRCACert());
 certList.add(signingInformation.getSigningCert());
 
 Store certs = new JcaCertStore(certList);
 
 generator.addCertificates(certs);
 
 CMSSignedData sigData = generator.generate(new CMSProcessableByteArray(manifestOut), false);
 byte[] signedDataBytes = sigData.getEncoded();
 
 return signedDataBytes;
 }
Create the .pkpass archive - Step 4

We can use java.util.zip.ZipOutputStream to zip everything together and put a .pkpass extension on the resulting archive. In this case we are only including four of the six or so potential image files you can use. FileSystemInformation is a class I created to hold the static directory information such as the location of the files. There is a fair bit of byte array conversion going on and that is because we are going to useZipOutputStream to create the archive in memory and pass it across the wire.

private static byte[] createPkPassArchive(byte[] pass, byte[] manifest, byte[] signature, FileSystemInformation fsi) throws IOException {
 
 	 final String[] fileList = {"icon.png","icon@2x.png","strip.png", "strip@2x.png",
 
 			 fsi.getPassName(), fsi.getManifestName(), fsi.getSignatureName()};
 
 	 final byte[][] contentsList = {
 
 			 convertURLtoByteArray(fsi.getIcon()),
 
 			 convertURLtoByteArray(fsi.getIcon2x()),
 
 			 convertURLtoByteArray(fsi.getStrip()),
 
 			 convertURLtoByteArray(fsi.getStrip2x()),
 
 			 pass,
 
 			 manifest,
 
 			 signature
 
 	 };
 
 
	ByteArrayOutputStream byteArrayOutputStreamForZippedPass = new ByteArrayOutputStream();
 
 ZipOutputStream zipOutputStream = new ZipOutputStream(byteArrayOutputStreamForZippedPass);
 
 
 for (int i = 0; i  fileList.length; i++) {
 
 	ZipEntry entry = new ZipEntry(fileList[i]);
 
 	byte[] contents = contentsList[i];
 
 	entry.setSize(contents.length);
 
 	zipOutputStream.putNextEntry(entry);
 
 	zipOutputStream.write(contents);
 
 	zipOutputStream.closeEntry();
 
 }
 
 zipOutputStream.close();
 
 return byteArrayOutputStreamForZippedPass.toByteArray();
 
	}

The nice thing about being forced to create the .pkpass archive in memory is that there is little to store in a database. Because there is little to store in a database I found using the provided Google DataStore to be sufficient to store what I needed and the Objectify Java API was perfect for creating a connection to the DataStore.

Note on Apple Passbook Notification Services(APNS)

Apple does not have a particular suggestion as to what URL the device calls to create the pass. Only that the media type on return is 'application/vnd.apple.pkpass' so feel free to use your Jersey annotations to create whatever GET URL you want.

Further passbook service URL's are defined by Apple in their APNS documentation. Things such as registering the pass for updates etc have their URL signature defined in the APNS related to Passbook. With Jersey it is a snap to create services for your Passbook that met APNS guidelines. See the example below:

@POST
 
		@Produces(MediaType.APPLICATION_JSON)
 
		@Path("/v1/devices/{deviceLibraryIdent}/registrations/{passTypeIdent}/{serialNumber}")
 
		public Response register(APNSRequest request, @PathParam("deviceLibraryIdent") String deviceLibraryIdent, 
 
				@PathParam("serialNumber") String serialNumber) {			
 
			....
 
		}

Good Luck!