Skip to content

Commit 6a6c326

Browse files
committed
Merge pull request robbiehanson#341 from ChatSecure/scram-sha1
Implemented SASL SCRAM-SHA-1 Authentication
2 parents 62deb74 + 06b70fd commit 6a6c326

File tree

5 files changed

+373
-2
lines changed

5 files changed

+373
-2
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// XMPPSCRAMSHA1Authentication.h
3+
// iPhoneXMPP
4+
//
5+
// Created by David Chiles on 3/21/14.
6+
//
7+
//
8+
9+
#import <Foundation/Foundation.h>
10+
#import "XMPPSASLAuthentication.h"
11+
#import "XMPPStream.h"
12+
13+
@interface XMPPSCRAMSHA1Authentication : NSObject <XMPPSASLAuthentication>
14+
15+
@end
16+
17+
@interface XMPPStream (XMPPSCRAMSHA1Authentication)
18+
19+
- (BOOL)supportsSCRAMSHA1Authentication;
20+
21+
@end
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
//
2+
// XMPPSCRAMSHA1Authentication.m
3+
// iPhoneXMPP
4+
//
5+
// Created by David Chiles on 3/21/14.
6+
//
7+
//
8+
9+
#import "XMPPSCRAMSHA1Authentication.h"
10+
#import "XMPP.h"
11+
#import "XMPPLogging.h"
12+
#import "XMPPStream.h"
13+
#import "XMPPInternal.h"
14+
#import "NSData+XMPP.h"
15+
#import "XMPPStringPrep.h"
16+
17+
#import <CommonCrypto/CommonKeyDerivation.h>
18+
19+
20+
#if ! __has_feature(objc_arc)
21+
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
22+
#endif
23+
24+
// Log levels: off, error, warn, info, verbose
25+
#if DEBUG
26+
static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO; // | XMPP_LOG_FLAG_TRACE;
27+
#else
28+
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
29+
#endif
30+
31+
@interface XMPPSCRAMSHA1Authentication ()
32+
{
33+
#if __has_feature(objc_arc_weak)
34+
__weak XMPPStream *xmppStream;
35+
#else
36+
__unsafe_unretained XMPPStream *xmppStream;
37+
#endif
38+
}
39+
40+
@property (nonatomic) BOOL awaitingChallenge;
41+
@property (nonatomic, strong) NSString *username;
42+
@property (nonatomic, strong) NSString *password;
43+
@property (nonatomic, strong) NSString *clientNonce;
44+
@property (nonatomic, strong) NSString *combinedNonce;
45+
@property (nonatomic, strong) NSString *salt;
46+
@property (nonatomic, strong) NSNumber *count;
47+
@property (nonatomic, strong) NSString *serverMessage1;
48+
@property (nonatomic, strong) NSString *clientFirstMessageBare;
49+
@property (nonatomic, strong) NSData *serverSignatureData;
50+
@property (nonatomic, strong) NSData *clientProofData;
51+
@property (nonatomic) CCHmacAlgorithm hashAlgorithm;
52+
53+
@end
54+
55+
///////////RFC5802 http://tools.ietf.org/html/rfc5802 //////////////
56+
57+
//Channel binding not yet supported
58+
59+
@implementation XMPPSCRAMSHA1Authentication
60+
61+
+ (NSString *)mechanismName
62+
{
63+
return @"SCRAM-SHA-1";
64+
}
65+
66+
- (id)initWithStream:(XMPPStream *)stream password:(NSString *)password
67+
{
68+
if ((self = [super init])) {
69+
xmppStream = stream;
70+
self.username = [XMPPStringPrep prepNode:[xmppStream.myJID user]];
71+
self.password = [XMPPStringPrep prepNode:password];
72+
self.hashAlgorithm = kCCHmacAlgSHA1;
73+
}
74+
return self;
75+
}
76+
77+
- (BOOL)start:(NSError **)errPtr
78+
{
79+
XMPPLogTrace();
80+
81+
if(self.username.length || self.password.length) {
82+
83+
NSXMLElement *auth = [NSXMLElement elementWithName:@"auth" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"];
84+
[auth addAttributeWithName:@"mechanism" stringValue:@"SCRAM-SHA-1"];
85+
[auth setStringValue:[self clientMessage1]];
86+
87+
[xmppStream sendAuthElement:auth];
88+
self.awaitingChallenge = YES;
89+
90+
return YES;
91+
}
92+
else {
93+
return NO;
94+
}
95+
}
96+
97+
- (XMPPHandleAuthResponse)handleAuth1:(NSXMLElement *)authResponse
98+
{
99+
XMPPLogTrace();
100+
101+
// We're expecting a challenge response.
102+
// If we get anything else we're going to assume it's some kind of failure response.
103+
104+
if (![[authResponse name] isEqualToString:@"challenge"])
105+
{
106+
return XMPP_AUTH_FAIL;
107+
}
108+
109+
NSDictionary *auth = [self dictionaryFromChallenge:authResponse];
110+
111+
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
112+
[numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
113+
114+
self.combinedNonce = [auth objectForKey:@"r"];
115+
self.salt = [auth objectForKey:@"s"];
116+
self.count = [numberFormatter numberFromString:[auth objectForKey:@"i"]];
117+
118+
//We have all the necessary information to calculate client proof and server signature
119+
if ([self calculateProofs]) {
120+
NSXMLElement *response = [NSXMLElement elementWithName:@"response" xmlns:@"urn:ietf:params:xml:ns:xmpp-sasl"];
121+
[response setStringValue:[self clientMessage2]];
122+
123+
[xmppStream sendAuthElement:response];
124+
self.awaitingChallenge = NO;
125+
126+
return XMPP_AUTH_CONTINUE;
127+
}
128+
else {
129+
return XMPP_AUTH_FAIL;
130+
}
131+
}
132+
133+
- (XMPPHandleAuthResponse)handleAuth2:(NSXMLElement *)authResponse
134+
{
135+
XMPPLogTrace();
136+
137+
NSDictionary *auth = [self dictionaryFromChallenge:authResponse];
138+
139+
if ([[authResponse name] isEqual:@"success"]) {
140+
NSString *receivedServerSignature = [auth objectForKey:@"v"];
141+
142+
if([self.serverSignatureData isEqualToData:[[receivedServerSignature dataUsingEncoding:NSUTF8StringEncoding] xmpp_base64Decoded]]){
143+
return XMPP_AUTH_SUCCESS;
144+
}
145+
else {
146+
return XMPP_AUTH_FAIL;
147+
}
148+
}
149+
else {
150+
return XMPP_AUTH_FAIL;
151+
}
152+
}
153+
154+
- (XMPPHandleAuthResponse)handleAuth:(NSXMLElement *)auth
155+
{
156+
XMPPLogTrace();
157+
158+
if (self.awaitingChallenge) {
159+
return [self handleAuth1:auth];
160+
}
161+
else {
162+
return [self handleAuth2:auth];
163+
}
164+
}
165+
166+
- (NSString *)clientMessage1
167+
{
168+
self.clientNonce = [XMPPStream generateUUID];
169+
170+
self.clientFirstMessageBare = [NSString stringWithFormat:@"n=%@,r=%@",self.username,self.clientNonce];
171+
172+
NSData *message1Data = [[NSString stringWithFormat:@"n,,%@",self.clientFirstMessageBare] dataUsingEncoding:NSUTF8StringEncoding];
173+
174+
return [message1Data xmpp_base64Encoded];
175+
}
176+
177+
- (NSString *)clientMessage2
178+
{
179+
NSString *clientProofString = [self.clientProofData xmpp_base64Encoded];
180+
NSData *message2Data = [[NSString stringWithFormat:@"c=biws,r=%@,p=%@",self.combinedNonce,clientProofString] dataUsingEncoding:NSUTF8StringEncoding];
181+
182+
return [message2Data xmpp_base64Encoded];
183+
}
184+
185+
- (BOOL)calculateProofs
186+
{
187+
//Check to see that we have a password, salt and iteration count above 4096 (from RFC5802)
188+
if (!self.password.length || !self.salt.length || self.count.unsignedIntegerValue < 4096) {
189+
return NO;
190+
}
191+
192+
NSData *passwordData = [self.password dataUsingEncoding:NSUTF8StringEncoding];
193+
NSData *saltData = [[self.salt dataUsingEncoding:NSUTF8StringEncoding] xmpp_base64Decoded];
194+
195+
NSData *saltedPasswordData = [self HashWithAlgorithm:self.hashAlgorithm password:passwordData salt:saltData iterations:[self.count unsignedIntValue]];
196+
197+
NSData *clientKeyData = [self HashWithAlgorithm:self.hashAlgorithm data:[@"Client Key" dataUsingEncoding:NSUTF8StringEncoding] key:saltedPasswordData];
198+
NSData *serverKeyData = [self HashWithAlgorithm:self.hashAlgorithm data:[@"Server Key" dataUsingEncoding:NSUTF8StringEncoding] key:saltedPasswordData];
199+
200+
NSData *storedKeyData = [clientKeyData xmpp_sha1Digest];
201+
202+
NSData *authMessageData = [[NSString stringWithFormat:@"%@,%@,c=biws,r=%@",self.clientFirstMessageBare,self.serverMessage1,self.combinedNonce] dataUsingEncoding:NSUTF8StringEncoding];
203+
204+
NSData *clientSignatureData = [self HashWithAlgorithm:self.hashAlgorithm data:authMessageData key:storedKeyData];
205+
206+
self.serverSignatureData = [self HashWithAlgorithm:self.hashAlgorithm data:authMessageData key:serverKeyData];
207+
self.clientProofData = [self xorData:clientKeyData withData:clientSignatureData];
208+
209+
//check to see that we caclulated some client proof and server signature
210+
if (self.clientProofData && self.serverSignatureData) {
211+
return YES;
212+
}
213+
else {
214+
return NO;
215+
}
216+
}
217+
218+
- (NSData *)HashWithAlgorithm:(CCHmacAlgorithm) algorithm password:(NSData *)passwordData salt:(NSData *)saltData iterations:(NSUInteger)rounds
219+
{
220+
NSMutableData *mutableSaltData = [saltData mutableCopy];
221+
UInt8 zeroHex= 0x00;
222+
UInt8 oneHex= 0x01;
223+
NSData *zeroData = [[NSData alloc] initWithBytes:&zeroHex length:sizeof(zeroHex)];
224+
NSData *oneData = [[NSData alloc] initWithBytes:&oneHex length:sizeof(oneHex)];
225+
226+
[mutableSaltData appendData:zeroData];
227+
[mutableSaltData appendData:zeroData];
228+
[mutableSaltData appendData:zeroData];
229+
[mutableSaltData appendData:oneData];
230+
231+
NSData *result = [self HashWithAlgorithm:algorithm data:mutableSaltData key:passwordData];
232+
NSData *previous = [result copy];
233+
234+
for (int i = 1; i < rounds; i++) {
235+
previous = [self HashWithAlgorithm:algorithm data:previous key:passwordData];
236+
result = [self xorData:result withData:previous];
237+
}
238+
239+
return result;
240+
}
241+
242+
- (NSData *)HashWithAlgorithm:(CCHmacAlgorithm) algorithm data:(NSData *)data key:(NSData *)key
243+
{
244+
unsigned char cHMAC[CC_SHA1_DIGEST_LENGTH];
245+
246+
CCHmac(algorithm, [key bytes], [key length], [data bytes], [data length], cHMAC);
247+
248+
return [[NSData alloc] initWithBytes:cHMAC length:sizeof(cHMAC)];
249+
}
250+
251+
- (NSData *)xorData:(NSData *)data1 withData:(NSData *)data2
252+
{
253+
NSMutableData *result = data1.mutableCopy;
254+
255+
char *dataPtr = (char *)result.mutableBytes;
256+
257+
char *keyData = (char *)data2.bytes;
258+
259+
char *keyPtr = keyData;
260+
int keyIndex = 0;
261+
262+
for (int x = 0; x < data1.length; x++) {
263+
*dataPtr = *dataPtr ^ *keyPtr;
264+
dataPtr++;
265+
keyPtr++;
266+
267+
if (++keyIndex == data2.length) {
268+
keyIndex = 0;
269+
keyPtr = keyData;
270+
}
271+
}
272+
return result;
273+
}
274+
275+
- (NSDictionary *)dictionaryFromChallenge:(NSXMLElement *)challenge
276+
{
277+
// The value of the challenge stanza is base 64 encoded.
278+
// Once "decoded", it's just a string of key=value pairs separated by commas.
279+
280+
NSData *base64Data = [[challenge stringValue] dataUsingEncoding:NSASCIIStringEncoding];
281+
NSData *decodedData = [base64Data xmpp_base64Decoded];
282+
283+
self.serverMessage1 = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding];
284+
285+
XMPPLogVerbose(@"%@: Decoded challenge: %@", THIS_FILE, self.serverMessage1);
286+
287+
NSArray *components = [self.serverMessage1 componentsSeparatedByString:@","];
288+
NSMutableDictionary *auth = [NSMutableDictionary dictionaryWithCapacity:5];
289+
290+
for (NSString *component in components)
291+
{
292+
NSRange separator = [component rangeOfString:@"="];
293+
if (separator.location != NSNotFound)
294+
{
295+
NSMutableString *key = [[component substringToIndex:separator.location] mutableCopy];
296+
NSMutableString *value = [[component substringFromIndex:separator.location+1] mutableCopy];
297+
298+
if(key) CFStringTrimWhitespace((__bridge CFMutableStringRef)key);
299+
if(value) CFStringTrimWhitespace((__bridge CFMutableStringRef)value);
300+
301+
if ([value hasPrefix:@"\""] && [value hasSuffix:@"\""] && [value length] > 2)
302+
{
303+
// Strip quotes from value
304+
[value deleteCharactersInRange:NSMakeRange(0, 1)];
305+
[value deleteCharactersInRange:NSMakeRange([value length]-1, 1)];
306+
}
307+
308+
if(key && value)
309+
{
310+
[auth setObject:value forKey:key];
311+
}
312+
}
313+
}
314+
315+
return auth;
316+
}
317+
@end
318+
319+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
320+
#pragma mark -
321+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
322+
323+
@implementation XMPPStream (XMPPSCRAMSHA1Authentication)
324+
325+
- (BOOL)supportsSCRAMSHA1Authentication
326+
{
327+
return [self supportsAuthenticationMechanism:[XMPPSCRAMSHA1Authentication mechanismName]];
328+
}
329+
330+
@end

Core/XMPP.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
#import "XMPPSASLAuthentication.h"
1818
#import "XMPPDigestMD5Authentication.h"
19+
#import "XMPPSCRAMSHA1Authentication.h"
1920
#import "XMPPPlainAuthentication.h"
2021
#import "XMPPXFacebookPlatformAuthentication.h"
2122
#import "XMPPAnonymousAuthentication.h"

Core/XMPPStream.m

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,8 +1916,13 @@ - (BOOL)authenticateWithPassword:(NSString *)inPassword error:(NSError **)errPtr
19161916
// P.S. - This method is deprecated.
19171917

19181918
id <XMPPSASLAuthentication> someAuth = nil;
1919-
1920-
if ([self supportsDigestMD5Authentication])
1919+
1920+
if ([self supportsSCRAMSHA1Authentication])
1921+
{
1922+
someAuth = [[XMPPSCRAMSHA1Authentication alloc] initWithStream:self password:password];
1923+
result = [self authenticate:someAuth error:&err];
1924+
}
1925+
else if ([self supportsDigestMD5Authentication])
19211926
{
19221927
someAuth = [[XMPPDigestMD5Authentication alloc] initWithStream:self password:password];
19231928
result = [self authenticate:someAuth error:&err];

0 commit comments

Comments
 (0)