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

com.netflix.msl.keyx.DiffieHellmanExchange Maven / Gradle / Ivy

There is a newer version: 1.2226.0
Show newest version
/**
 * Copyright (c) 2012-2017 Netflix, Inc.  All rights reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.msl.keyx;

import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;

import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.DHPublicKeySpec;
import javax.crypto.spec.SecretKeySpec;

import com.netflix.msl.MslCryptoException;
import com.netflix.msl.MslEncodingException;
import com.netflix.msl.MslError;
import com.netflix.msl.MslException;
import com.netflix.msl.MslInternalException;
import com.netflix.msl.MslKeyExchangeException;
import com.netflix.msl.MslMasterTokenException;
import com.netflix.msl.crypto.CryptoCache;
import com.netflix.msl.crypto.ICryptoContext;
import com.netflix.msl.crypto.JcaAlgorithm;
import com.netflix.msl.crypto.SessionCryptoContext;
import com.netflix.msl.entityauth.EntityAuthenticationData;
import com.netflix.msl.io.MslEncoderException;
import com.netflix.msl.io.MslEncoderFactory;
import com.netflix.msl.io.MslEncoderFormat;
import com.netflix.msl.io.MslObject;
import com.netflix.msl.tokens.MasterToken;
import com.netflix.msl.tokens.TokenFactory;
import com.netflix.msl.util.AuthenticationUtils;
import com.netflix.msl.util.MslContext;

/**
 * 

Diffie-Hellman key exchange.

* * @author Wesley Miaw */ public class DiffieHellmanExchange extends KeyExchangeFactory { /** Key Diffie-Hellman parameters ID. */ private static final String KEY_PARAMETERS_ID = "parametersid"; /** Key Diffie-Hellman public key. */ private static final String KEY_PUBLIC_KEY = "publickey"; /** * If the provided byte array begins with one and only one null byte this * function simply returns the original array. Otherwise a new array is * created that is a copy of the original array with exactly one null byte * in position zero, and this new array is returned. * * @param b the original array. * @return the resulting byte array. */ private static byte[] correctNullBytes(final byte[] b) { // Count the number of leading nulls. int leadingNulls = 0; for (int i = 0; i < b.length; ++i) { if (b[i] != 0x00) break; ++leadingNulls; } // If there is exactly one leading null, return the original array. if (leadingNulls == 1) return b; // Create a copy of the non-null bytes and prepend exactly one null // byte. final int copyLength = b.length - leadingNulls; final byte[] result = new byte[copyLength + 1]; result[0] = 0x00; System.arraycopy(b, leadingNulls, result, 1, copyLength); return result; } /** *

Diffie-Hellman key request data.

* *

* {@code { * "#mandatory" : [ "parametersid", "publickey" ], * "parametersid" : "string", * "publickey" : "binary", * }} where: *

    *
  • {@code parametersid} identifies the Diffie-Hellman paramters to use
  • *
  • {@code publickey} the public key used to generate the shared secret
  • *
*

*/ public static class RequestData extends KeyRequestData { /** * Create a new Diffie-Hellman request data repository with the * specified parameters ID and public key. The private key is also * required but is not included in the request data. * * @param parametersId the parameters ID. * @param publicKey the public key Y-value. * @param privateKey the private key. */ public RequestData(final String parametersId, final BigInteger publicKey, final DHPrivateKey privateKey) { super(KeyExchangeScheme.DIFFIE_HELLMAN); this.parametersId = parametersId; this.publicKey = publicKey; this.privateKey = privateKey; } /** * Create a new Diffie-Hellman request data repository from the * provided MSL object. The private key will be unknown. * * @param keyDataMo the MSL object. * @throws MslEncodingException if there is an error parsing the data. * @throws MslKeyExchangeException if the public key is invalid. */ public RequestData(final MslObject keyDataMo) throws MslEncodingException, MslKeyExchangeException { super(KeyExchangeScheme.DIFFIE_HELLMAN); try { parametersId = keyDataMo.getString(KEY_PARAMETERS_ID); final byte[] publicKeyY = keyDataMo.getBytes(KEY_PUBLIC_KEY); if (publicKeyY.length == 0) throw new MslKeyExchangeException(MslError.KEYX_INVALID_PUBLIC_KEY, "keydata " + keyDataMo.toString()); publicKey = new BigInteger(correctNullBytes(publicKeyY)); } catch (final MslEncoderException e) { throw new MslEncodingException(MslError.MSL_PARSE_ERROR, "keydata " + keyDataMo.toString(), e); } catch (final NumberFormatException e) { throw new MslKeyExchangeException(MslError.KEYX_INVALID_PUBLIC_KEY, "keydata " + keyDataMo.toString(), e); } privateKey = null; } /* (non-Javadoc) * @see com.netflix.msl.keyx.KeyRequestData#getKeydata(com.netflix.msl.io.MslEncoderFactory, com.netflix.msl.io.MslEncoderFormat) */ @Override protected MslObject getKeydata(final MslEncoderFactory encoder, final MslEncoderFormat format) throws MslEncoderException { final MslObject mo = encoder.createObject(); mo.put(KEY_PARAMETERS_ID, parametersId); final byte[] publicKeyY = publicKey.toByteArray(); mo.put(KEY_PUBLIC_KEY, correctNullBytes(publicKeyY)); return mo; } /** * @return the parameters ID. */ public String getParametersId() { return parametersId; } /** * @return the public key Y-value. */ public BigInteger getPublicKey() { return publicKey; } /** * @return the private key or null if unknown (reconstructed from a * MSL object). */ public DHPrivateKey getPrivateKey() { return privateKey; } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(final Object obj) { if (obj == this) return true; if (!(obj instanceof RequestData)) return false; final RequestData that = (RequestData)obj; final boolean privateKeysEqual = privateKey == that.privateKey || (privateKey != null && that.privateKey != null && Arrays.equals(privateKey.getEncoded(), that.privateKey.getEncoded())); return super.equals(obj) && parametersId.equals(that.parametersId) && publicKey.equals(that.publicKey) && privateKeysEqual; } /* * (non-Javadoc) * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { // Private keys are optional but must be considered. final int privateKeyHashCode = (privateKey != null) ? Arrays.hashCode(privateKey.getEncoded()) : 0; return super.hashCode() ^ parametersId.hashCode() ^ publicKey.hashCode() ^ privateKeyHashCode; } /** Diffie-Hellman parameters ID. */ private final String parametersId; /** Diffie-Hellman public key Y-value. */ private final BigInteger publicKey; /** Diffie-Hellman private key. */ private final DHPrivateKey privateKey; } /** *

Diffie-Hellman key response data.

* *

* {@code { * "#mandatory" : [ "parametersid", "publickey" ], * "parametersid" : "string", * "publickey" : "binary", * }} where: *

    *
  • {@code parametersid} identifies the Diffie-Hellman paramters to use
  • *
  • {@code publickey} the public key used to generate the shared secret
  • *
*

*/ public static class ResponseData extends KeyResponseData { /** * Create a new Diffie-Hellman response data repository with the * provided master token, specified parameters ID and public key. * * @param masterToken the master token. * @param parametersId the parameters ID. * @param publicKey the public key Y-value. */ public ResponseData(final MasterToken masterToken, final String parametersId, final BigInteger publicKey) { super(masterToken, KeyExchangeScheme.DIFFIE_HELLMAN); this.parametersId = parametersId; this.publicKey = publicKey; } /** * Create a new Diffie-Hellman response data repository with the * provided master token from the provided MSL object. * * @param masterToken the master token. * @param keyDataMo the MSL object. * @throws MslEncodingException if there is an error parsing the data. * @throws MslKeyExchangeException if the public key is invalid. */ public ResponseData(final MasterToken masterToken, final MslObject keyDataMo) throws MslEncodingException, MslKeyExchangeException { super(masterToken, KeyExchangeScheme.DIFFIE_HELLMAN); try { parametersId = keyDataMo.getString(KEY_PARAMETERS_ID); final byte[] publicKeyY = keyDataMo.getBytes(KEY_PUBLIC_KEY); if (publicKeyY.length == 0) throw new MslKeyExchangeException(MslError.KEYX_INVALID_PUBLIC_KEY, "keydata " + keyDataMo); publicKey = new BigInteger(correctNullBytes(publicKeyY)); } catch (final MslEncoderException e) { throw new MslEncodingException(MslError.MSL_PARSE_ERROR, "keydata " + keyDataMo, e); } catch (final NumberFormatException e) { throw new MslKeyExchangeException(MslError.KEYX_INVALID_PUBLIC_KEY, "keydata " + keyDataMo, e); } } /** * @return the parameters ID. */ public String getParametersId() { return parametersId; } /** * @return the public key Y-value. */ public BigInteger getPublicKey() { return publicKey; } /* (non-Javadoc) * @see com.netflix.msl.keyx.KeyResponseData#getKeydata(com.netflix.msl.io.MslEncoderFactory, com.netflix.msl.io.MslEncoderFormat) */ @Override protected MslObject getKeydata(final MslEncoderFactory encoder, final MslEncoderFormat format) throws MslEncoderException { final MslObject mo = encoder.createObject(); mo.put(KEY_PARAMETERS_ID, parametersId); final byte[] publicKeyY = publicKey.toByteArray(); mo.put(KEY_PUBLIC_KEY, correctNullBytes(publicKeyY)); return mo; } /* * (non-Javadoc) * * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(final Object obj) { if (obj == this) return true; if (!(obj instanceof ResponseData)) return false; final ResponseData that = (ResponseData) obj; return super.equals(obj) && parametersId.equals(that.parametersId) && publicKey.equals(that.publicKey); } /* * (non-Javadoc) * * @see java.lang.Object#hashCode() */ @Override public int hashCode() { return super.hashCode() ^ parametersId.hashCode() ^ publicKey.hashCode(); } /** Diffie-Hellman parameters ID. */ private final String parametersId; /** Diffie-Hellman public key. */ private final BigInteger publicKey; } /** * Container struct for session keys. */ private static class SessionKeys { /** * @param encryptionKey the encryption key. * @param hmacKey the HMAC key. */ public SessionKeys(final SecretKey encryptionKey, final SecretKey hmacKey) { this.encryptionKey = encryptionKey; this.hmacKey = hmacKey; } /** Encryption key. */ public final SecretKey encryptionKey; /** HMAC key. */ public final SecretKey hmacKey; } /** * Derives the encryption and HMAC session keys from a Diffie-Hellman * shared secret. * * @param publicKey Diffie-Hellman public key. * @param privateKey Diffie-Hellman private key. * @param params Diffie-Hellman parameter specification. * @return the derived session keys. */ private static SessionKeys deriveSessionKeys(final PublicKey publicKey, final PrivateKey privateKey, final DHParameterSpec params) { // Compute Diffie-Hellman shared secret. final byte[] sharedSecret; try { final KeyAgreement agreement = CryptoCache.getKeyAgreement("DiffieHellman"); agreement.init(privateKey, params); agreement.doPhase(publicKey, true); sharedSecret = correctNullBytes(agreement.generateSecret()); } catch (final NoSuchAlgorithmException e) { throw new MslInternalException("DiffieHellman algorithm not found.", e); } catch (final InvalidKeyException e) { throw new MslInternalException("Diffie-Hellman private key or generated public key rejected by Diffie-Hellman key agreement.", e); } catch (final InvalidAlgorithmParameterException e) { throw new MslInternalException("Diffie-Hellman algorithm parameters rejected by Diffie-Hellman key agreement.", e); } // Derive encryption and HMAC keys. final MessageDigest sha384; try { sha384 = CryptoCache.getMessageDigest("SHA-384"); } catch (final NoSuchAlgorithmException e) { throw new MslInternalException("SHA-384 algorithm not found.", e); } final byte[] hash = sha384.digest(sharedSecret); final byte[] kcedata = new byte[128 / Byte.SIZE]; System.arraycopy(hash, 0, kcedata, 0, kcedata.length); final byte[] kchdata = new byte[256 / Byte.SIZE]; System.arraycopy(hash, kcedata.length, kchdata, 0, kchdata.length); // Return encryption and HMAC keys. final SecretKey encryptionKey = new SecretKeySpec(kcedata, JcaAlgorithm.AES); final SecretKey hmacKey = new SecretKeySpec(kchdata, JcaAlgorithm.HMAC_SHA256); return new SessionKeys(encryptionKey, hmacKey); } /** * Create a new Diffie-Hellman key exchange factory. * * @param params Diffie-Hellman parameters. * @param authutils authentication utilities. */ public DiffieHellmanExchange(final DiffieHellmanParameters params, final AuthenticationUtils authutils) { super(KeyExchangeScheme.DIFFIE_HELLMAN); this.params = params; this.authutils = authutils; } /* (non-Javadoc) * @see com.netflix.msl.keyx.KeyExchangeFactory#createRequestData(com.netflix.msl.util.MslContext, com.netflix.msl.io.MslObject) */ @Override protected KeyRequestData createRequestData(final MslContext ctx, final MslObject keyRequestMo) throws MslEncodingException, MslKeyExchangeException, MslCryptoException { return new RequestData(keyRequestMo); } /* (non-Javadoc) * @see com.netflix.msl.keyx.KeyExchangeFactory#createResponseData(com.netflix.msl.util.MslContext, com.netflix.msl.tokens.MasterToken, com.netflix.msl.io.MslObject) */ @Override protected KeyResponseData createResponseData(final MslContext ctx, final MasterToken masterToken, final MslObject keyDataMo) throws MslEncodingException, MslKeyExchangeException { return new ResponseData(masterToken, keyDataMo); } /* (non-Javadoc) * @see com.netflix.msl.keyx.KeyExchangeFactory#generateResponse(com.netflix.msl.util.MslContext, com.netflix.msl.io.MslEncoderFormat, com.netflix.msl.keyx.KeyRequestData, com.netflix.msl.tokens.MasterToken) */ @Override public KeyExchangeData generateResponse(final MslContext ctx, final MslEncoderFormat format, final KeyRequestData keyRequestData, final MasterToken masterToken) throws MslException { if (!(keyRequestData instanceof RequestData)) throw new MslInternalException("Key request data " + keyRequestData.getClass().getName() + " was not created by this factory."); final RequestData request = (RequestData) keyRequestData; // If the master token was not issued by the local entity then we // should not be generating a key response for it. if (!masterToken.isVerified()) throw new MslMasterTokenException(MslError.MASTERTOKEN_UNTRUSTED, masterToken); // Verify the scheme is permitted. final String identity = masterToken.getIdentity(); if (!authutils.isSchemePermitted(identity, this.getScheme())) throw new MslKeyExchangeException(MslError.KEYX_INCORRECT_DATA, "Authentication scheme for entity not permitted " + identity + ":" + this.getScheme()).setMasterToken(masterToken); // Load matching Diffie-Hellman parameter specification. final String parametersId = request.getParametersId(); final DHParameterSpec paramSpec = params.getParameterSpec(parametersId); if (paramSpec == null) throw new MslKeyExchangeException(MslError.UNKNOWN_KEYX_PARAMETERS_ID, parametersId); // Reconstitute request public key. final PublicKey requestPublicKey; try { final KeyFactory factory = CryptoCache.getKeyFactory("DiffieHellman"); final BigInteger y = request.getPublicKey(); final DHPublicKeySpec publicKeySpec = new DHPublicKeySpec(y, paramSpec.getP(), paramSpec.getG()); requestPublicKey = factory.generatePublic(publicKeySpec); } catch (final NoSuchAlgorithmException e) { throw new MslInternalException("DiffieHellman algorithm not found.", e); } catch (final InvalidKeySpecException e) { throw new MslInternalException("Diffie-Hellman public key specification rejected by Diffie-Hellman key factory.", e); } // Generate public/private key pair. final DHPublicKey responsePublicKey; final DHPrivateKey responsePrivateKey; try { final KeyPairGenerator generator = CryptoCache.getKeyPairGenerator("DH"); generator.initialize(paramSpec); final KeyPair keyPair = generator.generateKeyPair(); responsePublicKey = (DHPublicKey)keyPair.getPublic(); responsePrivateKey = (DHPrivateKey)keyPair.getPrivate(); } catch (final NoSuchAlgorithmException e) { throw new MslInternalException("DiffieHellman algorithm not found.", e); } catch (final InvalidAlgorithmParameterException e) { throw new MslInternalException("Diffie-Hellman algorithm parameters rejected by Diffie-Hellman key agreement.", e); } // Construct encryption and HMAC keys. final SessionKeys sessionKeys = deriveSessionKeys(requestPublicKey, responsePrivateKey, paramSpec); // Create the master token. final TokenFactory tokenFactory = ctx.getTokenFactory(); final MasterToken newMasterToken = tokenFactory.renewMasterToken(ctx, masterToken, sessionKeys.encryptionKey, sessionKeys.hmacKey, null); // Create crypto context. final ICryptoContext cryptoContext = new SessionCryptoContext(ctx, newMasterToken); // Return the key exchange data. final KeyResponseData keyResponseData = new ResponseData(newMasterToken, parametersId, responsePublicKey.getY()); return new KeyExchangeData(keyResponseData, cryptoContext); } /* (non-Javadoc) * @see com.netflix.msl.keyx.KeyExchangeFactory#generateResponse(com.netflix.msl.util.MslContext, com.netflix.msl.io.MslEncoderFormat, com.netflix.msl.keyx.KeyRequestData, com.netflix.msl.entityauth.EntityAuthenticationData) */ @Override public KeyExchangeData generateResponse(final MslContext ctx, final MslEncoderFormat format, final KeyRequestData keyRequestData, final EntityAuthenticationData entityAuthData) throws MslException { if (!(keyRequestData instanceof RequestData)) throw new MslInternalException("Key request data " + keyRequestData.getClass().getName() + " was not created by this factory."); final RequestData request = (RequestData)keyRequestData; // Verify the scheme is permitted. final String identity = entityAuthData.getIdentity(); if (!authutils.isSchemePermitted(identity, this.getScheme())) throw new MslKeyExchangeException(MslError.KEYX_INCORRECT_DATA, "Authentication scheme for entity not permitted " + identity + ":" + this.getScheme()).setEntityAuthenticationData(entityAuthData); // Load matching Diffie-Hellman parameter specification. final String parametersId = request.getParametersId(); final DHParameterSpec paramSpec = params.getParameterSpec(parametersId); if (paramSpec == null) throw new MslKeyExchangeException(MslError.UNKNOWN_KEYX_PARAMETERS_ID, parametersId).setEntityAuthenticationData(entityAuthData); // Reconstitute request public key. final PublicKey requestPublicKey; try { final KeyFactory factory = CryptoCache.getKeyFactory("DiffieHellman"); final BigInteger y = request.getPublicKey(); final DHPublicKeySpec publicKeySpec = new DHPublicKeySpec(y, paramSpec.getP(), paramSpec.getG()); requestPublicKey = factory.generatePublic(publicKeySpec); } catch (final NoSuchAlgorithmException e) { throw new MslInternalException("DiffieHellman algorithm not found.", e); } catch (final InvalidKeySpecException e) { throw new MslInternalException("Diffie-Hellman public key specification rejected by Diffie-Hellman key factory.", e); } // Generate public/private key pair. final DHPublicKey responsePublicKey; final DHPrivateKey responsePrivateKey; try { final KeyPairGenerator generator = KeyPairGenerator.getInstance("DH"); generator.initialize(paramSpec); final KeyPair keyPair = generator.generateKeyPair(); responsePublicKey = (DHPublicKey)keyPair.getPublic(); responsePrivateKey = (DHPrivateKey)keyPair.getPrivate(); } catch (final NoSuchAlgorithmException e) { throw new MslInternalException("DiffieHellman algorithm not found.", e); } catch (final InvalidAlgorithmParameterException e) { throw new MslInternalException("Diffie-Hellman algorithm parameters rejected by Diffie-Hellman key agreement.", e); } // Construct encryption and HMAC keys. final SessionKeys sessionKeys = deriveSessionKeys(requestPublicKey, responsePrivateKey, paramSpec); // Create the master token. final TokenFactory tokenFactory = ctx.getTokenFactory(); final MasterToken masterToken = tokenFactory.createMasterToken(ctx, entityAuthData, sessionKeys.encryptionKey, sessionKeys.hmacKey, null); // Create crypto context. final ICryptoContext cryptoContext; try { cryptoContext = new SessionCryptoContext(ctx, masterToken); } catch (final MslMasterTokenException e) { throw new MslInternalException("Master token constructed by token factory is not trusted.", e); } // Return the key exchange data. final KeyResponseData keyResponseData = new ResponseData(masterToken, parametersId, responsePublicKey.getY()); return new KeyExchangeData(keyResponseData, cryptoContext); } /* (non-Javadoc) * @see com.netflix.msl.keyx.KeyExchangeFactory#getCryptoContext(com.netflix.msl.util.MslContext, com.netflix.msl.keyx.KeyRequestData, com.netflix.msl.keyx.KeyResponseData, com.netflix.msl.tokens.MasterToken) */ @Override public ICryptoContext getCryptoContext(final MslContext ctx, final KeyRequestData keyRequestData, final KeyResponseData keyResponseData, final MasterToken masterToken) throws MslKeyExchangeException, MslCryptoException, MslEncodingException, MslMasterTokenException { if (!(keyRequestData instanceof RequestData)) throw new MslInternalException("Key request data " + keyRequestData.getClass().getName() + " was not created by this factory."); final RequestData request = (RequestData)keyRequestData; if (!(keyResponseData instanceof ResponseData)) throw new MslInternalException("Key response data " + keyResponseData.getClass().getName() + " was not created by this factory."); final ResponseData response = (ResponseData)keyResponseData; // Verify response matches request. final String requestParametersId = request.getParametersId(); final String responseParametersId = response.getParametersId(); if (!requestParametersId.equals(responseParametersId)) throw new MslKeyExchangeException(MslError.KEYX_RESPONSE_REQUEST_MISMATCH, "request " + requestParametersId + "; response " + responseParametersId).setMasterToken(masterToken); // Reconstitute response public key. final DHPrivateKey privateKey = request.getPrivateKey(); if (privateKey == null) throw new MslKeyExchangeException(MslError.KEYX_PRIVATE_KEY_MISSING, "request Diffie-Hellman private key").setMasterToken(masterToken); final DHParameterSpec params = privateKey.getParams(); final PublicKey publicKey; try { final KeyFactory factory = CryptoCache.getKeyFactory("DiffieHellman"); final BigInteger y = response.getPublicKey(); final DHPublicKeySpec publicKeySpec = new DHPublicKeySpec(y, params.getP(), params.getG()); publicKey = factory.generatePublic(publicKeySpec); } catch (final NoSuchAlgorithmException e) { throw new MslInternalException("DiffieHellman algorithm not found.", e); } catch (final InvalidKeySpecException e) { throw new MslKeyExchangeException(MslError.KEYX_INVALID_PUBLIC_KEY, "Diffie-Hellman public key specification rejected by Diffie-Hellman key factory.", e); } // Create crypto context. final String identity = ctx.getEntityAuthenticationData(null).getIdentity(); final SessionKeys sessionKeys = deriveSessionKeys(publicKey, privateKey, params); final MasterToken responseMasterToken = response.getMasterToken(); return new SessionCryptoContext(ctx, responseMasterToken, identity, sessionKeys.encryptionKey, sessionKeys.hmacKey); } /** Diffie-Hellman parameters. */ private final DiffieHellmanParameters params; /** Authentication utilities. */ private final AuthenticationUtils authutils; }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy