From 2e0fc3ca09c28312f487b23382bc9f31aa118627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20H=C3=B6ft?= Date: Sun, 3 May 2020 19:17:54 +0200 Subject: [PATCH] feat(core): Add PBKDF2 support Extend NSData+Crypto to support PBKDF2 with SHA1 HMAC as dovecot is using it since v2.3.0. The format hashed passwords is {PBKDF2}$1$$$ The implementation of pkcs#5 PBKDF2 is taken from openbsd (with minor adjustments) as OpenSSL and GnuTLS would require quite new versions to support this hash. --- Documentation/SOGoInstallationGuide.asciidoc | 5 +- SoObjects/SOGo/GNUmakefile | 2 +- SoObjects/SOGo/NSData+Crypto.h | 2 +- SoObjects/SOGo/NSData+Crypto.m | 63 ++++++++- SoObjects/SOGo/pkcs5_pbkdf2.c | 141 +++++++++++++++++++ SoObjects/SOGo/pkcs5_pbkdf2.h | 15 ++ Tests/Unit/TestNSString+Crypto.m | 29 +++- 7 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 SoObjects/SOGo/pkcs5_pbkdf2.c create mode 100644 SoObjects/SOGo/pkcs5_pbkdf2.h diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index d69703d3e..9cf550b48 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -1660,8 +1660,9 @@ they have the same name as popular LDAP attributes (such as `givenName`, passwords. Possible values are: `none`, `plain`, `crypt`, `md5`, `md5-crypt`, `smd5`, `cram-md5`, `ldap-md5`, and `sha`, `sha256`, `sha256-crypt`, `sha512`, `sha512-crypt`, its ssha (e.g. `ssha` or -`ssha256`) variants, `blf-crypt`, and `sym-aes-128-cbc`. Passwords -can have the scheme prepended in the form `{scheme}encryptedPass`. +`ssha256`) variants, `blf-crypt`, `PBKDF2`, and `sym-aes-128-cbc`. +Passwords can have the scheme prepended in the form +`{scheme}encryptedPass`. If no scheme is given, _userPasswordAlgorithm_ is used instead. The schemes listed above follow the algorithms described in diff --git a/SoObjects/SOGo/GNUmakefile b/SoObjects/SOGo/GNUmakefile index df32913e9..572eaca66 100644 --- a/SoObjects/SOGo/GNUmakefile +++ b/SoObjects/SOGo/GNUmakefile @@ -167,7 +167,7 @@ SOGo_OBJC_FILES = \ SOGoCredentialsFile.m \ SOGoTextTemplateFile.m -SOGo_C_FILES += lmhash.c aes.c crypt_blowfish.c +SOGo_C_FILES += lmhash.c aes.c crypt_blowfish.c pkcs5_pbkdf2.c SOGo_RESOURCE_FILES = \ SOGoDefaults.plist \ diff --git a/SoObjects/SOGo/NSData+Crypto.h b/SoObjects/SOGo/NSData+Crypto.h index 117fa9241..b0106f77f 100644 --- a/SoObjects/SOGo/NSData+Crypto.h +++ b/SoObjects/SOGo/NSData+Crypto.h @@ -54,7 +54,7 @@ - (NSData *) asSymAES128CBCUsingIV: (NSString *) theIV keyPath: (NSString *) theKeyPath; - (NSData *) asCramMD5; - +- (NSData *) asPBKDF2SHA1UsingSalt: (NSData *) theSalt; - (NSData *) asCryptUsingSalt: (NSData *) theSalt; - (NSData *) asMD5CryptUsingSalt: (NSData *) theSalt; - (NSData *) asBlowfishCryptUsingSalt: (NSData *) theSalt; diff --git a/SoObjects/SOGo/NSData+Crypto.m b/SoObjects/SOGo/NSData+Crypto.m index b74f1c487..a3a985d37 100644 --- a/SoObjects/SOGo/NSData+Crypto.m +++ b/SoObjects/SOGo/NSData+Crypto.m @@ -52,6 +52,7 @@ #include "aes.h" #include "crypt_blowfish.h" #include "lmhash.h" +#include "pkcs5_pbkdf2.h" #import #import @@ -262,6 +263,10 @@ static const char salt_chars[] = { return [self asBlowfishCryptUsingSalt: theSalt]; } + else if ([passwordScheme caseInsensitiveCompare: @"pbkdf2"] == NSOrderedSame) + { + return [self asPBKDF2SHA1UsingSalt: theSalt]; + } else if ([[passwordScheme lowercaseString] hasPrefix: @"sym"]) { // We first support one sym cipher, AES-128-CBC. If something else is provided @@ -856,6 +861,60 @@ static const char salt_chars[] = return [NSData dataWithBytes: hashed_password length: strlen(hashed_password)]; } +- (NSData *) asPBKDF2SHA1UsingSalt: (NSData *) theSalt +{ + NSString *saltString; + unsigned char hashed_password[PBKDF2_KEY_SIZE_SHA1] = {0}; + int rounds = 0; + + if ([theSalt length] == 0) + { + // generate a salt with default complexity if none was provided + NSData* saltData = [NSData generateSaltForLength: PBKDF2_SALT_LEN withPrintable: YES]; + saltString = [[NSString alloc] initWithData: saltData encoding: NSUTF8StringEncoding]; + [saltString autorelease]; + } + else + { + NSString *saltAndRounds; + NSArray *saltAndRoundsComponents; + saltAndRounds = [[NSString alloc] initWithData: theSalt encoding: NSUTF8StringEncoding]; + // salt is expected to be of the form salt$rounds + saltAndRoundsComponents = [saltAndRounds componentsSeparatedByString: @"$"]; + AUTORELEASE(saltAndRounds); + + if ([saltAndRoundsComponents count] != 2) + { + return nil; + } + saltString = [saltAndRoundsComponents objectAtIndex: 0]; + + rounds = [[saltAndRoundsComponents objectAtIndex: 1] intValue]; + } + + if (rounds == 0) + rounds = PBKDF2_DEFAULT_ROUNDS; + + const char* password = [self bytes]; + const unsigned char* salt = (const unsigned char*)[saltString UTF8String]; +#if defined(HAVE_GNUTLS) + if (!check_gnutls_init()) + return nil; +#endif + if (pkcs5_pbkdf2(password, [self length], salt, PBKDF2_SALT_LEN, + hashed_password, PBKDF2_KEY_SIZE_SHA1, + rounds) != 0) + { + return nil; + } + + NSData *passwordData = + [NSData dataWithBytesNoCopy: hashed_password length: PBKDF2_KEY_SIZE_SHA1 freeWhenDone: NO]; + NSString *hexHash = [NSData encodeDataAsHexString: passwordData]; + + NSString* result = [NSString stringWithFormat: @"$1$%@$%u$%@", saltString, rounds, hexHash]; + return [result dataUsingEncoding:NSUTF8StringEncoding]; +} /** * Get the salt from a password encrypted with a specied scheme @@ -882,11 +941,13 @@ static const char salt_chars[] = } else if ([theScheme caseInsensitiveCompare: @"md5-crypt"] == NSOrderedSame || [theScheme caseInsensitiveCompare: @"sha256-crypt"] == NSOrderedSame || - [theScheme caseInsensitiveCompare: @"sha512-crypt"] == NSOrderedSame) + [theScheme caseInsensitiveCompare: @"sha512-crypt"] == NSOrderedSame || + [theScheme caseInsensitiveCompare: @"pbkdf2"] == NSOrderedSame) { // md5-crypt is generated the following "$1$$" // sha256-crypt is generated the following "$5$$" // sha512-crypt is generated the following "$6$$" + // pbkdf2 is generated as "$1$$$" NSString *cryptString; NSArray *cryptParts; diff --git a/SoObjects/SOGo/pkcs5_pbkdf2.c b/SoObjects/SOGo/pkcs5_pbkdf2.c new file mode 100644 index 000000000..22263e6f9 --- /dev/null +++ b/SoObjects/SOGo/pkcs5_pbkdf2.c @@ -0,0 +1,141 @@ +/* $OpenBSD: pkcs5_pbkdf2.c,v 1.11 2019/11/21 16:07:24 tedu Exp $ */ + +/*- + * Copyright (c) 2008 Damien Bergamini + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include + +#ifdef HAVE_GNUTLS +#include +#include +#define SHA_CTX gnutls_hash_hd_t +#define SHA1_Init(c) gnutls_hash_init(c, GNUTLS_DIG_SHA1) +#define SHA1_Update(c,b,l) gnutls_hash(*c, b, l) +#define SHA1_Final(b,c) gnutls_hash_deinit(*c, b); +#elif defined(HAVE_OPENSSL) +#include +#endif + +#define MINIMUM(a,b) (((a) < (b)) ? (a) : (b)) + +#define bcopy(b1,b2,len) (memmove((b2), (b1), (len)), (void) 0) + +#define SHA1_BLOCK_LENGTH 64 +#define SHA1_DIGEST_LENGTH 20 +#define SHA1_DIGEST_STRING_LENGTH (SHA1_DIGEST_LENGTH * 2 + 1) + +/* + * HMAC-SHA-1 (from RFC 2202). + */ +static void +hmac_sha1(const u_int8_t *text, size_t text_len, const u_int8_t *key, + size_t key_len, u_int8_t digest[SHA1_DIGEST_LENGTH]) +{ + SHA_CTX ctx; + u_int8_t k_pad[SHA1_BLOCK_LENGTH]; + u_int8_t tk[SHA1_DIGEST_LENGTH]; + int i; + + if (key_len > SHA1_BLOCK_LENGTH) { + SHA1_Init(&ctx); + SHA1_Update(&ctx, key, key_len); + SHA1_Final(tk, &ctx); + + key = tk; + key_len = SHA1_DIGEST_LENGTH; + } + + memset(k_pad, 0, sizeof k_pad); + bcopy(key, k_pad, key_len); + for (i = 0; i < SHA1_BLOCK_LENGTH; i++) + k_pad[i] ^= 0x36; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, k_pad, SHA1_BLOCK_LENGTH); + SHA1_Update(&ctx, text, text_len); + SHA1_Final(digest, &ctx); + + memset(k_pad, 0, sizeof k_pad); + bcopy(key, k_pad, key_len); + for (i = 0; i < SHA1_BLOCK_LENGTH; i++) + k_pad[i] ^= 0x5c; + + SHA1_Init(&ctx); + SHA1_Update(&ctx, k_pad, SHA1_BLOCK_LENGTH); + SHA1_Update(&ctx, digest, SHA1_DIGEST_LENGTH); + SHA1_Final(digest, &ctx); +} + +/* + * Password-Based Key Derivation Function 2 (PKCS #5 v2.0). + * Code based on IEEE Std 802.11-2007, Annex H.4.2. + */ +int +pkcs5_pbkdf2(const char *pass, size_t pass_len, const uint8_t *salt, + size_t salt_len, uint8_t *key, size_t key_len, unsigned int rounds) +{ + uint8_t *asalt, obuf[SHA1_DIGEST_LENGTH]; + uint8_t d1[SHA1_DIGEST_LENGTH], d2[SHA1_DIGEST_LENGTH]; + unsigned int i, j; + unsigned int count; + size_t r; + + if (rounds < 1 || key_len == 0) + goto bad; + if (salt_len == 0 || salt_len > SIZE_MAX - 4) + goto bad; + if ((asalt = malloc(salt_len + 4)) == NULL) + goto bad; + + memcpy(asalt, salt, salt_len); + + for (count = 1; key_len > 0; count++) { + asalt[salt_len + 0] = (count >> 24) & 0xff; + asalt[salt_len + 1] = (count >> 16) & 0xff; + asalt[salt_len + 2] = (count >> 8) & 0xff; + asalt[salt_len + 3] = count & 0xff; + hmac_sha1(asalt, salt_len + 4, (const u_int8_t *)pass, pass_len, d1); + memcpy(obuf, d1, sizeof(obuf)); + + for (i = 1; i < rounds; i++) { + hmac_sha1(d1, sizeof(d1), (const u_int8_t *)pass, pass_len, d2); + memcpy(d1, d2, sizeof(d1)); + for (j = 0; j < sizeof(obuf); j++) + obuf[j] ^= d1[j]; + } + + r = MINIMUM(key_len, SHA1_DIGEST_LENGTH); + memcpy(key, obuf, r); + key += r; + key_len -= r; + }; + explicit_bzero(asalt, salt_len + 4); + free(asalt); + explicit_bzero(d1, sizeof(d1)); + explicit_bzero(d2, sizeof(d2)); + explicit_bzero(obuf, sizeof(obuf)); + + return 0; + +bad: + /* overwrite with random in case caller doesn't check return code */ + //arc4random_buf(key, key_len); + return -1; +} diff --git a/SoObjects/SOGo/pkcs5_pbkdf2.h b/SoObjects/SOGo/pkcs5_pbkdf2.h new file mode 100644 index 000000000..cd28a02eb --- /dev/null +++ b/SoObjects/SOGo/pkcs5_pbkdf2.h @@ -0,0 +1,15 @@ +#ifndef PKCS5_PBKDF2_H +#define PKCS5_PBKDF2_H + +#include +#include + +#define PBKDF2_KEY_SIZE_SHA1 (20) +#define PBKDF2_SALT_LEN (16) +#define PBKDF2_DEFAULT_ROUNDS (5000) + +int +pkcs5_pbkdf2(const char *pass, size_t pass_len, const uint8_t *salt, + size_t salt_len, uint8_t *key, size_t key_len, unsigned int rounds); + +#endif /* ! PKCS5_PBKDF2_H */ \ No newline at end of file diff --git a/Tests/Unit/TestNSString+Crypto.m b/Tests/Unit/TestNSString+Crypto.m index 37d5e4162..0456c3435 100644 --- a/Tests/Unit/TestNSString+Crypto.m +++ b/Tests/Unit/TestNSString+Crypto.m @@ -1,6 +1,7 @@ -/* TestNSString+MD5SHA1.m - this file is part of SOGo +/* TestNSString+Crypto.m - this file is part of SOGo * * Copyright (C) 2011, 2012 Jeroen Dekkers + * Copyright (C) 2020 Nicolas Höft * * Author: Jeroen Dekkers * @@ -85,4 +86,30 @@ test([blf_key isEqualToCrypted:blf_result withDefaultScheme: @"BLF-CRYPT" keyPath: nil]); } +- (void) test_pbkdf2 +{ + NSString *error; + // well-known comparison + NSString *pbkdf2_key = @"123456"; + NSString *pbkdf2_hash = @"{PBKDF2}$1$xbhnwhLxltdS9L5M$5001$f1699047a6132383490817d6e58a5284f13339f0"; + NSString *pkbf2_prefix; + NSString *pkbf2_result; + + error = [NSString stringWithFormat: + @"string '%@' wrong PBKDF2: '%@'", + pbkdf2_key, pbkdf2_hash]; + testWithMessage([pbkdf2_key isEqualToCrypted:pbkdf2_hash withDefaultScheme: @"CRYPT" keyPath: nil], error); + + // generate a new pbkdf2-crypt key + pkbf2_prefix = @"$1$"; + pkbf2_result = [pbkdf2_key asCryptedPassUsingScheme: @"PBKDF2" keyPath: nil]; + + error = [NSString stringWithFormat: + @"returned hash '%@' has incorrect PBKDF2 prefix: '%@'", + pkbf2_result, pkbf2_prefix]; + + testWithMessage([pkbf2_result hasPrefix: pkbf2_prefix], error); + test([pbkdf2_key isEqualToCrypted:pkbf2_result withDefaultScheme: @"PBKDF2" keyPath: nil]); +} + @end