diff --git a/src/Core/Utilities/DuoWeb.cs b/src/Core/Utilities/DuoWeb.cs new file mode 100644 index 0000000000..e374e76490 --- /dev/null +++ b/src/Core/Utilities/DuoWeb.cs @@ -0,0 +1,242 @@ +/* +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); + + /// + /// 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. + /// + /// Duo integration key + /// Duo secret key + /// Application secret key + /// Primary-authenticated username + /// (optional) The current UTC time + /// signed request + 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}"; + } + + /// + /// Validate the signed response returned from Duo. + /// Returns the username of the authenticated user, or null. + /// + /// Duo integration key + /// Duo secret key + /// Application secret key + /// The signed response POST'ed to the server + /// (optional) The current UTC time + /// authenticated username, or null + 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); + } + } +}