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

org.apache.sshd.common.config.keys.KeyUtils Maven / Gradle / Ivy

There is a newer version: 2.14.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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 org.apache.sshd.common.config.keys;

import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.DSAKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPrivateKey;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.ECKey;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.interfaces.RSAKey;
import java.security.interfaces.RSAPrivateCrtKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.sshd.common.Factory;
import org.apache.sshd.common.cipher.ECCurves;
import org.apache.sshd.common.config.keys.impl.DSSPublicKeyEntryDecoder;
import org.apache.sshd.common.config.keys.impl.ECDSAPublicKeyEntryDecoder;
import org.apache.sshd.common.config.keys.impl.OpenSSHCertificateDecoder;
import org.apache.sshd.common.config.keys.impl.RSAPublicKeyDecoder;
import org.apache.sshd.common.config.keys.impl.SkECDSAPublicKeyEntryDecoder;
import org.apache.sshd.common.config.keys.impl.SkED25519PublicKeyEntryDecoder;
import org.apache.sshd.common.config.keys.u2f.SkED25519PublicKey;
import org.apache.sshd.common.config.keys.u2f.SkEcdsaPublicKey;
import org.apache.sshd.common.digest.BuiltinDigests;
import org.apache.sshd.common.digest.Digest;
import org.apache.sshd.common.digest.DigestFactory;
import org.apache.sshd.common.digest.DigestUtils;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils.MapBuilder;
import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder;
import org.apache.sshd.common.util.OsUtils;
import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.security.SecurityUtils;

/**
 * Utility class for keys
 *
 * @author Apache MINA SSHD Project
 */
public final class KeyUtils {
    /**
     * Name of algorithm for RSA keys to be used when calling security provider
     */
    public static final String RSA_ALGORITHM = "RSA";

    /**
     * The most commonly used RSA public key exponent
     */
    public static final BigInteger DEFAULT_RSA_PUBLIC_EXPONENT = BigInteger.valueOf(65537);

    /**
     * Name of algorithm for DSS keys to be used when calling security provider
     */
    public static final String DSS_ALGORITHM = "DSA";

    /**
     * Name of algorithm for EC keys to be used when calling security provider
     */
    public static final String EC_ALGORITHM = "EC";

    /**
     * The {@link Set} of {@link PosixFilePermission} not allowed if strict permissions are enforced on key files
     */
    public static final Set STRICTLY_PROHIBITED_FILE_PERMISSION = Collections.unmodifiableSet(
            EnumSet.of(
                    PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_WRITE, PosixFilePermission.GROUP_EXECUTE,
                    PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_WRITE, PosixFilePermission.OTHERS_EXECUTE));

    /**
     * System property that can be used to control the default fingerprint factory used for keys. If not set the
     * {@link #DEFAULT_FINGERPRINT_DIGEST_FACTORY} is used
     */
    public static final String KEY_FINGERPRINT_FACTORY_PROP = "org.apache.sshd.keyFingerprintFactory";

    /**
     * The default {@link Factory} of {@link Digest}s initialized as the value of
     * {@link #getDefaultFingerPrintFactory()} if not overridden by {@link #KEY_FINGERPRINT_FACTORY_PROP} or
     * {@link #setDefaultFingerPrintFactory(DigestFactory)}
     */
    public static final DigestFactory DEFAULT_FINGERPRINT_DIGEST_FACTORY = BuiltinDigests.sha256;

    /** @see https://tools.ietf.org/html/rfc8332#section-3 */
    public static final String RSA_SHA256_KEY_TYPE_ALIAS = "rsa-sha2-256";
    public static final String RSA_SHA512_KEY_TYPE_ALIAS = "rsa-sha2-512";
    public static final String RSA_SHA256_CERT_TYPE_ALIAS = "[email protected]";
    public static final String RSA_SHA512_CERT_TYPE_ALIAS = "[email protected]";

    private static final AtomicReference DEFAULT_DIGEST_HOLDER = new AtomicReference<>();

    private static final Map> BY_KEY_TYPE_DECODERS_MAP
            = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

    // order matters here, when, for example, SkED25519PublicKeyEntryDecoder is used, it registers the PrivateKey
    // type as java.security.PrivateKey, and all PrivateKey types are assignable to this, so it must be consulted last
    private static final Map, PublicKeyEntryDecoder> BY_KEY_CLASS_DECODERS_MAP = new LinkedHashMap<>();

    private static final Map KEY_TYPE_ALIASES
            = NavigableMapBuilder. builder(String.CASE_INSENSITIVE_ORDER)
                    .put(RSA_SHA256_KEY_TYPE_ALIAS, KeyPairProvider.SSH_RSA)
                    .put(RSA_SHA512_KEY_TYPE_ALIAS, KeyPairProvider.SSH_RSA)
                    .put(RSA_SHA256_CERT_TYPE_ALIAS, KeyPairProvider.SSH_RSA_CERT)
                    .put(RSA_SHA512_CERT_TYPE_ALIAS, KeyPairProvider.SSH_RSA_CERT)
                    .build();

    private static final Map SIGNATURE_ALGORITHM_MAP
            = MapBuilder. builder()
                    .put(RSA_SHA256_CERT_TYPE_ALIAS, RSA_SHA256_KEY_TYPE_ALIAS)
                    .put(RSA_SHA512_CERT_TYPE_ALIAS, RSA_SHA512_KEY_TYPE_ALIAS)
                    .put(KeyPairProvider.SSH_RSA_CERT, KeyPairProvider.SSH_RSA)
                    .put(KeyPairProvider.SSH_DSS_CERT, KeyPairProvider.SSH_DSS)
                    .put(KeyPairProvider.SSH_ED25519_CERT, KeyPairProvider.SSH_ED25519)
                    .put(KeyPairProvider.SSH_ECDSA_SHA2_NISTP256_CERT, KeyPairProvider.ECDSA_SHA2_NISTP256)
                    .put(KeyPairProvider.SSH_ECDSA_SHA2_NISTP384_CERT, KeyPairProvider.ECDSA_SHA2_NISTP384)
                    .put(KeyPairProvider.SSH_ECDSA_SHA2_NISTP521_CERT, KeyPairProvider.ECDSA_SHA2_NISTP521)
                    .build();

