Skip to content

Instantly share code, notes, and snippets.

@rruhlen
Created September 12, 2013 18:55
Show Gist options
  • Save rruhlen/6542239 to your computer and use it in GitHub Desktop.
Save rruhlen/6542239 to your computer and use it in GitHub Desktop.
Multipass SSO Java example for Desk.com
package com.meetup.util.desk;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.meetup.util.MeetupLogger;
import com.meetup.base.util.U;
import com.meetup.base.util.api.RemoteApiUtil;
import com.meetup.base.util.api.RemoteApiUtil.JsonEncodingException;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.security.Key;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.io.HexDump;
import org.apache.commons.codec.binary.Base64;
/**
* Signing utility for desk.com sso
* Result r = Multipass.signer( sitetoken, apitoken )
* .uid( ... )
* .customerName( ... )
* .customerEmail( ... )
* .expires( ... )
* .sign();
* String url = String.format( "host.com/path?multipass=%s&signature=%s",
* r.token() r.signature() );
*/
public class Multipass {
public static class Signer {
public static class Result {
private final String token;
private final String signature;
public Result( String token, String sig ) {
this.token = token;
this.signature = sig;
}
public String token() {
return this.token;
}
public String signature() {
return this.signature;
}
}
private static final MeetupLogger log =
MeetupLogger.getLogger( Multipass.class.getName() );
private static final String SIGN_ALG = "HmacSHA1";
private static final String TOKEN_ALG = "AES";
// desk.com requires AES128-cbc with 16 byte blocks
// see http://docs.oracle.com/javase/7/docs/api/javax/crypto/Cipher.html
private static final String TOKEN_CIPHER = "AES/CBC/NoPadding";
private static final String STR_ENCODING = "utf8";
private static final Set<String> require = ImmutableSet.of(
"uid", "customer_name", "customer_email", "expires"
);
private static final String DESK_IV_SRC = "OpenSSL for Ruby";
private static final int BLOCK_SIZE = DESK_IV_SRC.length();
private final ImmutableMap.Builder params = ImmutableMap.builder();
private final byte[] secretKey;
private final byte[] apiKey;
private final byte[] iv;
public Signer( String siteKey, String apiKey ) {
try { this.secretKey = sha1( bytes( apiKey + siteKey ), BLOCK_SIZE ); }
catch ( NoSuchAlgorithmException na ) {
throw new IllegalStateException( "This system does not support SHA-1 encryption",
na );
}
this.apiKey = bytes( apiKey );
this.iv = bytes( DESK_IV_SRC );
}
public Signer uid( String id ) {
this.params.put( "uid", id );
return this;
}
public Signer customerName( String n ) {
this.params.put( "customer_name", n );
return this;
}
public Signer customerEmail( String e ) {
this.params.put( "customer_email", e );
return this;
}
public Signer expires( DateTime e ) {
this.params.put( "expires", ISODateTimeFormat.ordinalDateTimeNoMillis().print( e ) );
return this;
}
public Signer to( String to ) {
this.params.put( "to", to );
return this;
}
public Signer custom( String key, String value ) {
this.params.put( "customer_custom_" + key, value );
return this;
}
public Result sign() {
Map<String, String> ps = this.params.build();
if ( validate( ps ) ) {
try {
String token = token();
return new Result( token, sign( token ) );
}
catch ( BadPaddingException pb ) {
pb.printStackTrace();
log.error( "bad padding", pb );
}
catch ( InvalidAlgorithmParameterException ip ) {
ip.printStackTrace();
log.error( "invalid algo param", ip );
}
catch ( IllegalBlockSizeException ib ) {
ib.printStackTrace();
log.error( "illegal block size", ib );
}
catch ( JsonEncodingException je ) {
je.printStackTrace();
log.error( "json encoding exception", je );
}
catch ( NoSuchAlgorithmException na ) {
na.printStackTrace();
log.error( "no such alg ", na );
}
catch ( NoSuchPaddingException np ) {
np.printStackTrace();
log.error("no such padding", np );
}
catch ( InvalidKeyException ik ) {
ik.printStackTrace();
log.error( "invalid key", ik );
}
}
else {
throw new IllegalStateException( "missing or invalid params: " + require );
}
return null;
}
public String json() throws JsonEncodingException {
return RemoteApiUtil.jsonize( params.build() ).toString();
}
private String sign( String token )
throws NoSuchAlgorithmException,
InvalidKeyException {
Key key = new SecretKeySpec( apiKey, SIGN_ALG );
Mac mac = Mac.getInstance( SIGN_ALG );
mac.init( key );
return base64( mac.doFinal( bytes( token ) ) );
}
public String decrypt( String token ) {
try {
byte[] decoded = Base64.decodeBase64( bytes( unesc( token ) ) );
Cipher cipher = Cipher.getInstance( TOKEN_CIPHER );
cipher.init( Cipher.DECRYPT_MODE,
new SecretKeySpec( secretKey, TOKEN_ALG ),
new IvParameterSpec( iv ) );
return new String( xorFirstBlock( cipher.doFinal( decoded ) ),
STR_ENCODING );
}
catch ( Exception e ) {
log.error( "error decrypting token " + token, e );
throw new RuntimeException( e );
}
}
private String token()
throws BadPaddingException,
NoSuchAlgorithmException,
NoSuchPaddingException,
InvalidAlgorithmParameterException,
IllegalBlockSizeException,
InvalidKeyException,
JsonEncodingException {
// cipher iv (Initialization Vector)
IvParameterSpec ivSpec = new IvParameterSpec( iv );
// payload
byte[] payload = bytes( json() );
int pad = BLOCK_SIZE - payload.length % BLOCK_SIZE;
byte[] padded = xorFirstBlock( pad == BLOCK_SIZE ? payload : padTo( payload, pad ) );
// encrypt
Cipher cipher = Cipher.getInstance( TOKEN_CIPHER );
cipher.init( Cipher.ENCRYPT_MODE,
new SecretKeySpec( secretKey, TOKEN_ALG ),
ivSpec );
byte[] encr = cipher.doFinal( padded );
// b64 encode and escape
return esc( base64( encr ) );
}
private byte[] xorFirstBlock( byte[] bytes ) {
for ( int i = 0; i < iv.length; i++ ) {
bytes[i] = (byte) ( bytes[i] ^ iv[i] );
}
return bytes;
}
private boolean validate( Map<String, String> ps ) {
for ( String r : require ) {
if ( U.isEmpty( ps.get( r ) ) ) {
return false;
}
}
return true;
}
// utils
private String esc( String s ) {
return s.replaceAll( "\\s+", "" )
.replaceAll( "\\=+$", "" )
.replaceAll( "\\+", "-" )
.replaceAll( "\\/","_" );
}
private String unesc( String s ) {
return s.replaceAll( "_", "/" )
.replaceAll( "\\-", "+" );
}
private byte[] padTo( byte[] bytes, int pad ) {
byte[] padded = Arrays.copyOf( bytes, bytes.length + pad );
if ( pad > 0 ) {
for ( int i = bytes.length; i < padded.length; i ++ ) {
padded[i] = (byte) pad;
}
}
return padded;
}
private byte[] bytes( String str ) {
try { return str.getBytes( STR_ENCODING ); }
catch ( UnsupportedEncodingException e ) {
// not likely
log.error( "unsupported encoding " + STR_ENCODING, e );
return null;
}
}
private byte[] sha1( byte[] bytes, int len ) throws NoSuchAlgorithmException {
return Arrays.copyOf( MessageDigest.getInstance( "SHA-1" ).digest( bytes ), len );
}
private final String base64( byte[] bytes ) {
return Base64.encodeBase64String( bytes );
}
}
/** factory for Signer */
public static final Signer signer( String siteKey, String apiKey ) {
return new Signer( siteKey, apiKey );
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment