1 package org.jivesoftware.openfire.sasl; 2 3 import org.jivesoftware.util.JiveGlobals; 4 import org.jivesoftware.util.StringUtils; 5 6 import javax.security.sasl.Sasl; 7 import javax.security.sasl.SaslException; 8 import javax.security.sasl.SaslServer; 9 import java.nio.charset.StandardCharsets; 10 import java.util.StringTokenizer; 11 12 /** 13 * Implementation of a proprietary Jive Software SASL mechanism that is based on a shared secret. Successful 14 * authentication will result in an anonymous authorization. 15 * 16 * @author Guus der Kinderen, guus@goodbytes.nl 17 */ 18 public class JiveSharedSecretSaslServer implements SaslServer 19 { 20 public static final String NAME = "JIVE-SHAREDSECRET"; 21 22 private boolean complete = false; 23 24 @Override getMechanismName()25 public String getMechanismName() 26 { 27 return NAME; 28 } 29 30 @Override evaluateResponse( byte[] response )31 public byte[] evaluateResponse( byte[] response ) throws SaslException 32 { 33 if ( isComplete() ) 34 { 35 throw new IllegalStateException( "Authentication exchange already completed." ); 36 } 37 38 if ( response == null || response.length == 0 ) 39 { 40 // No info was provided so send a challenge to get it. 41 return new byte[ 0 ]; 42 } 43 44 complete = true; 45 46 // Parse data and obtain username & password. 47 final StringTokenizer tokens = new StringTokenizer( new String( response, StandardCharsets.UTF_8 ), "\0" ); 48 tokens.nextToken(); 49 final String secretDigest = tokens.nextToken(); 50 51 if ( authenticateSharedSecret( secretDigest ) ) 52 { 53 return null; // Success! 54 } 55 else 56 { 57 // Otherwise, authentication failed. 58 throw new SaslException( "Authentication failed" ); 59 } 60 } 61 62 @Override isComplete()63 public boolean isComplete() 64 { 65 return complete; 66 } 67 68 @Override getAuthorizationID()69 public String getAuthorizationID() 70 { 71 if ( !isComplete() ) 72 { 73 throw new IllegalStateException( "Authentication exchange not completed." ); 74 } 75 76 return null; // Anonymous! 77 } 78 79 @Override unwrap( byte[] incoming, int offset, int len )80 public byte[] unwrap( byte[] incoming, int offset, int len ) throws SaslException 81 { 82 if ( !isComplete() ) 83 { 84 throw new IllegalStateException( "Authentication exchange not completed." ); 85 } 86 87 throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." ); 88 } 89 90 @Override wrap( byte[] outgoing, int offset, int len )91 public byte[] wrap( byte[] outgoing, int offset, int len ) throws SaslException 92 { 93 if ( !isComplete() ) 94 { 95 throw new IllegalStateException( "Authentication exchange not completed." ); 96 } 97 98 throw new IllegalStateException( "SASL Mechanism '" + getMechanismName() + " does not support integrity nor privacy." ); 99 } 100 101 @Override getNegotiatedProperty( String propName )102 public Object getNegotiatedProperty( String propName ) 103 { 104 if ( !isComplete() ) 105 { 106 throw new IllegalStateException( "Authentication exchange not completed." ); 107 } 108 109 if ( propName.equals( Sasl.QOP ) ) 110 { 111 return "auth"; 112 } 113 else 114 { 115 return null; 116 } 117 } 118 119 @Override dispose()120 public void dispose() throws SaslException 121 { 122 complete = false; 123 } 124 125 /** 126 * Returns true if the supplied digest matches the shared secret value. The digest must be an MD5 hash of the secret 127 * key, encoded as hex. This value is supplied by clients attempting shared secret authentication. 128 * 129 * @param digest the MD5 hash of the secret key, encoded as hex. 130 * @return true if authentication succeeds. 131 */ authenticateSharedSecret( String digest )132 public static boolean authenticateSharedSecret( String digest ) 133 { 134 if ( !isSharedSecretAllowed() ) 135 { 136 return false; 137 } 138 139 return StringUtils.hash( getSharedSecret() ).equals( digest ); 140 } 141 142 /** 143 * Returns true if shared secret authentication is enabled. Shared secret authentication creates an anonymous 144 * session, but requires that the authenticating entity know a shared secret key. The client sends a digest of the 145 * secret key, which is compared against a digest of the local shared key. 146 * 147 * @return true if shared secret authentication is enabled. 148 */ isSharedSecretAllowed()149 public static boolean isSharedSecretAllowed() 150 { 151 return JiveGlobals.getBooleanProperty( "xmpp.auth.sharedSecretEnabled" ); 152 } 153 154 /** 155 * Returns the shared secret value, or {@code null} if shared secret authentication is disabled. If this is the 156 * first time the shared secret value has been requested (and shared secret auth is enabled), the key will be 157 * randomly generated and stored in the property {@code xmpp.auth.sharedSecret}. 158 * 159 * @return the shared secret value. 160 */ getSharedSecret()161 public static String getSharedSecret() 162 { 163 if ( !isSharedSecretAllowed() ) 164 { 165 return null; 166 } 167 168 String sharedSecret = JiveGlobals.getProperty( "xmpp.auth.sharedSecret" ); 169 if ( sharedSecret == null ) 170 { 171 sharedSecret = StringUtils.randomString( 8 ); 172 JiveGlobals.setProperty( "xmpp.auth.sharedSecret", sharedSecret ); 173 } 174 return sharedSecret; 175 } 176 177 /** 178 * Sets whether shared secret authentication is enabled. Shared secret authentication creates an anonymous session, 179 * but requires that the authenticating entity know a shared secret key. The client sends a digest of the secret 180 * key, which is compared against a digest of the local shared key. 181 * 182 * @param sharedSecretAllowed true if shared secret authentication should be enabled. 183 */ setSharedSecretAllowed( boolean sharedSecretAllowed )184 public static void setSharedSecretAllowed( boolean sharedSecretAllowed ) 185 { 186 JiveGlobals.setProperty( "xmpp.auth.sharedSecretEnabled", sharedSecretAllowed ? "true" : "false" ); 187 } 188 } 189