    static {

        // order matters here, when, for example, SkED25519PublicKeyEntryDecoder is used, it registers the PrivateKey
        // type as java.security.PrivateKey, and all PrivateKey types are assignable to this, so it must be consulted last

        registerPublicKeyEntryDecoder(OpenSSHCertificateDecoder.INSTANCE);
        registerPublicKeyEntryDecoder(RSAPublicKeyDecoder.INSTANCE);
        registerPublicKeyEntryDecoder(DSSPublicKeyEntryDecoder.INSTANCE);

        if (SecurityUtils.isECCSupported()) {
            registerPublicKeyEntryDecoder(ECDSAPublicKeyEntryDecoder.INSTANCE);
        }
        if (SecurityUtils.isEDDSACurveSupported()) {
            registerPublicKeyEntryDecoder(SecurityUtils.getEDDSAPublicKeyEntryDecoder());
        }

        // order matters, these must be last since they register their PrivateKey type as java.security.PrivateKey
        // there is logical code which discovers a decoder type by instance assignability to this registered type

        if (SecurityUtils.isECCSupported()) {
            registerPublicKeyEntryDecoder(SkECDSAPublicKeyEntryDecoder.INSTANCE);
        }
        if (SecurityUtils.isEDDSACurveSupported()) {
            registerPublicKeyEntryDecoder(SkED25519PublicKeyEntryDecoder.INSTANCE);
        }

    }

    private KeyUtils() {
        throw new UnsupportedOperationException("No instance");
    }

