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