All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.rahas.impl.util.SAMLUtils Maven / Gradle / Ivy

There is a newer version: 1.7.1
Show newest version
package org.apache.rahas.impl.util;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.rahas.RahasConstants;
import org.apache.rahas.RahasData;
import org.apache.rahas.TrustException;
import org.apache.rahas.impl.TokenIssuerUtil;
import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSSecurityException;
import org.apache.ws.security.components.crypto.Crypto;
import org.apache.ws.security.message.WSSecEncryptedKey;
import org.apache.ws.security.util.Base64;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.utils.EncryptionConstants;
import org.joda.time.DateTime;
import org.opensaml.Configuration;
import org.opensaml.saml1.core.*;
import org.opensaml.ws.wssecurity.KeyIdentifier;
import org.opensaml.ws.wssecurity.SecurityTokenReference;
import org.opensaml.ws.wssecurity.WSSecurityConstants;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.XMLObjectBuilder;
import org.opensaml.xml.encryption.CipherData;
import org.opensaml.xml.encryption.CipherValue;
import org.opensaml.xml.encryption.EncryptedKey;
import org.opensaml.xml.encryption.EncryptionMethod;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.schema.XSString;
import org.opensaml.xml.schema.impl.XSStringBuilder;
import org.opensaml.xml.security.SecurityHelper;
import org.opensaml.xml.security.credential.Credential;
import org.opensaml.xml.signature.*;
import org.opensaml.xml.signature.KeyInfo;
import org.opensaml.xml.signature.X509Data;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.namespace.QName;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.*;

/**
 * Utility class for SAML 1 assertions. Responsible for manipulating all SAML1 specific objects
 * like Assertion, ConfirmationMethod etc ...
 */
public class SAMLUtils {

    private static final Log log = LogFactory.getLog(SAMLUtils.class);

    public static Collection getCertChainCollection(X509Certificate[] issuerCerts) {
        ArrayList certCollection = new ArrayList();

        if (issuerCerts == null) {
            return certCollection;
        } else {
            Collections.addAll(certCollection, issuerCerts);
        }

        return certCollection;
    }

    /**
     * Builds the requested XMLObject.
     *
     * @param objectQName name of the XMLObject
     * @return the build XMLObject
     * @throws org.apache.rahas.TrustException If unable to find the appropriate builder.
     */
    public static XMLObject buildXMLObject(QName objectQName) throws TrustException {
        XMLObjectBuilder builder = Configuration.getBuilderFactory().getBuilder(objectQName);
        if (builder == null) {
            log.debug("Unable to find OpenSAML builder for object " + objectQName);
            throw new TrustException("builderNotFound",new Object[]{objectQName});
        }
        return builder.buildObject(objectQName.getNamespaceURI(), objectQName.getLocalPart(), objectQName.getPrefix());
    }