    /**
     * 

* Checks if a path has strict permissions *

*
    *
  • *

    * The path may not have {@link PosixFilePermission#OTHERS_EXECUTE} permission *

    *
  • * *
  • *

    * (For {@code Unix}) The path may not have group or others permissions *

    *
  • * *
  • *

    * (For {@code Unix}) If the path is a file, then its folder may not have group or others permissions *

    *
  • * *
  • *

    * The path must be owned by current user. *

    *
  • * *
  • *

    * (For {@code Unix}) The path may be owned by root. *

    *
  • * *
  • *

    * (For {@code Unix}) If the path is a file, then its folder must also have valid owner. *

    *
  • * *
* * @param path The {@link Path} to be checked - ignored if {@code null} or does not exist * @param options The {@link LinkOption}s to use to query the file's permissions * @return The violated permission as {@link SimpleImmutableEntry} where key is a message and value is * the offending object {@link PosixFilePermission} or {@link String} for owner - {@code null} * if no violations detected * @throws IOException If failed to retrieve the permissions * @see #STRICTLY_PROHIBITED_FILE_PERMISSION */ public static SimpleImmutableEntry validateStrictKeyFilePermissions(Path path, LinkOption... options) throws IOException { if ((path == null) || (!Files.exists(path, options))) { return null; } /* * Android permission are not really consistent with standard Linux ones since the device is * a "single" user O/S but with each application being a different "user". We therefore assume * that if the application has access to a file, then it is good enough since there is really * only one user, and we don't have to worry about multi-user access. Furthermore, the SE Linux * security available on Android seems to be enough of a safeguard against inadvertent or even * malicious access. */ if (OsUtils.isAndroid()) { return null; } Collection perms = IoUtils.getPermissions(path, options); if (GenericUtils.isEmpty(perms)) { return null; } if (perms.contains(PosixFilePermission.OTHERS_EXECUTE)) { PosixFilePermission p = PosixFilePermission.OTHERS_EXECUTE; return new SimpleImmutableEntry<>(String.format("Permissions violation (%s)", p), p); } if (OsUtils.isUNIX()) { PosixFilePermission p = IoUtils.validateExcludedPermissions(perms, STRICTLY_PROHIBITED_FILE_PERMISSION); if (p != null) { return new SimpleImmutableEntry<>(String.format("Permissions violation (%s)", p), p); } if (Files.isRegularFile(path, options)) { Path parent = path.getParent(); p = IoUtils.validateExcludedPermissions(IoUtils.getPermissions(parent, options), STRICTLY_PROHIBITED_FILE_PERMISSION); if (p != null) { return new SimpleImmutableEntry<>(String.format("Parent permissions violation (%s)", p), p); } } } String owner = IoUtils.getFileOwner(path, options); if (GenericUtils.isEmpty(owner)) { // we cannot get owner // general issue: jvm does not support permissions // security issue: specific filesystem does not support permissions return null; } String current = OsUtils.getCurrentUser(); Set expected = new HashSet<>(); expected.add(current); if (OsUtils.isUNIX()) { // Windows "Administrator" was considered however in Windows most likely a group is used. expected.add(OsUtils.ROOT_USER); } if (!expected.contains(owner)) { return new SimpleImmutableEntry<>(String.format("Owner violation (%s)", owner), owner); } if (OsUtils.isUNIX()) { if (Files.isRegularFile(path, options)) { String parentOwner = IoUtils.getFileOwner(path.getParent(), options); if ((!GenericUtils.isEmpty(parentOwner)) && (!expected.contains(parentOwner))) { return new SimpleImmutableEntry<>(String.format("Parent owner violation (%s)", parentOwner), parentOwner); } } } return null; } /** * Reads a single {@link PublicKey} from a public key file. * * @param path {@link Path} of the file to read; must not be {@code null} * @return the {@link PublicKey}, may be {@code null} if the file is empty * @throws IOException if the file cannot be read or parsed * @throws GeneralSecurityException if the file contents cannot be read as a single {@link PublicKey} */ public static PublicKey loadPublicKey(Path path) throws IOException, GeneralSecurityException { List keys = AuthorizedKeyEntry.readAuthorizedKeys(Objects.requireNonNull(path)); if (GenericUtils.isEmpty(keys)) { return null; } if (keys.size() > 1) { throw new InvalidKeySpecException("Public key file contains multiple entries: " + path); } return keys.get(0).resolvePublicKey(null, PublicKeyEntryResolver.IGNORING); } /** * @param keyType The key type - {@code OpenSSH} name - e.g., {@code ssh-rsa, ssh-dss} * @param keySize The key size (in bits) * @return A {@link KeyPair} of the specified type and size * @throws GeneralSecurityException If failed to generate the key pair * @see #getPublicKeyEntryDecoder(String) * @see PublicKeyEntryDecoder#generateKeyPair(int) */ public static KeyPair generateKeyPair(String keyType, int keySize) throws GeneralSecurityException { PublicKeyEntryDecoder decoder = getPublicKeyEntryDecoder(keyType); if (decoder == null) { throw new InvalidKeySpecException("No decoder for key type=" + keyType); } return decoder.generateKeyPair(keySize); } /** * Performs a deep-clone of the original {@link KeyPair} - i.e., creates new public/private keys that are * clones of the original one * * @param keyType The key type - {@code OpenSSH} name - e.g., {@code ssh-rsa, ssh-dss} * @param kp The {@link KeyPair} to clone - ignored if {@code null} * @return The cloned instance * @throws GeneralSecurityException If failed to clone the pair */ public static KeyPair cloneKeyPair(String keyType, KeyPair kp) throws GeneralSecurityException { PublicKeyEntryDecoder decoder = getPublicKeyEntryDecoder(keyType); if (decoder == null) { throw new InvalidKeySpecException("No decoder for key type=" + keyType); } return decoder.cloneKeyPair(kp); } /** * @param decoder The decoder to register * @throws IllegalArgumentException if no decoder or not key type or no supported names for the decoder * @see PublicKeyEntryDecoder#getPublicKeyType() * @see PublicKeyEntryDecoder#getSupportedKeyTypes() */ public static void registerPublicKeyEntryDecoder(PublicKeyEntryDecoder decoder) { Objects.requireNonNull(decoder, "No decoder specified"); Class pubType = Objects.requireNonNull(decoder.getPublicKeyType(), "No public key type declared"); Class prvType = Objects.requireNonNull(decoder.getPrivateKeyType(), "No private key type declared"); synchronized (BY_KEY_CLASS_DECODERS_MAP) { BY_KEY_CLASS_DECODERS_MAP.put(pubType, decoder); BY_KEY_CLASS_DECODERS_MAP.put(prvType, decoder); } registerPublicKeyEntryDecoderKeyTypes(decoder); } /** * Registers the specified decoder for all the types it {@link PublicKeyEntryDecoder#getSupportedKeyTypes() * supports} * * @param decoder The (never {@code null}) {@link PublicKeyEntryDecoder decoder} to register * @see #registerPublicKeyEntryDecoderForKeyType(String, PublicKeyEntryDecoder) */ public static void registerPublicKeyEntryDecoderKeyTypes(PublicKeyEntryDecoder decoder) { Objects.requireNonNull(decoder, "No decoder specified"); Collection names = ValidateUtils.checkNotNullAndNotEmpty(decoder.getSupportedKeyTypes(), "No supported key types"); for (String n : names) { PublicKeyEntryDecoder prev = registerPublicKeyEntryDecoderForKeyType(n, decoder); if (prev != null) { // noinspection UnnecessaryContinue continue; // debug breakpoint } } } /** * @param keyType The key (never {@code null}/empty) key type * @param decoder The (never {@code null}) {@link PublicKeyEntryDecoder decoder} to register * @return The previously registered decoder for this key type - {@code null} if none */ public static PublicKeyEntryDecoder registerPublicKeyEntryDecoderForKeyType( String keyType, PublicKeyEntryDecoder decoder) { keyType = ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type specified"); Objects.requireNonNull(decoder, "No decoder specified"); synchronized (BY_KEY_TYPE_DECODERS_MAP) { return BY_KEY_TYPE_DECODERS_MAP.put(keyType, decoder); } } /** * @param decoder The (never {@code null}) {@link PublicKeyEntryDecoder decoder} to unregister * @return The case insensitive {@link NavigableSet} of all the effectively un-registered key types * out of all the {@link PublicKeyEntryDecoder#getSupportedKeyTypes() supported} ones. * @see #unregisterPublicKeyEntryDecoderKeyTypes(PublicKeyEntryDecoder) */ public static NavigableSet unregisterPublicKeyEntryDecoder(PublicKeyEntryDecoder decoder) { Objects.requireNonNull(decoder, "No decoder specified"); Class pubType = Objects.requireNonNull(decoder.getPublicKeyType(), "No public key type declared"); Class prvType = Objects.requireNonNull(decoder.getPrivateKeyType(), "No private key type declared"); synchronized (BY_KEY_CLASS_DECODERS_MAP) { BY_KEY_CLASS_DECODERS_MAP.remove(pubType); BY_KEY_CLASS_DECODERS_MAP.remove(prvType); } return unregisterPublicKeyEntryDecoderKeyTypes(decoder); } /** * Unregisters the specified decoder for all the types it supports * * @param decoder The (never {@code null}) {@link PublicKeyEntryDecoder decoder} to unregister * @return The case insensitive {@link NavigableSet} of all the effectively un-registered key types * out of all the {@link PublicKeyEntryDecoder#getSupportedKeyTypes() supported} ones. * @see #unregisterPublicKeyEntryDecoderForKeyType(String) */ public static NavigableSet unregisterPublicKeyEntryDecoderKeyTypes(PublicKeyEntryDecoder decoder) { Objects.requireNonNull(decoder, "No decoder specified"); Collection names = ValidateUtils.checkNotNullAndNotEmpty( decoder.getSupportedKeyTypes(), "No supported key types"); NavigableSet removed = Collections.emptyNavigableSet(); for (String n : names) { PublicKeyEntryDecoder prev = unregisterPublicKeyEntryDecoderForKeyType(n); if (prev == null) { continue; } if (removed.isEmpty()) { removed = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); } if (!removed.add(n)) { // noinspection UnnecessaryContinue continue; // debug breakpoint } } return removed; } /** * Unregister the decoder registered for the specified key type * * @param keyType The key (never {@code null}/empty) key type * @return The unregistered {@link PublicKeyEntryDecoder} - {@code null} if none registered for this key * type */ public static PublicKeyEntryDecoder unregisterPublicKeyEntryDecoderForKeyType(String keyType) { keyType = ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type specified"); synchronized (BY_KEY_TYPE_DECODERS_MAP) { return BY_KEY_TYPE_DECODERS_MAP.remove(keyType); } } /** * @param keyType The {@code OpenSSH} key type string - e.g., {@code ssh-rsa, ssh-dss} - ignored if * {@code null}/empty * @return The registered {@link PublicKeyEntryDecoder} or {code null} if not found */ public static PublicKeyEntryDecoder getPublicKeyEntryDecoder(String keyType) { if (GenericUtils.isEmpty(keyType)) { return null; } synchronized (BY_KEY_TYPE_DECODERS_MAP) { return BY_KEY_TYPE_DECODERS_MAP.get(keyType); } } /** * @param kp The {@link KeyPair} to examine - ignored if {@code null} * @return The matching {@link PublicKeyEntryDecoder} provided both the public and private keys have the * same decoder - {@code null} if no match found * @see #getPublicKeyEntryDecoder(Key) */ public static PublicKeyEntryDecoder getPublicKeyEntryDecoder(KeyPair kp) { if (kp == null) { return null; } PublicKeyEntryDecoder d1 = getPublicKeyEntryDecoder(kp.getPublic()); PublicKeyEntryDecoder d2 = getPublicKeyEntryDecoder(kp.getPrivate()); if (d1 == d2) { return d1; } else { return null; // some kind of mixed keys... } } /** * @param key The {@link Key} (public or private) - ignored if {@code null} * @return The registered {@link PublicKeyEntryDecoder} for this key or {code null} if no match found * @see #getPublicKeyEntryDecoder(Class) */ public static PublicKeyEntryDecoder getPublicKeyEntryDecoder(Key key) { if (key == null) { return null; } else { return getPublicKeyEntryDecoder(key.getClass()); } } /** * @param keyType The key {@link Class} - ignored if {@code null} or not a {@link Key} compatible type * @return The registered {@link PublicKeyEntryDecoder} or {code null} if no match found */ public static PublicKeyEntryDecoder getPublicKeyEntryDecoder(Class keyType) { if ((keyType == null) || (!Key.class.isAssignableFrom(keyType))) { return null; } synchronized (BY_KEY_TYPE_DECODERS_MAP) { PublicKeyEntryDecoder decoder = BY_KEY_CLASS_DECODERS_MAP.get(keyType); if (decoder != null) { return decoder; } // in case it is a derived class for (PublicKeyEntryDecoder dec : BY_KEY_CLASS_DECODERS_MAP.values()) { Class pubType = dec.getPublicKeyType(); Class prvType = dec.getPrivateKeyType(); if (pubType.isAssignableFrom(keyType) || prvType.isAssignableFrom(keyType)) { return dec; } } } return null; } /** * @return The default {@link DigestFactory} by the {@link #getFingerPrint(PublicKey)} and * {@link #getFingerPrint(String)} methods * @see #KEY_FINGERPRINT_FACTORY_PROP * @see #setDefaultFingerPrintFactory(DigestFactory) */ public static DigestFactory getDefaultFingerPrintFactory() { DigestFactory factory = null; synchronized (DEFAULT_DIGEST_HOLDER) { factory = DEFAULT_DIGEST_HOLDER.get(); if (factory != null) { return factory; } String propVal = System.getProperty(KEY_FINGERPRINT_FACTORY_PROP); if (GenericUtils.isEmpty(propVal)) { factory = DEFAULT_FINGERPRINT_DIGEST_FACTORY; } else { factory = ValidateUtils.checkNotNull(BuiltinDigests.fromFactoryName(propVal), "Unknown digest factory: %s", propVal); } ValidateUtils.checkTrue(factory.isSupported(), "Selected fingerprint digest not supported: %s", factory.getName()); DEFAULT_DIGEST_HOLDER.set(factory); } return factory; } /** * @param f The {@link DigestFactory} of {@link Digest}s to be used - may not be {@code null} */ public static void setDefaultFingerPrintFactory(DigestFactory f) { synchronized (DEFAULT_DIGEST_HOLDER) { DEFAULT_DIGEST_HOLDER.set(Objects.requireNonNull(f, "No digest factory")); } } /** * @param key the public key - ignored if {@code null} * @return the fingerprint or {@code null} if no key. Note: if exception encountered then returns the * exception's simple class name * @see #getFingerPrint(Factory, PublicKey) */ public static String getFingerPrint(PublicKey key) { return getFingerPrint(getDefaultFingerPrintFactory(), key); } /** * @param password The {@link String} to digest - ignored if {@code null}/empty, otherwise its UTF-8 representation * is used as input for the fingerprint * @return The fingerprint - {@code null} if {@code null}/empty input. Note: if exception * encountered then returns the exception's simple class name * @see #getFingerPrint(String, Charset) */ public static String getFingerPrint(String password) { return getFingerPrint(password, StandardCharsets.UTF_8); } /** * @param password The {@link String} to digest - ignored if {@code null}/empty * @param charset The {@link Charset} to use in order to convert the string to its byte representation to use as * input for the fingerprint * @return The fingerprint - {@code null} if {@code null}/empty input. Note: if exception * encountered then returns the exception's simple class name * @see #getFingerPrint(Factory, String, Charset) * @see #getDefaultFingerPrintFactory() */ public static String getFingerPrint(String password, Charset charset) { return getFingerPrint(getDefaultFingerPrintFactory(), password, charset); } /** * @param f The {@link Factory} to create the {@link Digest} to use * @param key the public key - ignored if {@code null} * @return the fingerprint or {@code null} if no key. Note: if exception encountered then returns the * exception's simple class name * @see #getFingerPrint(Digest, PublicKey) */ public static String getFingerPrint(Factory f, PublicKey key) { return (key == null) ? null : getFingerPrint(Objects.requireNonNull(f, "No digest factory").create(), key); } /** * @param d The {@link Digest} to use * @param key the public key - ignored if {@code null} * @return the fingerprint or {@code null} if no key. Note: if exception encountered then returns the * exception's simple class name * @see DigestUtils#getFingerPrint(Digest, byte[], int, int) */ public static String getFingerPrint(Digest d, PublicKey key) { if (key == null) { return null; } try { Buffer buffer = new ByteArrayBuffer(); buffer.putRawPublicKey(key); return DigestUtils.getFingerPrint(d, buffer.array(), 0, buffer.wpos()); } catch (Exception e) { return e.toString(); } } public static byte[] getRawFingerprint(PublicKey key) throws Exception { return getRawFingerprint(getDefaultFingerPrintFactory(), key); } public static byte[] getRawFingerprint(Factory f, PublicKey key) throws Exception { return (key == null) ? null : getRawFingerprint(Objects.requireNonNull(f, "No digest factory").create(), key); } public static byte[] getRawFingerprint(Digest d, PublicKey key) throws Exception { if (key == null) { return null; } Buffer buffer = new ByteArrayBuffer(); buffer.putRawPublicKey(key); return DigestUtils.getRawFingerprint(d, buffer.array(), 0, buffer.wpos()); } /** * @param f The {@link Factory} to create the {@link Digest} to use * @param s The {@link String} to digest - ignored if {@code null}/empty, otherwise its UTF-8 representation is * used as input for the fingerprint * @return The fingerprint - {@code null} if {@code null}/empty input. Note: if exception encountered then * returns the exception's simple class name * @see #getFingerPrint(Digest, String, Charset) */ public static String getFingerPrint(Factory f, String s) { return getFingerPrint(f, s, StandardCharsets.UTF_8); } /** * @param f The {@link Factory} to create the {@link Digest} to use * @param s The {@link String} to digest - ignored if {@code null}/empty * @param charset The {@link Charset} to use in order to convert the string to its byte representation to use as * input for the fingerprint * @return The fingerprint - {@code null} if {@code null}/empty input Note: if exception encountered * then returns the exception's simple class name * @see DigestUtils#getFingerPrint(Digest, String, Charset) */ public static String getFingerPrint(Factory f, String s, Charset charset) { return getFingerPrint(f.create(), s, charset); } /** * @param d The {@link Digest} to use * @param s The {@link String} to digest - ignored if {@code null}/empty, otherwise its UTF-8 representation is * used as input for the fingerprint * @return The fingerprint - {@code null} if {@code null}/empty input. Note: if exception encountered then * returns the exception's simple class name * @see DigestUtils#getFingerPrint(Digest, String, Charset) */ public static String getFingerPrint(Digest d, String s) { return getFingerPrint(d, s, StandardCharsets.UTF_8); } /** * @param d The {@link Digest} to use to calculate the fingerprint * @param s The string to digest - ignored if {@code null}/empty * @param charset The {@link Charset} to use in order to convert the string to its byte representation to use as * input for the fingerprint * @return The fingerprint - {@code null} if {@code null}/empty input. Note: if exception encountered * then returns the exception's simple class name * @see DigestUtils#getFingerPrint(Digest, String, Charset) */ public static String getFingerPrint(Digest d, String s, Charset charset) { if (GenericUtils.isEmpty(s)) { return null; } try { return DigestUtils.getFingerPrint(d, s, charset); } catch (Exception e) { return e.getClass().getSimpleName(); } } /** * @param expected The expected fingerprint if {@code null} or empty then returns a failure with the default * fingerprint. * @param key the {@link PublicKey} - if {@code null} then returns null. * @return SimpleImmutableEntry - key is success indicator, value is actual fingerprint, * {@code null} if no key. * @see #getDefaultFingerPrintFactory() * @see #checkFingerPrint(String, Factory, PublicKey) */ public static SimpleImmutableEntry checkFingerPrint(String expected, PublicKey key) { return checkFingerPrint(expected, getDefaultFingerPrintFactory(), key); } /** * @param expected The expected fingerprint if {@code null} or empty then returns a failure with the default * fingerprint. * @param f The {@link Factory} to be used to generate the default {@link Digest} for the key * @param key the {@link PublicKey} - if {@code null} then returns null. * @return SimpleImmutableEntry - key is success indicator, value is actual fingerprint, * {@code null} if no key. */ public static SimpleImmutableEntry checkFingerPrint( String expected, Factory f, PublicKey key) { return checkFingerPrint(expected, Objects.requireNonNull(f, "No digest factory").create(), key); } /** * @param expected The expected fingerprint if {@code null} or empty then returns a failure with the default * fingerprint. * @param d The {@link Digest} to be used to generate the default fingerprint for the key * @param key the {@link PublicKey} - if {@code null} then returns null. * @return SimpleImmutableEntry - key is success indicator, value is actual fingerprint, * {@code null} if no key. */ public static SimpleImmutableEntry checkFingerPrint(String expected, Digest d, PublicKey key) { if (key == null) { return null; } if (GenericUtils.isEmpty(expected)) { return new SimpleImmutableEntry<>(false, getFingerPrint(d, key)); } // de-construct fingerprint int pos = expected.indexOf(':'); if ((pos < 0) || (pos >= (expected.length() - 1))) { return new SimpleImmutableEntry<>(false, getFingerPrint(d, key)); } String name = expected.substring(0, pos); String value = expected.substring(pos + 1); DigestFactory expectedFactory; // We know that all digest names have a length > 2 - if 2 (or less) then assume a pure HEX value if (name.length() > 2) { expectedFactory = BuiltinDigests.fromFactoryName(name); if (expectedFactory == null) { return new SimpleImmutableEntry<>(false, getFingerPrint(d, key)); } expected = name.toUpperCase() + ":" + value; } else { expectedFactory = BuiltinDigests.md5; expected = expectedFactory.getName().toUpperCase() + ":" + expected; } String fingerprint = getFingerPrint(expectedFactory, key); boolean matches = BuiltinDigests.md5.getName().equals(expectedFactory.getName()) ? expected.equalsIgnoreCase(fingerprint) // HEX is case insensitive : expected.equals(fingerprint); return new SimpleImmutableEntry<>(matches, fingerprint); } /** * @param kp a key pair - ignored if {@code null}. If the private key is non-{@code null} then it is used to * determine the type, otherwise the public one is used. * @return the key type or {@code null} if cannot determine it * @see #getKeyType(Key) */ public static String getKeyType(KeyPair kp) { if (kp == null) { return null; } PrivateKey key = kp.getPrivate(); if (key != null) { return getKeyType(key); } else { return getKeyType(kp.getPublic()); } } /** * @param key a public or private key * @return the key type or {@code null} if cannot determine it */ public static String getKeyType(Key key) { if (key == null) { return null; } else if (key instanceof DSAKey) { return KeyPairProvider.SSH_DSS; } else if (key instanceof RSAKey) { return KeyPairProvider.SSH_RSA; } else if (key instanceof ECKey) { ECKey ecKey = (ECKey) key; ECParameterSpec ecSpec = ecKey.getParams(); ECCurves curve = ECCurves.fromCurveParameters(ecSpec); if (curve == null) { return null; // debug breakpoint } else { return curve.getKeyType(); } } else if (key instanceof SkEcdsaPublicKey) { return SkECDSAPublicKeyEntryDecoder.KEY_TYPE; } else if (SecurityUtils.EDDSA.equalsIgnoreCase(key.getAlgorithm())) { return KeyPairProvider.SSH_ED25519; } else if (key instanceof SkED25519PublicKey) { return SkED25519PublicKeyEntryDecoder.KEY_TYPE; } else if (key instanceof OpenSshCertificate) { return ((OpenSshCertificate) key).getKeyType(); } return null; } /** * @param keyType A key type name - ignored if {@code null}/empty * @return A {@link List} of they canonical key name and all its aliases * @see #getCanonicalKeyType(String) */ public static List getAllEquivalentKeyTypes(String keyType) { if (GenericUtils.isEmpty(keyType)) { return Collections.emptyList(); } String canonicalName = getCanonicalKeyType(keyType); List equivalents = new ArrayList<>(); equivalents.add(canonicalName); synchronized (KEY_TYPE_ALIASES) { for (Map.Entry ae : KEY_TYPE_ALIASES.entrySet()) { String alias = ae.getKey(); String name = ae.getValue(); if (canonicalName.equalsIgnoreCase(name)) { equivalents.add(alias); } } } return equivalents; } /** * @param keyType The available key-type - ignored if {@code null}/empty * @return The canonical key type - same as input if no alias registered for the provided key type * @see #RSA_SHA256_KEY_TYPE_ALIAS * @see #RSA_SHA512_KEY_TYPE_ALIAS */ public static String getCanonicalKeyType(String keyType) { if (GenericUtils.isEmpty(keyType)) { return keyType; } String canonicalName; synchronized (KEY_TYPE_ALIASES) { canonicalName = KEY_TYPE_ALIASES.get(keyType); } if (GenericUtils.isEmpty(canonicalName)) { return keyType; } return canonicalName; } /** * @return A case insensitive {@link NavigableSet} of the currently registered key type "aliases". * @see #getCanonicalKeyType(String) */ public static NavigableSet getRegisteredKeyTypeAliases() { synchronized (KEY_TYPE_ALIASES) { return KEY_TYPE_ALIASES.isEmpty() ? Collections.emptyNavigableSet() : GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, KEY_TYPE_ALIASES.keySet()); } } /** * Registers a collection of aliases to a canonical key type * * @param keyType The (never {@code null}/empty) canonical name * @param aliases The (never {@code null}/empty) aliases * @return A {@link List} of the replaced aliases - empty if no previous aliases for the canonical name */ public static List registerCanonicalKeyTypes(String keyType, Collection aliases) { ValidateUtils.checkNotNullAndNotEmpty(keyType, "No key type value"); ValidateUtils.checkNotNullAndNotEmpty(aliases, "No aliases provided"); List replaced = Collections.emptyList(); synchronized (KEY_TYPE_ALIASES) { for (String a : aliases) { ValidateUtils.checkNotNullAndNotEmpty(a, "Null/empty alias registration for %s", keyType); String prev = KEY_TYPE_ALIASES.put(a, keyType); if (GenericUtils.isEmpty(prev)) { continue; } if (replaced.isEmpty()) { replaced = new ArrayList<>(); } replaced.add(prev); } } return replaced; } /** * @param alias The alias to unregister (ignored if {@code null}/empty) * @return The associated canonical key type - {@code null} if alias not registered */ public static String unregisterCanonicalKeyTypeAlias(String alias) { if (GenericUtils.isEmpty(alias)) { return alias; } synchronized (KEY_TYPE_ALIASES) { return KEY_TYPE_ALIASES.remove(alias); } } /** * Determines the key size in bits * * @param key The {@link Key} to examine - ignored if {@code null} * @return The key size - non-positive value if cannot determine it */ public static int getKeySize(Key key) { if (key == null) { return -1; } else if (key instanceof RSAKey) { BigInteger n = ((RSAKey) key).getModulus(); return n.bitLength(); } else if (key instanceof DSAKey) { DSAParams params = ((DSAKey) key).getParams(); BigInteger p = params.getP(); return p.bitLength(); } else if (key instanceof ECKey) { ECParameterSpec ecSpec = ((ECKey) key).getParams(); ECCurves curve = ECCurves.fromCurveParameters(ecSpec); if (curve != null) { return curve.getKeySize(); } } else if (SecurityUtils.EDDSA.equalsIgnoreCase(key.getAlgorithm())) { return SecurityUtils.getEDDSAKeySize(key); } return -1; } /** * @param key The {@link PublicKey} to be checked - ignored if {@code null} * @param keySet The keys to be searched - ignored if {@code null}/empty * @return The matching {@link PublicKey} from the keys or {@code null} if no match found * @see #compareKeys(PublicKey, PublicKey) */ public static PublicKey findMatchingKey(PublicKey key, PublicKey... keySet) { if (key == null || GenericUtils.isEmpty(keySet)) { return null; } else { return findMatchingKey(key, Arrays.asList(keySet)); } } /** * @param key The {@link PublicKey} to be checked - ignored if {@code null} * @param keySet The keys to be searched - ignored if {@code null}/empty * @return The matching {@link PublicKey} from the keys or {@code null} if no match found * @see #compareKeys(PublicKey, PublicKey) */ public static PublicKey findMatchingKey(PublicKey key, Collection keySet) { if (key == null || GenericUtils.isEmpty(keySet)) { return null; } for (PublicKey k : keySet) { if (compareKeys(key, k)) { return k; } } return null; } public static boolean compareKeyPairs(KeyPair k1, KeyPair k2) { if (Objects.equals(k1, k2)) { return true; } else if ((k1 == null) || (k2 == null)) { return false; // both null is covered by Objects#equals } else { return compareKeys(k1.getPublic(), k2.getPublic()) && compareKeys(k1.getPrivate(), k2.getPrivate()); } } public static boolean compareKeys(PublicKey k1, PublicKey k2) { if ((k1 instanceof RSAPublicKey) && (k2 instanceof RSAPublicKey)) { return compareRSAKeys(RSAPublicKey.class.cast(k1), RSAPublicKey.class.cast(k2)); } else if ((k1 instanceof DSAPublicKey) && (k2 instanceof DSAPublicKey)) { return compareDSAKeys(DSAPublicKey.class.cast(k1), DSAPublicKey.class.cast(k2)); } else if ((k1 instanceof ECPublicKey) && (k2 instanceof ECPublicKey)) { return compareECKeys(ECPublicKey.class.cast(k1), ECPublicKey.class.cast(k2)); } else if ((k1 instanceof SkEcdsaPublicKey) && (k2 instanceof SkEcdsaPublicKey)) { return compareSkEcdsaKeys(SkEcdsaPublicKey.class.cast(k1), SkEcdsaPublicKey.class.cast(k2)); } else if ((k1 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k1.getAlgorithm()) && (k2 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k2.getAlgorithm())) { return SecurityUtils.compareEDDSAPPublicKeys(k1, k2); } else if ((k1 instanceof SkED25519PublicKey) && (k2 instanceof SkED25519PublicKey)) { return compareSkEd25519Keys(SkED25519PublicKey.class.cast(k1), SkED25519PublicKey.class.cast(k2)); } else if ((k1 instanceof OpenSshCertificate) && (k2 instanceof OpenSshCertificate)) { return compareOpenSSHCertificateKeys(OpenSshCertificate.class.cast(k1), OpenSshCertificate.class.cast(k2)); } else { return false; // either key is null or not of same class } } public static PublicKey recoverPublicKey(PrivateKey key) throws GeneralSecurityException { if (key instanceof RSAPrivateKey) { return recoverRSAPublicKey((RSAPrivateKey) key); } else if (key instanceof DSAPrivateKey) { return recoverDSAPublicKey((DSAPrivateKey) key); } else if ((key != null) && SecurityUtils.EDDSA.equalsIgnoreCase(key.getAlgorithm())) { return SecurityUtils.recoverEDDSAPublicKey(key); } else { return null; } } public static boolean compareKeys(PrivateKey k1, PrivateKey k2) { if ((k1 instanceof RSAPrivateKey) && (k2 instanceof RSAPrivateKey)) { return compareRSAKeys(RSAPrivateKey.class.cast(k1), RSAPrivateKey.class.cast(k2)); } else if ((k1 instanceof DSAPrivateKey) && (k2 instanceof DSAPrivateKey)) { return compareDSAKeys(DSAPrivateKey.class.cast(k1), DSAPrivateKey.class.cast(k2)); } else if ((k1 instanceof ECPrivateKey) && (k2 instanceof ECPrivateKey)) { return compareECKeys(ECPrivateKey.class.cast(k1), ECPrivateKey.class.cast(k2)); } else if ((k1 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k1.getAlgorithm()) && (k2 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k2.getAlgorithm())) { return SecurityUtils.compareEDDSAPrivateKeys(k1, k2); } else { return false; // either key is null or not of same class } } public static boolean compareRSAKeys(RSAPublicKey k1, RSAPublicKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { return Objects.equals(k1.getPublicExponent(), k2.getPublicExponent()) && Objects.equals(k1.getModulus(), k2.getModulus()); } } public static boolean compareRSAKeys(RSAPrivateKey k1, RSAPrivateKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { return Objects.equals(k1.getModulus(), k2.getModulus()) && Objects.equals(k1.getPrivateExponent(), k2.getPrivateExponent()); } } public static boolean compareOpenSSHCertificateKeys(OpenSshCertificate k1, OpenSshCertificate k2) { if (k1 == k2) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered above } else { return Objects.equals(k1.getSerial(), k2.getSerial()) && Arrays.equals(k1.getSignature(), k2.getSignature()) && compareKeys(k1.getCertPubKey(), k2.getCertPubKey()); } } public static RSAPublicKey recoverRSAPublicKey(RSAPrivateKey privateKey) throws GeneralSecurityException { if (privateKey instanceof RSAPrivateCrtKey) { return recoverFromRSAPrivateCrtKey((RSAPrivateCrtKey) privateKey); } else { // Not ideal, but best we can do under the circumstances return recoverRSAPublicKey(privateKey.getModulus(), DEFAULT_RSA_PUBLIC_EXPONENT); } } public static RSAPublicKey recoverFromRSAPrivateCrtKey(RSAPrivateCrtKey rsaKey) throws GeneralSecurityException { return recoverRSAPublicKey(rsaKey.getPrimeP(), rsaKey.getPrimeQ(), rsaKey.getPublicExponent()); } public static RSAPublicKey recoverRSAPublicKey(BigInteger p, BigInteger q, BigInteger publicExponent) throws GeneralSecurityException { return recoverRSAPublicKey(p.multiply(q), publicExponent); } public static RSAPublicKey recoverRSAPublicKey(BigInteger modulus, BigInteger publicExponent) throws GeneralSecurityException { KeyFactory kf = SecurityUtils.getKeyFactory(RSA_ALGORITHM); return (RSAPublicKey) kf.generatePublic(new RSAPublicKeySpec(modulus, publicExponent)); } public static boolean compareDSAKeys(DSAPublicKey k1, DSAPublicKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { return Objects.equals(k1.getY(), k2.getY()) && compareDSAParams(k1.getParams(), k2.getParams()); } } public static boolean compareDSAKeys(DSAPrivateKey k1, DSAPrivateKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { return Objects.equals(k1.getX(), k2.getX()) && compareDSAParams(k1.getParams(), k2.getParams()); } } public static boolean compareDSAParams(DSAParams p1, DSAParams p2) { if (Objects.equals(p1, p2)) { return true; } else if (p1 == null || p2 == null) { return false; // both null is covered by Objects#equals } else { return Objects.equals(p1.getG(), p2.getG()) && Objects.equals(p1.getP(), p2.getP()) && Objects.equals(p1.getQ(), p2.getQ()); } } // based on code from // https://github.com/alexo/SAML-2.0/blob/master/java-opensaml/opensaml-security-api/src/main/java/org/opensaml/xml/security/SecurityHelper.java public static DSAPublicKey recoverDSAPublicKey(DSAPrivateKey privateKey) throws GeneralSecurityException { DSAParams keyParams = privateKey.getParams(); BigInteger p = keyParams.getP(); BigInteger x = privateKey.getX(); BigInteger q = keyParams.getQ(); BigInteger g = keyParams.getG(); BigInteger y = g.modPow(x, p); KeyFactory kf = SecurityUtils.getKeyFactory(DSS_ALGORITHM); return (DSAPublicKey) kf.generatePublic(new DSAPublicKeySpec(y, p, q, g)); } public static boolean compareECKeys(ECPrivateKey k1, ECPrivateKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { return Objects.equals(k1.getS(), k2.getS()) && compareECParams(k1.getParams(), k2.getParams()); } } public static boolean compareECKeys(ECPublicKey k1, ECPublicKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { return Objects.equals(k1.getW(), k2.getW()) && compareECParams(k1.getParams(), k2.getParams()); } } public static boolean compareECParams(ECParameterSpec s1, ECParameterSpec s2) { if (Objects.equals(s1, s2)) { return true; } else if (s1 == null || s2 == null) { return false; // both null is covered by Objects#equals } else { return Objects.equals(s1.getOrder(), s2.getOrder()) && (s1.getCofactor() == s2.getCofactor()) && Objects.equals(s1.getGenerator(), s2.getGenerator()) && Objects.equals(s1.getCurve(), s2.getCurve()); } } public static boolean compareSkEcdsaKeys(SkEcdsaPublicKey k1, SkEcdsaPublicKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { return Objects.equals(k1.getAppName(), k2.getAppName()) && Objects.equals(k1.isNoTouchRequired(), k2.isNoTouchRequired()) && compareECKeys(k1.getDelegatePublicKey(), k2.getDelegatePublicKey()); } } public static boolean compareSkEd25519Keys(SkED25519PublicKey k1, SkED25519PublicKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { return Objects.equals(k1.getAppName(), k2.getAppName()) && Objects.equals(k1.isNoTouchRequired(), k2.isNoTouchRequired()) && SecurityUtils.compareEDDSAPPublicKeys(k1.getDelegatePublicKey(), k2.getDelegatePublicKey()); } } public static String getSignatureAlgorithm(String chosenAlgorithm, PublicKey key) { // check key as we know only certificates require a mapped signature algorithm currently if (key instanceof OpenSshCertificate) { synchronized (SIGNATURE_ALGORITHM_MAP) { return SIGNATURE_ALGORITHM_MAP.get(chosenAlgorithm); } } else { return chosenAlgorithm; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy