mirror of
https://github.com/bitwarden/server.git
synced 2025-01-05 19:17:36 +01:00
243 lines
8.5 KiB
C#
243 lines
8.5 KiB
C#
/*
|
|
Original source modified from https://github.com/duosecurity/duo_dotnet
|
|
|
|
=============================================================================
|
|
=============================================================================
|
|
|
|
ref: https://github.com/duosecurity/duo_dotnet/blob/master/LICENSE
|
|
|
|
Copyright (c) 2011, Duo Security, Inc.
|
|
All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
modification, are permitted provided that the following conditions
|
|
are met:
|
|
|
|
1. Redistributions of source code must retain the above copyright
|
|
notice, this list of conditions and the following disclaimer.
|
|
2. Redistributions in binary form must reproduce the above copyright
|
|
notice, this list of conditions and the following disclaimer in the
|
|
documentation and/or other materials provided with the distribution.
|
|
3. The name of the author may not be used to endorse or promote products
|
|
derived from this software without specific prior written permission.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
|
|
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
|
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
|
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
using System;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace Bit.Core.Utilities.Duo
|
|
{
|
|
public static class DuoWeb
|
|
{
|
|
private const string DuoProfix = "TX";
|
|
private const string AppPrefix = "APP";
|
|
private const string AuthPrefix = "AUTH";
|
|
private const int DuoExpire = 300;
|
|
private const int AppExpire = 3600;
|
|
private const int IKeyLength = 20;
|
|
private const int SKeyLength = 40;
|
|
private const int AKeyLength = 40;
|
|
|
|
public static string ErrorUser = "ERR|The username passed to sign_request() is invalid.";
|
|
public static string ErrorIKey = "ERR|The Duo integration key passed to sign_request() is invalid.";
|
|
public static string ErrorSKey = "ERR|The Duo secret key passed to sign_request() is invalid.";
|
|
public static string ErrorAKey = "ERR|The application secret key passed to sign_request() must be at least " +
|
|
"40 characters.";
|
|
public static string ErrorUnknown = "ERR|An unknown error has occurred.";
|
|
|
|
// throw on invalid bytes
|
|
private static Encoding _encoding = new UTF8Encoding(false, true);
|
|
private static DateTime _epoc = new DateTime(1970, 1, 1);
|
|
|
|
/// <summary>
|
|
/// Generate a signed request for Duo authentication.
|
|
/// The returned value should be passed into the Duo.init() call
|
|
/// in the rendered web page used for Duo authentication.
|
|
/// </summary>
|
|
/// <param name="ikey">Duo integration key</param>
|
|
/// <param name="skey">Duo secret key</param>
|
|
/// <param name="akey">Application secret key</param>
|
|
/// <param name="username">Primary-authenticated username</param>
|
|
/// <param name="currentTime">(optional) The current UTC time</param>
|
|
/// <returns>signed request</returns>
|
|
public static string SignRequest(string ikey, string skey, string akey, string username,
|
|
DateTime? currentTime = null)
|
|
{
|
|
string duoSig;
|
|
string appSig;
|
|
|
|
var currentTimeValue = currentTime ?? DateTime.UtcNow;
|
|
|
|
if (username == string.Empty)
|
|
{
|
|
return ErrorUser;
|
|
}
|
|
if (username.Contains("|"))
|
|
{
|
|
return ErrorUser;
|
|
}
|
|
if (ikey.Length != IKeyLength)
|
|
{
|
|
return ErrorIKey;
|
|
}
|
|
if (skey.Length != SKeyLength)
|
|
{
|
|
return ErrorSKey;
|
|
}
|
|
if (akey.Length < AKeyLength)
|
|
{
|
|
return ErrorAKey;
|
|
}
|
|
|
|
try
|
|
{
|
|
duoSig = SignVals(skey, username, ikey, DuoProfix, DuoExpire, currentTimeValue);
|
|
appSig = SignVals(akey, username, ikey, AppPrefix, AppExpire, currentTimeValue);
|
|
}
|
|
catch
|
|
{
|
|
return ErrorUnknown;
|
|
}
|
|
|
|
return $"{duoSig}:{appSig}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate the signed response returned from Duo.
|
|
/// Returns the username of the authenticated user, or null.
|
|
/// </summary>
|
|
/// <param name="ikey">Duo integration key</param>
|
|
/// <param name="skey">Duo secret key</param>
|
|
/// <param name="akey">Application secret key</param>
|
|
/// <param name="sigResponse">The signed response POST'ed to the server</param>
|
|
/// <param name="currentTime">(optional) The current UTC time</param>
|
|
/// <returns>authenticated username, or null</returns>
|
|
public static string VerifyResponse(string ikey, string skey, string akey, string sigResponse,
|
|
DateTime? currentTime = null)
|
|
{
|
|
string authUser = null;
|
|
string appUser = null;
|
|
var currentTimeValue = currentTime ?? DateTime.UtcNow;
|
|
|
|
try
|
|
{
|
|
var sigs = sigResponse.Split(':');
|
|
var authSig = sigs[0];
|
|
var appSig = sigs[1];
|
|
|
|
authUser = ParseVals(skey, authSig, AuthPrefix, ikey, currentTimeValue);
|
|
appUser = ParseVals(akey, appSig, AppPrefix, ikey, currentTimeValue);
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (authUser != appUser)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return authUser;
|
|
}
|
|
|
|
private static string SignVals(string key, string username, string ikey, string prefix, long expire,
|
|
DateTime currentTime)
|
|
{
|
|
var ts = (long)(currentTime - _epoc).TotalSeconds;
|
|
expire = ts + expire;
|
|
var val = $"{username}|{ikey}|{expire.ToString()}";
|
|
var cookie = $"{prefix}|{Encode64(val)}";
|
|
var sig = Sign(key, cookie);
|
|
return $"{cookie}|{sig}";
|
|
}
|
|
|
|
private static string ParseVals(string key, string val, string prefix, string ikey, DateTime currentTime)
|
|
{
|
|
var ts = (long)(currentTime - _epoc).TotalSeconds;
|
|
|
|
var parts = val.Split('|');
|
|
if (parts.Length != 3)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var uPrefix = parts[0];
|
|
var uB64 = parts[1];
|
|
var uSig = parts[2];
|
|
|
|
var sig = Sign(key, $"{uPrefix}|{uB64}");
|
|
if (Sign(key, sig) != Sign(key, uSig))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (uPrefix != prefix)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var cookie = Decode64(uB64);
|
|
var cookieParts = cookie.Split('|');
|
|
if (cookieParts.Length != 3)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var username = cookieParts[0];
|
|
var uIKey = cookieParts[1];
|
|
var expire = cookieParts[2];
|
|
|
|
if (uIKey != ikey)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var expireTs = Convert.ToInt32(expire);
|
|
if (ts >= expireTs)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return username;
|
|
}
|
|
|
|
private static string Sign(string skey, string data)
|
|
{
|
|
var keyBytes = Encoding.ASCII.GetBytes(skey);
|
|
var dataBytes = Encoding.ASCII.GetBytes(data);
|
|
|
|
using (var hmac = new HMACSHA1(keyBytes))
|
|
{
|
|
var hash = hmac.ComputeHash(dataBytes);
|
|
var hex = BitConverter.ToString(hash);
|
|
return hex.Replace("-", "").ToLower();
|
|
}
|
|
}
|
|
|
|
private static string Encode64(string plaintext)
|
|
{
|
|
var plaintextBytes = _encoding.GetBytes(plaintext);
|
|
return Convert.ToBase64String(plaintextBytes);
|
|
}
|
|
|
|
private static string Decode64(string encoded)
|
|
{
|
|
var plaintextBytes = Convert.FromBase64String(encoded);
|
|
return _encoding.GetString(plaintextBytes);
|
|
}
|
|
}
|
|
}
|