    /**
     * Builds an assertion from an XML element.
     * @param assertionElement The XML element.
     * @return An Assertion object.
     */
    public static Assertion buildAssertion(Element assertionElement) {

       return (Assertion) Configuration.getBuilderFactory().
               getBuilder(Assertion.DEFAULT_ELEMENT_NAME).buildObject(assertionElement);

    }

/**
     * Signs the SAML assertion. The steps to sign SAML assertion is as follows,
     * 
    *
  1. Get certificate for issuer alias
  2. *
  3. Extract private key
  4. *
  5. Create {@link org.opensaml.xml.security.credential.Credential} object
  6. *
  7. Create {@link org.opensaml.xml.signature.Signature} object
  8. *
  9. Set Signature object in Assertion
  10. *
  11. Prepare signing environment - SecurityHelper.prepareSignatureParams
  12. *
  13. Perform signing action - Signer.signObject
  14. *
* @param assertion The assertion to be signed. * @param crypto Certificate and private key data are stored in Crypto object * @param issuerKeyAlias Key alias * @param issuerKeyPassword Key password * @throws TrustException If an error occurred while signing the assertion. */ public static void signAssertion(Assertion assertion, Crypto crypto, String issuerKeyAlias, String issuerKeyPassword) throws TrustException { X509Certificate issuerCerts = CommonUtil.getCertificateByAlias(crypto, issuerKeyAlias); String signatureAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_RSA; PublicKey issuerPublicKey = issuerCerts.getPublicKey(); String publicKeyAlgorithm = issuerPublicKey.getAlgorithm(); if (publicKeyAlgorithm.equalsIgnoreCase("DSA")) { signatureAlgorithm = XMLSignature.ALGO_ID_SIGNATURE_DSA; } PrivateKey issuerPrivateKey; try { issuerPrivateKey = crypto.getPrivateKey( issuerKeyAlias, issuerKeyPassword); } catch (Exception e) { log.debug("Unable to get issuer private key for issuer alias " + issuerKeyAlias); throw new TrustException("issuerPrivateKeyNotFound", new Object[]{issuerKeyAlias}); } Credential signingCredential = SecurityHelper.getSimpleCredential(issuerPublicKey, issuerPrivateKey); Signature signature = (Signature) SAMLUtils.buildXMLObject(Signature.DEFAULT_ELEMENT_NAME); signature.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); signature.setSigningCredential(signingCredential); signature.setSignatureAlgorithm(signatureAlgorithm); X509Data x509Data = createX509Data(issuerCerts); KeyInfo keyInfo = createKeyInfo(x509Data); signature.setKeyInfo(keyInfo); assertion.setSignature(signature); try { Document document = CommonUtil.getOMDOMDocument(); Configuration.getMarshallerFactory().getMarshaller(assertion).marshall(assertion, document); } catch (MarshallingException e) { log.debug("Error while marshalling assertion ", e); throw new TrustException("errorMarshallingAssertion", e); } try { Signer.signObject(signature); } catch (SignatureException e) { log.debug("Error signing SAML Assertion. An error occurred while signing SAML Assertion with alias " + issuerKeyAlias, e); throw new TrustException("errorSigningAssertion", e); } } /** * Get subject confirmation method of the given SAML 1.1 Assertion. * This is used in rampart-core. * @param assertion SAML 1.1 Assertion * @return subject confirmation method */ public static String getSAML11SubjectConfirmationMethod(Assertion assertion) { String subjectConfirmationMethod = RahasConstants.SAML11_SUBJECT_CONFIRMATION_HOK; // iterate the statements and get the subject confirmation method. List statements = assertion.getStatements(); // TODO check whether there is an efficient method of doing this if (!statements.isEmpty()) { SubjectStatement subjectStatement = (SubjectStatement) statements.get(0); Subject subject = subjectStatement.getSubject(); if (subject != null) { SubjectConfirmation subjectConfirmation = subject.getSubjectConfirmation(); if (subjectConfirmation != null) { List confirmationMethods = subjectConfirmation.getConfirmationMethods(); if (!confirmationMethods.isEmpty()) { subjectConfirmationMethod = confirmationMethods.get(0).getConfirmationMethod(); } } } } return subjectConfirmationMethod; } /** * Create named identifier. * @param principalName Name of the subject. * @param format Format of the subject, whether it is an email, uid etc ... * @return The NamedIdentifier object. * @throws org.apache.rahas.TrustException If unable to find the builder. */ public static NameIdentifier createNamedIdentifier(String principalName, String format) throws TrustException{ NameIdentifier nameId = (NameIdentifier)SAMLUtils.buildXMLObject(NameIdentifier.DEFAULT_ELEMENT_NAME); nameId.setNameIdentifier(principalName); nameId.setFormat(format); return nameId; } /** * Creates the subject confirmation method. * Relevant XML element would look like as follows, * * urn:oasis:names:tc:SAML:1.0:cm:holder-of-key * * @param confirmationMethod Name of the actual confirmation method. Could be * holder-of-key - "urn:oasis:names:tc:SAML:1.0:cm:holder-of-key" * sender-vouches - "urn:oasis:names:tc:SAML:1.0:cm:sender-vouches" * bearer - TODO * @return Returns the opensaml representation of the ConfirmationMethod. * @throws TrustException If unable to find appropriate XMLObject builder for confirmation QName. */ public static ConfirmationMethod createSubjectConfirmationMethod(final String confirmationMethod) throws TrustException { ConfirmationMethod confirmationMethodObject = (ConfirmationMethod)SAMLUtils.buildXMLObject(ConfirmationMethod.DEFAULT_ELEMENT_NAME); confirmationMethodObject.setConfirmationMethod(confirmationMethod); return confirmationMethodObject; } /** * Creates opensaml SubjectConfirmation representation. The relevant XML would looks as follows, * * * urn:oasis:names:tc:SAML:1.0:cm:sender-vouches * * * @param confirmationMethod The subject confirmation method. Bearer, Sender-Vouches or Holder-Of-Key. * @param keyInfoContent The KeyInfo content. According to SPEC (SAML 1.1) this could be null. * @return OpenSAML representation of SubjectConfirmation. * @throws TrustException If unable to find any of the XML builders. */ public static SubjectConfirmation createSubjectConfirmation(final String confirmationMethod, KeyInfo keyInfoContent) throws TrustException { SubjectConfirmation subjectConfirmation = (SubjectConfirmation)SAMLUtils.buildXMLObject(SubjectConfirmation.DEFAULT_ELEMENT_NAME); ConfirmationMethod method = SAMLUtils.createSubjectConfirmationMethod(confirmationMethod); subjectConfirmation.getConfirmationMethods().add(method); if (keyInfoContent != null) { subjectConfirmation.setKeyInfo(keyInfoContent); } return subjectConfirmation; } /** * Creates an opensaml Subject representation. The relevant XML would looks as follows, * * * uid=joe,ou=people,ou=saml-demo,o=baltimore.com * * * * urn:oasis:names:tc:SAML:1.0:cm:holder-of-key * * * ... * * * * @param nameIdentifier Represent the "NameIdentifier" of XML element above. * @param confirmationMethod Represent the bearer, HOK or Sender-Vouches. * @param keyInfoContent Key info information. This could be null. * @return OpenSAML representation of the Subject. * @throws TrustException If a relevant XML builder is unable to find. */ public static Subject createSubject(final NameIdentifier nameIdentifier, final String confirmationMethod, KeyInfo keyInfoContent) throws TrustException { Subject subject = (Subject)SAMLUtils.buildXMLObject(Subject.DEFAULT_ELEMENT_NAME); subject.setNameIdentifier(nameIdentifier); SubjectConfirmation subjectConfirmation = SAMLUtils.createSubjectConfirmation(confirmationMethod,keyInfoContent); subject.setSubjectConfirmation(subjectConfirmation); return subject; } /** * Creates an AuthenticationStatement. The relevant XML element looks as follows, * * * * [email protected] * * urn:oasis:names:tc:SAML:1.0:cm:bearer * * * * * @param subject OpenSAML Subject implementation. * @param authenticationMethod How subject is authenticated ? i.e. by using a password, kerberos, certificate * etc ... The method is defined as a URL in SAML specification. * @param authenticationInstant Time which authentication took place. * @return opensaml AuthenticationStatement object. * @throws org.apache.rahas.TrustException If unable to find the builder. */ public static AuthenticationStatement createAuthenticationStatement(Subject subject, String authenticationMethod, DateTime authenticationInstant) throws TrustException { AuthenticationStatement authenticationStatement = (AuthenticationStatement)SAMLUtils.buildXMLObject(AuthenticationStatement.DEFAULT_ELEMENT_NAME); authenticationStatement.setSubject(subject); authenticationStatement.setAuthenticationMethod(authenticationMethod); authenticationStatement.setAuthenticationInstant(authenticationInstant); return authenticationStatement; } /**Creates an attribute statement. Sample attribute statement would look like follows, * * * * uid=joe,ou=people,ou=saml-demo,o=baltimore.com * * * * urn:oasis:names:tc:SAML:1.0:cm:holder-of-key * * * ... * * * * * gold * * * [email protected] * * * * @param subject The OpenSAML representation of the Subject. * @param attributeList List of attribute values to include within the message. * @return OpenSAML representation of AttributeStatement. * @throws org.apache.rahas.TrustException If unable to find the appropriate builder. */ public static AttributeStatement createAttributeStatement(Subject subject, List attributeList) throws TrustException { AttributeStatement attributeStatement = (AttributeStatement)SAMLUtils.buildXMLObject(AttributeStatement.DEFAULT_ELEMENT_NAME); attributeStatement.setSubject(subject); attributeStatement.getAttributes().addAll(attributeList); return attributeStatement; } /** * Creates Conditions object. Analogous XML element is as follows, * * NotBefore="2002-06-19T16:53:33.173Z" * NotOnOrAfter="2002-06-19T17:08:33.173Z"/> * @param notBefore The validity of the Assertion starts from this value. * @param notOnOrAfter The validity ends from this value. * @return OpenSAML Conditions object. * @throws org.apache.rahas.TrustException If unable to find appropriate builder. */ public static Conditions createConditions(DateTime notBefore, DateTime notOnOrAfter) throws TrustException { Conditions conditions = (Conditions)SAMLUtils.buildXMLObject(Conditions.DEFAULT_ELEMENT_NAME); conditions.setNotBefore(notBefore); conditions.setNotOnOrAfter(notOnOrAfter); return conditions; } /** * This method creates the final SAML assertion. The final SAML assertion would looks like as follows, * * * NotBefore="2002-06-19T16:53:33.173Z" * NotOnOrAfter="2002-06-19T17:08:33.173Z"/> * * * * uid=joe,ou=people,ou=saml-demo,o=baltimore.com * * * * urn:oasis:names:tc:SAML:1.0:cm:holder-of-key * * * ... * * * * * gold * * * [email protected] * * * ... * * @param issuerName Represents the "Issuer" in Assertion. * @param notBefore The Condition's NotBefore value * @param notOnOrAfter The Condition's NotOnOrAfter value * @param statements Other statements. * @return An opensaml Assertion object. * @throws org.apache.rahas.TrustException If unable to find the appropriate builder. */ public static Assertion createAssertion(String issuerName, DateTime notBefore, DateTime notOnOrAfter, List statements) throws TrustException { Assertion assertion = (Assertion)SAMLUtils.buildXMLObject(Assertion.DEFAULT_ELEMENT_NAME); assertion.setIssuer(issuerName); assertion.setConditions(SAMLUtils.createConditions(notBefore, notOnOrAfter)); assertion.getStatements().addAll(statements); return assertion; } /** * Creates a SAML attribute similar to following, * * gold * * @param name attribute name * @param namespace attribute namespace. * @param value attribute value. * @return OpenSAML representation of the attribute. * @throws org.apache.rahas.TrustException If unable to find the appropriate builder. */ public static Attribute createAttribute(String name, String namespace, String value) throws TrustException { Attribute attribute = (Attribute)SAMLUtils.buildXMLObject(Attribute.DEFAULT_ELEMENT_NAME); attribute.setAttributeName(name); attribute.setAttributeNamespace(namespace); XSStringBuilder attributeValueBuilder = (XSStringBuilder)Configuration.getBuilderFactory(). getBuilder(XSString.TYPE_NAME); XSString stringValue = attributeValueBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); stringValue.setValue(value); attribute.getAttributeValues().add(stringValue); return attribute; } /** * Creates a KeyInfo object * @return OpenSAML KeyInfo representation. * @throws TrustException If an error occurred while creating KeyInfo. */ public static KeyInfo createKeyInfo() throws TrustException { return (KeyInfo)SAMLUtils.buildXMLObject(KeyInfo.DEFAULT_ELEMENT_NAME); } /** * Creates a KeyInfo element given EncryptedKey. The relevant XML would looks as follows, * * * * @param encryptedKey The OpemSAML representation of encrypted key. * @return The appropriate opensaml representation of the KeyInfo. * @throws org.apache.rahas.TrustException If unable to find the builder. */ public static KeyInfo createKeyInfo(EncryptedKey encryptedKey) throws TrustException { KeyInfo keyInfo = createKeyInfo(); keyInfo.getEncryptedKeys().add(encryptedKey); return keyInfo; } /** * Creates a KeyInfo element given EncryptedKey. The relevant XML would looks as follows, * * * * @param x509Data The OpemSAML representation X509Data * @return The appropriate opensaml representation of the KeyInfo. * @throws org.apache.rahas.TrustException If unable to find the builder. */ public static KeyInfo createKeyInfo(X509Data x509Data) throws TrustException { KeyInfo keyInfo = createKeyInfo(); keyInfo.getX509Datas().add(x509Data); return keyInfo; } /** * Creates the certificate based KeyInfo object. * @param certificate The public key certificate used to create the KeyInfo object. * @return OpenSAML representation of KeyInfo object. * @throws TrustException If an error occurred while creating the KeyInfo */ public static KeyInfo getCertificateBasedKeyInfo(X509Certificate certificate) throws TrustException { X509Data x509Data = SAMLUtils.createX509Data(certificate); return SAMLUtils.createKeyInfo(x509Data); } /** * This method creates KeyInfo element of an assertion. This is a facade, in which it calls * to other helper methods to create KeyInfo. The TokenIssuer will call this method to * create the KeyInfo. * @param doc An Axiom based DOM Document. * @param data The ephemeral key which we use here need in encrypting the message also. Therefore * we need to save the ephemeral key in RahasData passed here. * @param serviceCert Public key used to encrypt the assertion is extracted from this certificate. * @param keySize Size of the key to be used * @param crypto The relevant private key * @param keyComputation Key computation mechanism. * @return OpenSAML KeyInfo representation. * @throws WSSecurityException We use WSS4J to generate encrypted key. This exception will trigger if an * error occurs while generating the encrypted key. * @throws TrustException If an error occurred while creating KeyInfo object. */ public static KeyInfo getSymmetricKeyBasedKeyInfo(Document doc, RahasData data, X509Certificate serviceCert, int keySize, Crypto crypto, int keyComputation) throws WSSecurityException, TrustException { byte[] ephemeralKey = TokenIssuerUtil.getSharedSecret( data, keyComputation, keySize); WSSecEncryptedKey encryptedKey = getSymmetricKeyBasedKeyInfoContent(doc, ephemeralKey, serviceCert, keySize, crypto); // Extract the base64 encoded secret value byte[] tempKey = new byte[keySize / 8]; System.arraycopy(encryptedKey.getEphemeralKey(), 0, tempKey, 0, keySize / 8); data.setEphmeralKey(tempKey); EncryptedKey samlEncryptedKey = SAMLUtils.createEncryptedKey(serviceCert, encryptedKey); return SAMLUtils.createKeyInfo(samlEncryptedKey); } // TODO remove keySize parameter static WSSecEncryptedKey getSymmetricKeyBasedKeyInfoContent(Document doc, byte[] ephemeralKey, X509Certificate serviceCert, int keySize, Crypto crypto) throws WSSecurityException, TrustException { // Create the encrypted key WSSecEncryptedKey encryptedKeyBuilder = new WSSecEncryptedKey(); // Use thumbprint id encryptedKeyBuilder .setKeyIdentifierType(WSConstants.THUMBPRINT_IDENTIFIER); // SEt the encryption cert encryptedKeyBuilder.setUseThisCert(serviceCert); // TODO setting keysize is removed with wss4j 1.6 migration - do we actually need this ? encryptedKeyBuilder.setEphemeralKey(ephemeralKey); // Set key encryption algo encryptedKeyBuilder .setKeyEncAlgo(EncryptionConstants.ALGO_ID_KEYTRANSPORT_RSA15); // Build encryptedKeyBuilder.prepare(doc, crypto); return encryptedKeyBuilder; } /** * Creates the X509 data element in a SAML issuer token. Should create an element similar to following, * * * MIICNTCCAZ6gAwIB... * * * @param clientCert Client certificate to be used when generating X509 data * @return SAML X509Data representation. * @throws TrustException If an error occurred while creating X509Data and X509Certificate. */ static X509Data createX509Data(X509Certificate clientCert) throws TrustException { byte[] clientCertBytes; try { clientCertBytes = clientCert.getEncoded(); } catch (CertificateEncodingException e) { log.error("An error occurred while encoding certificate.", e); throw new TrustException("An error occurred while encoding certificate.", e); } String base64Cert = Base64.encode(clientCertBytes); org.opensaml.xml.signature.X509Certificate x509Certificate = (org.opensaml.xml.signature.X509Certificate)SAMLUtils.buildXMLObject (org.opensaml.xml.signature.X509Certificate.DEFAULT_ELEMENT_NAME); x509Certificate.setValue(base64Cert); X509Data x509Data = (X509Data)SAMLUtils.buildXMLObject(X509Data.DEFAULT_ELEMENT_NAME); x509Data.getX509Certificates().add(x509Certificate); return x509Data; } /** * This method will created the "EncryptedKey" of a SAML assertion. * An encrypted key would look like as follows, * * * * * * a/jhNus21KVuoFx65LmkW2O/l10= * * * * * * dnP0MBHiMLlSmnjJhGFs/I8/z... * * * * @param certificate Certificate which holds the public key to encrypt ephemeral key. * @param wsSecEncryptedKey WS Security object which contains encrypted ephemeral key. * TODO Passing WSSecEncryptedKey is an overhead. We should be able to create encrypted ephemeral * key without WSS4J * @return OpenSAML EncryptedKey representation. * @throws TrustException If an error occurred while creating EncryptedKey. */ static EncryptedKey createEncryptedKey(X509Certificate certificate, WSSecEncryptedKey wsSecEncryptedKey) throws TrustException { SecurityTokenReference securityTokenReference = (SecurityTokenReference)SAMLUtils.buildXMLObject(SecurityTokenReference.ELEMENT_NAME); KeyIdentifier keyIdentifier = (KeyIdentifier)SAMLUtils.buildXMLObject(KeyIdentifier.ELEMENT_NAME); // Encoding type set to http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0 // #Base64Binary keyIdentifier.setEncodingType(KeyIdentifier.ENCODING_TYPE_BASE64_BINARY); keyIdentifier.setValueType(WSSecurityConstants.THUMB_PRINT_SHA1); keyIdentifier.setValue(getThumbprintSha1(certificate)); securityTokenReference.getUnknownXMLObjects().add(keyIdentifier); KeyInfo keyInfo = SAMLUtils.createKeyInfo(); keyInfo.getXMLObjects().add(securityTokenReference); CipherValue cipherValue = (CipherValue)buildXMLObject(CipherValue.DEFAULT_ELEMENT_NAME); cipherValue.setValue(Base64.encode(wsSecEncryptedKey.getEncryptedEphemeralKey())); CipherData cipherData = (CipherData)buildXMLObject(CipherData.DEFAULT_ELEMENT_NAME); cipherData.setCipherValue(cipherValue); EncryptionMethod encryptionMethod = (EncryptionMethod)buildXMLObject(EncryptionMethod.DEFAULT_ELEMENT_NAME); encryptionMethod.setAlgorithm(EncryptionConstants.ALGO_ID_KEYTRANSPORT_RSA15); EncryptedKey encryptedKey = (EncryptedKey)SAMLUtils.buildXMLObject(EncryptedKey.DEFAULT_ELEMENT_NAME); encryptedKey.setID(wsSecEncryptedKey.getId()); encryptedKey.setEncryptionMethod(encryptionMethod); encryptedKey.setCipherData(cipherData); encryptedKey.setKeyInfo(keyInfo); return encryptedKey; } private static String getThumbprintSha1(X509Certificate cert) throws TrustException { MessageDigest sha; try { sha = MessageDigest.getInstance("SHA-1"); } catch (NoSuchAlgorithmException e1) { throw new TrustException("sha1NotFound", e1); } sha.reset(); try { sha.update(cert.getEncoded()); } catch (CertificateEncodingException e1) { throw new TrustException("certificateEncodingError", e1); } byte[] data = sha.digest(); return Base64.encode(data); } /** * Converts java.util.Date to opensaml DateTime object. * @param date Java util date * @return opensaml specific DateTime object. */ public static DateTime convertToDateTime(Date date) { return new DateTime(date); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy