Imports System.Data Imports System.Web.Security Imports System.Security.Cryptography.X509Certificates Imports System.Xml Imports System.Collections Imports System.Collections.Generic Imports System.Security.Cryptography.Xml Imports System.Text Imports System.IO Imports ComponentSpace.SAML2 Imports ComponentSpace.SAML2.Assertions Imports ComponentSpace.SAML2.Protocols Imports ComponentSpace.SAML2.Bindings Imports ComponentSpace.SAML2.Profiles.ArtifactResolution Imports ComponentSpace.SAML2.Profiles.SSOBrowser Imports ComponentSpace.SAML2.Metadata 'Imports ComponentSpace.SAML2.Utility Partial Class SAML2 Inherits ArchiGenObj.PBasePage Private CC As CBroker Private SessionTimeOutPage As String = "Default.aspx" Private _CurrentX509Key As X509Certificate2 Protected Sub Page_BeforePreInit() Handles Me.BeforePreInit CC = New CBroker(AA) _CurrentX509Key = CC.Security.GetCertifyX509("162BD43B4EFA82D911B2B357A68958C2958C96AA") 'old key was C73104F8768F755FEA05610830F6823A444BC57F End Sub Protected Sub Page_Unload1(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Unload If IsNothing(CC) = False Then CC.Dispose() End Sub Public Overrides Function SecurePage() As Boolean Return False End Function Protected Sub Page_PrintForm() Handles Me.PrintForm Dim LogData As String = "" Dim strSQL As String = "" Dim Usr_Email As String = "" Dim ClientIP As String = "" Dim APIPartnerID As String = "" Dim boolLogUserIn As Boolean = False Dim SAMLDateTime As DateTime = Nothing 'Parse the incoming token - the format should be something like this: Dim token() As String = {""} If Len(Request.Form("SAMLResponse")) > 0 Then 'we need to receive the saml response and create a de-serialized saml repsonse object Dim samlResponseXml As XmlElement = Nothing Dim relayState As String = Nothing Try ServiceProvider.ReceiveSAMLResponseByHTTPPost(New System.Web.HttpRequestWrapper(Request), samlResponseXml, relayState) Catch ex As Exception FailLogin("Malformed SAML Assertion", ex.Message & ex.StackTrace, "The single sign on request was in an invalid format.") : Exit Sub End Try Dim samlResponse As New SAMLResponse(samlResponseXml) Dim samlAssertion As SAMLAssertion = Nothing Dim samlSignedAssertion As XmlElement Dim samlEncAssertion As ComponentSpace.SAML2.Assertions.EncryptedAssertion = Nothing Dim slOldKeys As SortedList = GetOldKeys() Dim strKey As String Dim booKeySuccess As Boolean = False Dim booUsedOldKey As Boolean = False Dim strOldKey As String = "" If samlResponse.GetAssertions.Count > 0 Then samlAssertion = samlResponse.GetAssertions(0) booKeySuccess = True ElseIf samlResponse.GetSignedAssertions.Count > 0 Then samlSignedAssertion = samlResponse.GetSignedAssertions(0) samlAssertion = New SAMLAssertion(samlSignedAssertion) booKeySuccess = True ElseIf samlResponse.GetEncryptedAssertions.Count > 0 Then samlEncAssertion = samlResponse.GetEncryptedAssertions(0) Try 'first try our current key samlAssertion = samlEncAssertion.Decrypt(_CurrentX509Key, Nothing, Nothing) booKeySuccess = True Catch ex As Exception 'loop through all our older keys For Each deKey As DictionaryEntry In slOldKeys strKey = CStr(deKey.Value) Try 'try an older key _CurrentX509Key = CC.Security.GetCertifyX509(strKey) 'CC.Security.GetCertifyX509("4F8D041DB3F84B5ADE499AE6E0971914E93291C7") samlAssertion = samlEncAssertion.Decrypt(_CurrentX509Key, Nothing, Nothing) booKeySuccess = True booUsedOldKey = True strOldKey = strKey SaveError("Attempted use of old x509 cert: " & strKey, Nothing) Exit For Catch ex2 As Exception 'we don't want to fail right now because we might have other keys to try End Try Next End Try Else FailLogin("SAML Response contained no assertion", "SAML Response contained no assertion", "The single sign on request did not contain a SAML assertion.") : Exit Sub End If 'we tried all our keys and none of them worked :( If booKeySuccess = False Then FailLogin("Could not decrypt SAML Assertions", "Tried all keys and could not decrypt SAML Assertions", "The single sign on request was not encrypted with a PGP key from Certify.") : Exit Sub End If Dim strCustomerX509 As String = Nothing If IsNothing(samlAssertion.ToXml.SelectSingleNode("//*[local-name() = 'X509Certificate']")) = False Then strCustomerX509 = samlAssertion.ToXml.SelectSingleNode("//*[local-name() = 'X509Certificate']").InnerText.Replace(" ", "") ElseIf IsNothing(samlResponseXml.SelectSingleNode("//*[local-name() = 'X509Certificate']")) = False Then strCustomerX509 = samlResponseXml.SelectSingleNode("//*[local-name() = 'X509Certificate']").InnerText.Replace(" ", "") Else FailLogin("No X509Certificate found in the assertion or response", "No X509Certificate found in the assertion or response", "The single sign on request did not contain a X509 certificate.") : Exit Sub End If Dim cerCustomerX509 As New X509Certificate2(Convert.FromBase64String(strCustomerX509)) 'now we can pick out the stuff we're after If IsNothing(samlAssertion.Subject.NameID) = False Then Usr_Email = samlAssertion.Subject.NameID.NameIdentifier ElseIf IsNothing(samlAssertion.GetAttributeValue("E-Mail")) = False Then Usr_Email = samlAssertion.GetAttributeValue("E-Mail") ' our ADFS documentation says to use 'E-Mail-Address' ElseIf IsNothing(samlAssertion.GetAttributeValue("E-Mail-Address")) = False Then Usr_Email = samlAssertion.GetAttributeValue("E-Mail-Address") End If If Usr_Email.Contains("#EXT#") = True Then 'AzureAD does this, so we should go get it from what comes over by default If IsNothing(samlAssertion.GetAttributes("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress")) = False Then Usr_Email = samlAssertion.GetAttributeValue("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress") End If End If SAMLDateTime = samlAssertion.IssueInstant If Usr_Email.Length = 0 Then FailLogin("No email address", samlAssertion.ToString, "The single sign on request did not contain an email address.") : Exit Sub End If 'if we used an old key log it here because we have the user at this point If booUsedOldKey = True Then AA.Log.Write(ArchiGenObj.CLog.LogLevel.LogLevelInfo, "SAML2.aspx", "Using an older key " & strOldKey & " EMAIL:" & Usr_Email & " REALIP:" & AA.Request.UserHostAddress) End If LogData = "X509Thumbprint:" & cerCustomerX509.Thumbprint & " EMAIL:" & Usr_Email & " CLIENT_IP:" & ClientIP & " DateTimeStamp:" & SAMLDateTime & " CLIENTIP:" & ClientIP & " REALIP:" & AA.Request.UserHostAddress If Usr_Email.Contains("'") Then 'apparently it's ok to have apostrophes in email addresses, but we need a legit character here! Usr_Email = AA.Server.HtmlDecode(Usr_Email) End If 'Check to make sure the Usr_Email exists and is a corporate account with a Company_API_ID value that is not null and greater than zero length. strSQL = "Select Emp_CompanyFK, Usr.*, CompanyPref.CompanyPref_Data, Company_Name, Redirect_URL.CompanyPref_Data As Redirect_URL, Usr_Disable From Company " _ & "Inner Join Emp On Emp_CompanyFK = Company_PK " _ & "Inner Join Usr On Usr_PK = Emp_UsrFK " _ & "Inner Join CompanyPref On CompanyPref_CompanyFK = Emp_CompanyFK " _ & "Left Join CompanyPref Redirect_URL On Redirect_URL.CompanyPref_CompanyFK = Emp_CompanyFK And Redirect_URL.CompanyPref_Key1 = 'SAMLSSOURL' " _ & "Where CompanyPref.CompanyPref_Key1 = 'X509Thumbprint' " _ & "And Usr_Email = '" & Replace(Usr_Email, "'", "''") & "' " Dim dtEmp As DataTable = AA.Database.Federated_GetDataTable(strSQL, 2) If dtEmp.Rows.Count = 0 Then FailLogin("No records found for " & Usr_Email, LogData, "An account does not exist in Certify for the email address: " & Usr_Email) : Exit Sub End If 'Set the Redirect URL If IsDBNull(dtEmp(0)("Redirect_URL")) = False AndAlso dtEmp(0)("Redirect_URL").Length > 0 Then SessionTimeOutPage = dtEmp(0)("Redirect_URL") End If 'Disabled user check If dtEmp(0)("Usr_Disable") = True Then FailLogin("Disabled user attemping to log in " & Usr_Email, LogData, "The account is disabled in Certify for the email address: " & Usr_Email) : Exit Sub End If If dtEmp(0)("CompanyPref_Data").Length > 0 Then Dim CompanyPrefThumbprint As String = dtEmp(0)("CompanyPref_Data") If CompanyPrefThumbprint = cerCustomerX509.Thumbprint Then boolLogUserIn = True Else Try Dim xmlThumbprintDoc As New XmlDocument xmlThumbprintDoc.LoadXml(CompanyPrefThumbprint) Dim xmlThumbprintNodeList As XmlNodeList = xmlThumbprintDoc.SelectNodes("/X509Thumbprints/X509Thumbprint") For Each ThumbprintNode As XmlElement In xmlThumbprintNodeList Dim StartDate As DateTime = Today.AddDays(-1) Dim EndDate As DateTime = Today.AddDays(1) If ThumbprintNode.HasAttribute("StartDate") = True Then StartDate = CDate(ThumbprintNode.GetAttribute("StartDate")) End If If ThumbprintNode.HasAttribute("EndDate") Then EndDate = CDate(ThumbprintNode.GetAttribute("EndDate") & " 23:59:59") End If If Today > StartDate AndAlso Today < EndDate AndAlso ThumbprintNode.InnerText = cerCustomerX509.Thumbprint Then boolLogUserIn = True Exit For End If Next If boolLogUserIn = False Then FailLogin("No valid thumbprint match for " & Usr_Email, LogData, "The X509 thumbprint (" & cerCustomerX509.Thumbprint & ") does not match the value Certify has on record.") : Exit Sub End If Catch ex As Exception FailLogin("No valid thumbprint match for " & Usr_Email, LogData, "The X509 thumbprint (" & cerCustomerX509.Thumbprint & ") does not match the value Certify has on record.") : Exit Sub End Try End If End If '''' 'Try ' Select Case samlResponseXml.OwnerDocument.DocumentElement.NamespaceURI ' Case SAML.NamespaceURIs.Assertion ' If SAMLAssertionSignature.IsSigned(samlResponseXml) Then ' Dim verified As Boolean = SAMLAssertionSignature.Verify(samlResponseXml, _CurrentX509Key) ' If verified = True Then ' ' AA.Log.Write(ArchiGenObj.CLog.LogLevel.LogLevelInfo, "SAML2.aspx", "SAML Assertion Signed and Verified for " & dtEmp.Rows(0).Item("Company_Name")) ' Else ' ' AA.Log.Write(ArchiGenObj.CLog.LogLevel.LogLevelInfo, "SAML2.aspx", "SAML Assertion Signed but NOT Verified for " & dtEmp.Rows(0).Item("Company_Name")) ' End If ' Else ' 'AA.Log.Write(ArchiGenObj.CLog.LogLevel.LogLevelInfo, "SAML2.aspx", "SAML Assertion NOT signed for " & dtEmp.Rows(0).Item("Company_Name")) ' End If ' Case SAML.NamespaceURIs.Protocol ' If SAMLMessageSignature.IsSigned(samlResponseXml) Then ' Dim verified As Boolean = SAMLMessageSignature.Verify(samlResponseXml, _CurrentX509Key) ' If verified = True Then ' 'AA.Log.Write(ArchiGenObj.CLog.LogLevel.LogLevelInfo, "SAML2.aspx", "SAML Message Signed and Verified for " & dtEmp.Rows(0).Item("Company_Name")) ' Else ' ' AA.Log.Write(ArchiGenObj.CLog.LogLevel.LogLevelInfo, "SAML2.aspx", "SAML Message Signed but NOT Verified for " & dtEmp.Rows(0).Item("Company_Name")) ' End If ' Else ' 'AA.Log.Write(ArchiGenObj.CLog.LogLevel.LogLevelInfo, "SAML2.aspx", "SAML Message Not Signed for " & dtEmp.Rows(0).Item("Company_Name")) ' End If ' Case Else ' End Select 'Catch ex As Exception ' 'just an experiment to see what folks are using for signatures 'End Try '''' 'Check to make sure the DateTimeStamp is within the last one hour. Dim SSOLifetime As New TimeSpan(1, 0, 0) If Now.Subtract(SSOLifetime) > SAMLDateTime Then 'check operator on this...I may have it backwards FailLogin("Date/Time outside of window.", LogData, "The single sign on request timestamp was outside of a one hour window.") : Exit Sub End If 'Stash the SessionTimeoutPage parameter in a RAM cookie. 'We can't store it in the session because it would disappear at session timeout. 'We don't want to assign a date to this cookie because that would store it on the file system of the user's computer 'just store it in a cookie with a hard-coded name. AA.Response.Cookies.Item("SessionTimeoutPage").Value = AA.Crypto.EncryptString(SessionTimeOutPage) AA.Response.Cookies.Item("SessionTimeoutPage").Path = AA.Parser.VDirPlusFolderName() If AA.Session.Item("SecureCookies") = "1" Then AA.Response.Cookies.Item("SessionTimeoutPage").Secure = True End If If boolLogUserIn = True Then 'Log the user in. LogInUser(dtEmp.Rows(0)) End If 'Redirect to Home page. 'Response.Redirect("Home.aspx") CC.Nav.DoInitialPageRedirect() Else 'lets try to generate the metadata here ' Create an SP entity descriptor Dim spEntityDescriptor As EntityDescriptor = CreateSPEntityDescriptor() Dim XmlElement As XmlElement = spEntityDescriptor.ToXml XmlElement = spEntityDescriptor.ToXml() SAMLMetadataSignature.Generate(XmlElement, _CurrentX509Key.PrivateKey, _CurrentX509Key) ' Verify the SP entity descriptor signature If Not SAMLMetadataSignature.Verify(XmlElement) Then Throw New ArgumentException("The SP entity descriptor signature failed to verify") End If ' Read the IdP entity descriptor 'ReadMetadata(xmlElement) Response.Clear() AA.Response.ContentType = "octet-stream" AA.Response.AddHeader("Content-Disposition", "attachment;filename=" & Chr(34) & "federationmetadata.xml" & Chr(34)) AA.Response.Write(XmlElement.OuterXml) 'AA.Response.Flush() AA.Response.End() End If End Sub Private Sub FailLogin(ByVal LogMessage As String, ByVal LogData As String, ByVal UserMessage As String) Dim strScript As String Dim strContent As String SaveError(LogMessage & vbCrLf & LogData, Nothing) strScript = "var count = 15;" & vbCrLf _ & "var redirect = '" & SessionTimeOutPage & "';" & vbCrLf _ & "function countDown(){" & vbCrLf _ & " var timer = document.getElementById('timer');" & vbCrLf _ & " if(count > 0){" & vbCrLf _ & " count--;" & vbCrLf _ & " timer.innerHTML = 'This page will redirect in ' + count + ' seconds.';" & vbCrLf _ & " setTimeout('countDown()', 1000);" & vbCrLf _ & " }" & vbCrLf _ & " else {" & vbCrLf _ & " window.location.href = redirect;" & vbCrLf _ & " }" & vbCrLf _ & "}" MC.Controls.Add(AA.UI.ReturnScriptBlock(strScript)) strContent = "
" _ & "

Login Failed

" _ & "

" & UserMessage & "

" _ & "

Please contact your system administrator for more information.

" _ & "

" _ & "
" MC.Controls.Add(AA.UI.ReturnLabel(strContent)) End Sub Public Sub SaveError(ByVal exceptionString As String, ByVal samlAssertion As String) Dim strSQL, strTmp As String Dim sbPost As StringBuilder = New StringBuilder For Each strTmp In Request.Form.Keys If strTmp = "SAMLResponse" Then ' if we have a decrypted samlAssertion passed, remove the curlies and use that If samlAssertion IsNot Nothing AndAlso samlAssertion.Length <> 0 Then samlAssertion = samlAssertion.TrimStart("{").TrimEnd("}") Else ' otherwise decode the base64 string Dim data() As Byte = System.Convert.FromBase64String(Request.Form.Item(strTmp)) samlAssertion = System.Text.ASCIIEncoding.ASCII.GetString(data) End If ' log some pretty xml sbPost.Append(PrettyXml(samlAssertion) & vbCrLf) End If sbPost.Append(strTmp & "=" & Request.Form.Item(strTmp) & "&") Next ' remove the final ampersand If sbPost.Length > 0 Then sbPost.Remove(sbPost.Length - 1, 1) End If strSQL = "Insert into Errors (Errors_Date, Errors_User, Errors_ClientIP, Errors_ServerIP, " _ & "Errors_ServerName, Errors_QueryString, Errors_FormPost, Errors_ExceptionData) " _ & "Values (getDate(), 'SSO', " _ & "'" & Replace(Request.ServerVariables("REMOTE_ADDR") & "", "'", "''") & "', " _ & "'" & Replace(Request.ServerVariables("LOCAL_ADDR") & "", "'", "''") & "', " _ & "'" & Replace(Request.ServerVariables("SERVER_NAME") & "", "'", "''") & "', " _ & "'" & Replace(Request.ServerVariables("QUERY_STRING") & "", "'", "''") & "', " _ & "'" & Replace(sbPost.ToString, "'", "''") & "', " _ & "'" & exceptionString.Replace("'", "''") & "')" AA.Database.ExecuteNonQuery(strSQL) End Sub Private Function PrettyXml(ByVal xmlString As String) As String ' attempt to read the xmlString into a doc, format it, then send back a string Try Dim StrReader As New StringReader(xmlString) Dim XmlDoc As New XmlDocument XmlDoc.Load(StrReader) Dim StrBuilder As New StringBuilder() Using StrWriter As New StringWriter(StrBuilder) Using XmlWriter As New XmlTextWriter(StrWriter) XmlWriter.Formatting = Formatting.Indented XmlDoc.WriteTo(XmlWriter) End Using End Using Return StrBuilder.ToString Catch ex As Exception ' invalid xml, just throw it back Return xmlString End Try End Function Private Sub LogInUser(ByVal row As DataRow) Dim salSafe As ArchiGenObj.CSafeAccess.SafeAccessLevel = ArchiGenObj.CSafeAccess.SafeAccessLevel.SAL_Safe Session.Item("Usr_PK") = row.Item("Usr_PK") Session.Item("Usr_FName") = row.Item("Usr_FName") Session.Item("Usr_LName") = row.Item("Usr_LName") Session.Item("Usr_RoleFK") = row.Item("Usr_RoleFK") Session.Item("Usr_CultureFK") = row.Item("Usr_CultureFK") Session.Item("Usr_LanguageFK") = row.Item("Usr_LanguageFK") If IsDBNull(row.Item("Emp_CompanyFK")) = False Then Session.Item("CompanyID") = row.Item("Emp_CompanyFK") End If Session.Item("SafeAccessLevel") = salSafe Session.Item("SSOAuthenticated") = "1" AA.ShardMan.SetSessionShardID(row.Item("ShardID")) AA.Info.DeleteUserPreference("FailedLoginAttempts") AA.Info.DeleteUserPreference("LastFailedLogin") AA.Language.CacheLanguageForUser() AA.Culture.CacheCultureForUser() End Sub '---------------------Metadata functions--------------------------------------------- Private Function CreateKeyInfo(x509Certificate As X509Certificate2) As KeyInfo Dim keyInfoX509Data As New KeyInfoX509Data() keyInfoX509Data.AddCertificate(_CurrentX509Key) Dim keyInfo As New KeyInfo() keyInfo.AddClause(keyInfoX509Data) Return keyInfo End Function ' Creates a KeyDescriptor from the supplied X.509 certificate Private Function CreateKeyDescriptor(x509Certificate As X509Certificate2) As KeyDescriptor Dim keyDescriptor As New KeyDescriptor() Dim keyInfo As KeyInfo = CreateKeyInfo(_CurrentX509Key) keyDescriptor.KeyInfo = keyInfo.GetXml() Return keyDescriptor End Function ' Creates an IdP SSO descriptor Private Function CreateIDPSSODescriptor() As IDPSSODescriptor Dim idpSSODescriptor As New IDPSSODescriptor() idpSSODescriptor.WantAuthnRequestsSigned = True idpSSODescriptor.ProtocolSupportEnumeration = SAML.NamespaceURIs.Protocol idpSSODescriptor.KeyDescriptors.Add(CreateKeyDescriptor(_CurrentX509Key)) Dim artifactResolutionService As New IndexedEndpointType(1, True) artifactResolutionService.Binding = SAMLIdentifiers.BindingURIs.SOAP artifactResolutionService.Location = "https://www.certify.com/SAML2.aspx" idpSSODescriptor.ArtifactResolutionServices.Add(artifactResolutionService) idpSSODescriptor.NameIDFormats.Add(SAMLIdentifiers.NameIdentifierFormats.Transient) Dim singleSignOnService As New EndpointType(SAMLIdentifiers.BindingURIs.HTTPRedirect, "https://www.certify.com/SAML2.aspx", Nothing) idpSSODescriptor.SingleSignOnServices.Add(singleSignOnService) Return idpSSODescriptor End Function ' Creates an IdP entity descriptor Private Function CreateIDPEntityDescriptor() As EntityDescriptor Dim entityDescriptor As New EntityDescriptor() entityDescriptor.EntityID = New EntityIDType("https://www.certify.com") entityDescriptor.IDPSSODescriptors.Add(CreateIDPSSODescriptor()) Dim organization As New Organization() organization.OrganizationNames.Add(New OrganizationName("Certify, Inc.", "en")) organization.OrganizationDisplayNames.Add(New OrganizationDisplayName("Certify, Inc.", "en")) organization.OrganizationURLs.Add(New OrganizationURL("www.certify.com", "en")) entityDescriptor.Organization = organization Dim contactPerson As New ContactPerson() contactPerson.ContactTypeValue = "technical" contactPerson.GivenName = "Certify" contactPerson.Surname = "Support" contactPerson.EmailAddresses.Add("support@certify.com") entityDescriptor.ContactPeople.Add(contactPerson) Return entityDescriptor End Function ' Creates an SP SSO descriptor Private Function CreateSPSSODescriptor() As SPSSODescriptor Dim spSSODescriptor As New SPSSODescriptor() spSSODescriptor.ProtocolSupportEnumeration = SAML.NamespaceURIs.Protocol spSSODescriptor.WantAssertionsSigned = True spSSODescriptor.KeyDescriptors.Add(CreateKeyDescriptor(_CurrentX509Key)) Dim assertionConsumerService1 As New IndexedEndpointType(1, True) assertionConsumerService1.Binding = SAMLIdentifiers.BindingURIs.HTTPPost assertionConsumerService1.Location = "https://www.certify.com/SAML2.aspx" spSSODescriptor.AssertionConsumerServices.Add(assertionConsumerService1) spSSODescriptor.NameIDFormats.Add(SAMLIdentifiers.NameIdentifierFormats.Transient) Return spSSODescriptor End Function '' Creates an SP entity descriptor Private Function CreateSPEntityDescriptor() As EntityDescriptor Dim entityDescriptor As New EntityDescriptor() entityDescriptor.EntityID = New EntityIDType("https://www.certify.com") entityDescriptor.SPSSODescriptors.Add(CreateSPSSODescriptor()) Dim organization As New Organization() organization.OrganizationNames.Add(New OrganizationName("Certify, Inc.", "en")) organization.OrganizationDisplayNames.Add(New OrganizationDisplayName("Certify, Inc.", "en")) organization.OrganizationURLs.Add(New OrganizationURL("www.certify.com", "en")) entityDescriptor.Organization = organization Dim contactPerson As New ContactPerson() contactPerson.ContactTypeValue = "technical" contactPerson.GivenName = "Certify" contactPerson.Surname = "Support" contactPerson.EmailAddresses.Add("support@certify.com") entityDescriptor.ContactPeople.Add(contactPerson) Return entityDescriptor End Function Private Function GetOldKeys() As SortedList 'NOTE: When adding a key to the sorted list, add it as 1 and increment the other keys, that way we'll try them in order of most recently used to oldest Dim slKeys As New SortedList slKeys.Add(1, "C73104F8768F755FEA05610830F6823A444BC57F") slKeys.Add(2, "4F8D041DB3F84B5ADE499AE6E0971914E93291C7") Return slKeys End Function End Class