mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
One standard password-hash format everywhere: mux_sha_crypt implements the sha-crypt construction (glibc/musl/openssl-compatible, including rounds= presence, clamping-with-clamped-value-in-output, and 16-char salt truncation) over OS crypto primitives -- OpenSSL EVP on Unix, Windows CNG with a reusable hash handle. mux_crypt routes $5$/$6$ through it on BOTH platforms, so a Unix-written password database verifies on Windows and vice versa, and Unix stops depending on which libc crypt(3) understands those formats (macOS's does not). The libc fall-through remains only for the legacy tail (DES, _-extended, $1$). New hashes are $6$ with an explicit rounds= from the new conf directive password_hash_rounds (default 220000, tracking current OWASP guidance for the SHA-512 PRF; measured ~190 ms/hash on 2022-era server hardware). The check_pass auto-upgrade trigger becomes parameter-aware: a $5$/$6$ hash whose stored rounds differ from policy re-encodes on the next successful login (work-factor migration, automatic), P6H/legacy conversion-on-login is preserved, and the implicit default (password_methods unset) no longer rewrites SHA1-or-stronger hashes -- it used to re-hash every login and would silently downgrade $6$ to $SHA1$ after a config reset. tests/shacrypt (make test-shacrypt, also in test-asan) pins 14 KATs with every golden value from `openssl passwd` as an external oracle, including both published spec vectors. Verified on Windows: 14/14 KATs, digest KATs still 17/17, smoke ALL 1601 PASSED / 0 failed, and a live netmux end-to-end: @pcreate under sha512 policy stores $6$, a $SHA1$ player auto-upgrades on login, a rounds=5000 hash refreshes to 220000 on login, and every Windows/CNG-generated hash re-derives byte-identically with `openssl passwd -6`. Stacked on #1963's CNG backend (same OS-primitives posture and bcrypt.lib link). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
445 lines
12 KiB
C++
445 lines
12 KiB
C++
/*! \file shacrypt.cpp
|
|
* \brief Portable sha-crypt ($5$/$6$) over OS crypto primitives.
|
|
*
|
|
* Implements the standard sha-crypt construction (Ulrich Drepper's
|
|
* SHA-256/SHA-512 crypt, as shipped by glibc/musl and produced by
|
|
* `openssl passwd -5/-6`) so both platforms generate and verify the same
|
|
* modular-crypt password format (#1962). The SHA-256/512 compression
|
|
* itself comes from the OS — OpenSSL EVP under UNIX_DIGEST, Windows CNG
|
|
* otherwise — per the #1963 posture; what ships here is the construction
|
|
* that orchestrates those primitives, in the same category as HMAC or the
|
|
* RFC 6455 concatenation.
|
|
*
|
|
* Using our own construction on Unix too (rather than libc crypt(3)) makes
|
|
* behavior identical on every box: macOS crypt() lacks $5$/$6$ entirely,
|
|
* and rounds= handling stops depending on whichever libc is present.
|
|
*
|
|
* Output is byte-compatible with glibc: rounds= presence and clamped value
|
|
* are preserved from the setting, salt is truncated to 16, and the
|
|
* character permutation/base64 match — pinned against `openssl passwd`
|
|
* oracle vectors in tests/shacrypt.
|
|
*/
|
|
|
|
#include "copyright.h"
|
|
#include "autoconf.h"
|
|
#include "config.h"
|
|
#include "core.h"
|
|
#include "shacrypt.h"
|
|
|
|
#include <climits>
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Incremental digest over the OS primitive: OpenSSL EVP or Windows CNG.
|
|
// The rounds loop runs password_hash_rounds digests per call, so contexts
|
|
// are created once per mux_sha_crypt() call and reused per round.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#ifdef UNIX_DIGEST
|
|
|
|
#include <openssl/evp.h>
|
|
|
|
typedef struct
|
|
{
|
|
EVP_MD_CTX *ctx;
|
|
const EVP_MD *md;
|
|
unsigned int digest_len;
|
|
} shc_hash;
|
|
|
|
static bool shc_open(shc_hash *h, bool bSha512)
|
|
{
|
|
#if HAVE_EVP_MD_CTX_NEW
|
|
h->ctx = EVP_MD_CTX_new();
|
|
#else
|
|
h->ctx = EVP_MD_CTX_create();
|
|
#endif
|
|
h->md = bSha512 ? EVP_sha512() : EVP_sha256();
|
|
h->digest_len = bSha512 ? 64 : 32;
|
|
return nullptr != h->ctx;
|
|
}
|
|
|
|
static bool shc_begin(shc_hash *h)
|
|
{
|
|
return 0 != EVP_DigestInit_ex(h->ctx, h->md, nullptr);
|
|
}
|
|
|
|
static bool shc_update(shc_hash *h, const uint8_t *p, size_t n)
|
|
{
|
|
return 0 != EVP_DigestUpdate(h->ctx, p, n);
|
|
}
|
|
|
|
static bool shc_final(shc_hash *h, uint8_t *out)
|
|
{
|
|
unsigned int len = 0;
|
|
return 0 != EVP_DigestFinal_ex(h->ctx, out, &len);
|
|
}
|
|
|
|
static void shc_close(shc_hash *h)
|
|
{
|
|
#if HAVE_EVP_MD_CTX_NEW
|
|
EVP_MD_CTX_free(h->ctx);
|
|
#else
|
|
EVP_MD_CTX_destroy(h->ctx);
|
|
#endif
|
|
h->ctx = nullptr;
|
|
}
|
|
|
|
#elif defined(WIN32)
|
|
|
|
#include <bcrypt.h>
|
|
#include <mutex>
|
|
|
|
#ifndef NT_SUCCESS
|
|
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
|
|
#endif
|
|
|
|
typedef struct
|
|
{
|
|
BCRYPT_HASH_HANDLE hash;
|
|
unsigned int digest_len;
|
|
} shc_hash;
|
|
|
|
static BCRYPT_ALG_HANDLE shc_provider(bool bSha512)
|
|
{
|
|
// Opened once per algorithm and cached for the life of the process,
|
|
// same rationale as sha1.cpp's provider table.
|
|
static BCRYPT_ALG_HANDLE hSha256 = nullptr;
|
|
static BCRYPT_ALG_HANDLE hSha512 = nullptr;
|
|
static std::once_flag once256, once512;
|
|
|
|
if (bSha512)
|
|
{
|
|
std::call_once(once512, []
|
|
{
|
|
BCRYPT_ALG_HANDLE h = nullptr;
|
|
if (NT_SUCCESS(BCryptOpenAlgorithmProvider(&h,
|
|
BCRYPT_SHA512_ALGORITHM, nullptr, BCRYPT_HASH_REUSABLE_FLAG)))
|
|
{
|
|
hSha512 = h;
|
|
}
|
|
});
|
|
return hSha512;
|
|
}
|
|
std::call_once(once256, []
|
|
{
|
|
BCRYPT_ALG_HANDLE h = nullptr;
|
|
if (NT_SUCCESS(BCryptOpenAlgorithmProvider(&h,
|
|
BCRYPT_SHA256_ALGORITHM, nullptr, BCRYPT_HASH_REUSABLE_FLAG)))
|
|
{
|
|
hSha256 = h;
|
|
}
|
|
});
|
|
return hSha256;
|
|
}
|
|
|
|
static bool shc_open(shc_hash *h, bool bSha512)
|
|
{
|
|
BCRYPT_ALG_HANDLE alg = shc_provider(bSha512);
|
|
if (nullptr == alg)
|
|
{
|
|
return false;
|
|
}
|
|
h->digest_len = bSha512 ? 64 : 32;
|
|
// A reusable hash handle resets itself on BCryptFinishHash, so one
|
|
// handle serves every begin/update/final cycle of this call.
|
|
return NT_SUCCESS(BCryptCreateHash(alg, &h->hash, nullptr, 0, nullptr, 0,
|
|
BCRYPT_HASH_REUSABLE_FLAG));
|
|
}
|
|
|
|
static bool shc_begin(shc_hash *h)
|
|
{
|
|
// Reusable handles are ready after open and after each finish.
|
|
return nullptr != h->hash;
|
|
}
|
|
|
|
static bool shc_update(shc_hash *h, const uint8_t *p, size_t n)
|
|
{
|
|
return NT_SUCCESS(BCryptHashData(h->hash,
|
|
const_cast<PUCHAR>(p), static_cast<ULONG>(n), 0));
|
|
}
|
|
|
|
static bool shc_final(shc_hash *h, uint8_t *out)
|
|
{
|
|
return NT_SUCCESS(BCryptFinishHash(h->hash, out, h->digest_len, 0));
|
|
}
|
|
|
|
static void shc_close(shc_hash *h)
|
|
{
|
|
if (h->hash)
|
|
{
|
|
BCryptDestroyHash(h->hash);
|
|
h->hash = nullptr;
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
#error "No digest backend: need OpenSSL (UNIX_DIGEST) or Windows CNG (#1963)."
|
|
|
|
#endif
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// sha-crypt proper.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define SHC_ROUNDS_DEFAULT 5000UL
|
|
#define SHC_ROUNDS_MIN 1000UL
|
|
#define SHC_ROUNDS_MAX 999999999UL
|
|
#define SHC_SALT_MAX 16
|
|
|
|
// crypt-style base64 alphabet (distinct from MIME base64).
|
|
static const char shc_itoa64[] =
|
|
"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
|
|
static void shc_b64_from_24bit(UTF8 **pp, uint8_t b2, uint8_t b1, uint8_t b0, int n)
|
|
{
|
|
unsigned int w = (static_cast<unsigned int>(b2) << 16)
|
|
| (static_cast<unsigned int>(b1) << 8)
|
|
| static_cast<unsigned int>(b0);
|
|
while (n-- > 0)
|
|
{
|
|
*(*pp)++ = static_cast<UTF8>(shc_itoa64[w & 0x3F]);
|
|
w >>= 6;
|
|
}
|
|
}
|
|
|
|
static void shc_wipe(void *p, size_t n)
|
|
{
|
|
volatile uint8_t *vp = static_cast<volatile uint8_t *>(p);
|
|
while (n--)
|
|
{
|
|
*vp++ = 0;
|
|
}
|
|
}
|
|
|
|
const UTF8 *mux_sha_crypt(const UTF8 *szPassword, const UTF8 *szSetting)
|
|
{
|
|
// ---- parse the setting: $5$|$6$ [rounds=N$] salt[$...] ----
|
|
bool bSha512;
|
|
if ('$' == szSetting[0] && '$' == szSetting[2] && '5' == szSetting[1])
|
|
{
|
|
bSha512 = false;
|
|
}
|
|
else if ('$' == szSetting[0] && '$' == szSetting[2] && '6' == szSetting[1])
|
|
{
|
|
bSha512 = true;
|
|
}
|
|
else
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
const UTF8 *p = szSetting + 3;
|
|
unsigned long rounds = SHC_ROUNDS_DEFAULT;
|
|
bool bRoundsCustom = false;
|
|
if (0 == strncmp(reinterpret_cast<const char *>(p), "rounds=", 7))
|
|
{
|
|
char *endp = nullptr;
|
|
unsigned long r = strtoul(reinterpret_cast<const char *>(p) + 7, &endp, 10);
|
|
if (endp && '$' == *endp)
|
|
{
|
|
// glibc clamps and then reports the clamped value in the output.
|
|
if (r < SHC_ROUNDS_MIN)
|
|
{
|
|
r = SHC_ROUNDS_MIN;
|
|
}
|
|
if (SHC_ROUNDS_MAX < r)
|
|
{
|
|
r = SHC_ROUNDS_MAX;
|
|
}
|
|
rounds = r;
|
|
bRoundsCustom = true;
|
|
p = reinterpret_cast<const UTF8 *>(endp) + 1;
|
|
}
|
|
}
|
|
|
|
const UTF8 *salt = p;
|
|
size_t salt_len = 0;
|
|
while ( salt[salt_len]
|
|
&& '$' != salt[salt_len]
|
|
&& salt_len < SHC_SALT_MAX)
|
|
{
|
|
salt_len++;
|
|
}
|
|
|
|
const size_t key_len = strlen(reinterpret_cast<const char *>(szPassword));
|
|
const uint8_t *key = reinterpret_cast<const uint8_t *>(szPassword);
|
|
const uint8_t *slt = reinterpret_cast<const uint8_t *>(salt);
|
|
|
|
shc_hash H;
|
|
if (!shc_open(&H, bSha512))
|
|
{
|
|
return nullptr;
|
|
}
|
|
const size_t L = H.digest_len;
|
|
|
|
uint8_t alt_result[64];
|
|
uint8_t temp_result[64];
|
|
bool ok = true;
|
|
|
|
// Digest B = H(password || salt || password).
|
|
ok = ok && shc_begin(&H)
|
|
&& shc_update(&H, key, key_len)
|
|
&& shc_update(&H, slt, salt_len)
|
|
&& shc_update(&H, key, key_len)
|
|
&& shc_final(&H, alt_result);
|
|
|
|
// Digest A = H(password || salt || B-blocks per key_len || bit-walk).
|
|
ok = ok && shc_begin(&H)
|
|
&& shc_update(&H, key, key_len)
|
|
&& shc_update(&H, slt, salt_len);
|
|
if (ok)
|
|
{
|
|
size_t cnt;
|
|
for (cnt = key_len; ok && cnt > L; cnt -= L)
|
|
{
|
|
ok = shc_update(&H, alt_result, L);
|
|
}
|
|
ok = ok && shc_update(&H, alt_result, cnt);
|
|
for (cnt = key_len; ok && cnt > 0; cnt >>= 1)
|
|
{
|
|
ok = (cnt & 1) ? shc_update(&H, alt_result, L)
|
|
: shc_update(&H, key, key_len);
|
|
}
|
|
}
|
|
ok = ok && shc_final(&H, alt_result);
|
|
|
|
// P sequence: H(password repeated key_len times), tiled to key_len bytes.
|
|
ok = ok && shc_begin(&H);
|
|
for (size_t i = 0; ok && i < key_len; i++)
|
|
{
|
|
ok = shc_update(&H, key, key_len);
|
|
}
|
|
ok = ok && shc_final(&H, temp_result);
|
|
|
|
// p_bytes / s_bytes are small (key and salt sized); the salt is <= 16
|
|
// and passwords are LBUF-bounded, but allocate exactly what is needed.
|
|
uint8_t *p_bytes = nullptr;
|
|
uint8_t *s_bytes = nullptr;
|
|
if (ok)
|
|
{
|
|
p_bytes = new uint8_t[key_len ? key_len : 1];
|
|
size_t cnt;
|
|
uint8_t *cp = p_bytes;
|
|
for (cnt = key_len; cnt >= L; cnt -= L)
|
|
{
|
|
memcpy(cp, temp_result, L);
|
|
cp += L;
|
|
}
|
|
memcpy(cp, temp_result, cnt);
|
|
}
|
|
|
|
// S sequence: H(salt repeated 16 + alt_result[0] times), tiled to salt_len.
|
|
ok = ok && shc_begin(&H);
|
|
if (ok)
|
|
{
|
|
const size_t reps = 16u + alt_result[0];
|
|
for (size_t i = 0; ok && i < reps; i++)
|
|
{
|
|
ok = shc_update(&H, slt, salt_len);
|
|
}
|
|
}
|
|
ok = ok && shc_final(&H, temp_result);
|
|
if (ok)
|
|
{
|
|
s_bytes = new uint8_t[salt_len ? salt_len : 1];
|
|
size_t cnt;
|
|
uint8_t *cp = s_bytes;
|
|
for (cnt = salt_len; cnt >= L; cnt -= L)
|
|
{
|
|
memcpy(cp, temp_result, L);
|
|
cp += L;
|
|
}
|
|
memcpy(cp, temp_result, cnt);
|
|
}
|
|
|
|
// The rounds loop: repeatedly remix password and salt material.
|
|
for (unsigned long r = 0; ok && r < rounds; r++)
|
|
{
|
|
ok = shc_begin(&H);
|
|
ok = ok && ((r & 1) ? shc_update(&H, p_bytes, key_len)
|
|
: shc_update(&H, alt_result, L));
|
|
if (ok && 0 != (r % 3))
|
|
{
|
|
ok = shc_update(&H, s_bytes, salt_len);
|
|
}
|
|
if (ok && 0 != (r % 7))
|
|
{
|
|
ok = shc_update(&H, p_bytes, key_len);
|
|
}
|
|
ok = ok && ((r & 1) ? shc_update(&H, alt_result, L)
|
|
: shc_update(&H, p_bytes, key_len));
|
|
ok = ok && shc_final(&H, alt_result);
|
|
}
|
|
|
|
shc_close(&H);
|
|
if (p_bytes)
|
|
{
|
|
shc_wipe(p_bytes, key_len ? key_len : 1);
|
|
delete [] p_bytes;
|
|
}
|
|
if (s_bytes)
|
|
{
|
|
shc_wipe(s_bytes, salt_len ? salt_len : 1);
|
|
delete [] s_bytes;
|
|
}
|
|
shc_wipe(temp_result, sizeof(temp_result));
|
|
if (!ok)
|
|
{
|
|
shc_wipe(alt_result, sizeof(alt_result));
|
|
return nullptr;
|
|
}
|
|
|
|
// ---- emit $<id>$[rounds=N$]<salt>$<base64 permutation> ----
|
|
//
|
|
// "$6$rounds=999999999$" (20) + 16 salt + '$' + 86 hash + NUL = 124.
|
|
thread_local UTF8 buf[128];
|
|
UTF8 *q = buf;
|
|
*q++ = '$';
|
|
*q++ = bSha512 ? '6' : '5';
|
|
*q++ = '$';
|
|
if (bRoundsCustom)
|
|
{
|
|
q += mux_snprintf(q, 24, T("rounds=%lu$"), rounds);
|
|
}
|
|
memcpy(q, salt, salt_len);
|
|
q += salt_len;
|
|
*q++ = '$';
|
|
|
|
if (bSha512)
|
|
{
|
|
static const uint8_t ord[21][3] =
|
|
{
|
|
{ 0,21,42},{22,43, 1},{44, 2,23},{ 3,24,45},{25,46, 4},
|
|
{47, 5,26},{ 6,27,48},{28,49, 7},{50, 8,29},{ 9,30,51},
|
|
{31,52,10},{53,11,32},{12,33,54},{34,55,13},{56,14,35},
|
|
{15,36,57},{37,58,16},{59,17,38},{18,39,60},{40,61,19},
|
|
{62,20,41},
|
|
};
|
|
for (int i = 0; i < 21; i++)
|
|
{
|
|
shc_b64_from_24bit(&q, alt_result[ord[i][0]],
|
|
alt_result[ord[i][1]],
|
|
alt_result[ord[i][2]], 4);
|
|
}
|
|
shc_b64_from_24bit(&q, 0, 0, alt_result[63], 2);
|
|
}
|
|
else
|
|
{
|
|
static const uint8_t ord[10][3] =
|
|
{
|
|
{ 0,10,20},{21, 1,11},{12,22, 2},{ 3,13,23},{24, 4,14},
|
|
{15,25, 5},{ 6,16,26},{27, 7,17},{18,28, 8},{ 9,19,29},
|
|
};
|
|
for (int i = 0; i < 10; i++)
|
|
{
|
|
shc_b64_from_24bit(&q, alt_result[ord[i][0]],
|
|
alt_result[ord[i][1]],
|
|
alt_result[ord[i][2]], 4);
|
|
}
|
|
shc_b64_from_24bit(&q, 0, alt_result[31], alt_result[30], 3);
|
|
}
|
|
*q = '\0';
|
|
|
|
shc_wipe(alt_result, sizeof(alt_result));
|
|
return buf;
|
|
}
|