001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.eclipse.aether.repository;
020
021import java.nio.charset.StandardCharsets;
022import java.security.MessageDigest;
023import java.security.NoSuchAlgorithmException;
024
025import org.eclipse.aether.RepositorySystemSession;
026
027/**
028 * A helper to calculate a fingerprint/digest for the authentication data of a repository/proxy. Such a fingerprint can
029 * be used to detect changes in the authentication data across JVM restarts without exposing sensitive information.
030 */
031public final class AuthenticationDigest {
032
033    private final MessageDigest digest;
034
035    private final RepositorySystemSession session;
036
037    private final RemoteRepository repository;
038
039    private final Proxy proxy;
040
041    /**
042     * Gets the fingerprint for the authentication of the specified repository.
043     *
044     * @param session The repository system session during which the fingerprint is requested, must not be {@code null}.
045     * @param repository The repository whose authentication is to be fingerprinted, must not be {@code null}.
046     * @return The fingerprint of the repository authentication or an empty string if no authentication is configured,
047     *         never {@code null}.
048     */
049    public static String forRepository(RepositorySystemSession session, RemoteRepository repository) {
050        String digest = "";
051        Authentication auth = repository.getAuthentication();
052        if (auth != null) {
053            AuthenticationDigest authDigest = new AuthenticationDigest(session, repository, null);
054            auth.digest(authDigest);
055            digest = authDigest.digest();
056        }
057        return digest;
058    }
059
060    /**
061     * Gets the fingerprint for the authentication of the specified repository's proxy.
062     *
063     * @param session The repository system session during which the fingerprint is requested, must not be {@code null}.
064     * @param repository The repository whose proxy authentication is to be fingerprinted, must not be {@code null}.
065     * @return The fingerprint of the proxy authentication or an empty string if no proxy is present or if no proxy
066     *         authentication is configured, never {@code null}.
067     */
068    public static String forProxy(RepositorySystemSession session, RemoteRepository repository) {
069        String digest = "";
070        Proxy proxy = repository.getProxy();
071        if (proxy != null) {
072            Authentication auth = proxy.getAuthentication();
073            if (auth != null) {
074                AuthenticationDigest authDigest = new AuthenticationDigest(session, repository, proxy);
075                auth.digest(authDigest);
076                digest = authDigest.digest();
077            }
078        }
079        return digest;
080    }
081
082    private AuthenticationDigest(RepositorySystemSession session, RemoteRepository repository, Proxy proxy) {
083        this.session = session;
084        this.repository = repository;
085        this.proxy = proxy;
086        digest = newDigest();
087    }
088
089    private static MessageDigest newDigest() {
090        try {
091            return MessageDigest.getInstance("SHA-1");
092        } catch (NoSuchAlgorithmException e) {
093            try {
094                return MessageDigest.getInstance("MD5");
095            } catch (NoSuchAlgorithmException ne) {
096                throw new IllegalStateException(ne);
097            }
098        }
099    }
100
101    /**
102     * Gets the repository system session during which the authentication fingerprint is calculated.
103     *
104     * @return The repository system session, never {@code null}.
105     */
106    public RepositorySystemSession getSession() {
107        return session;
108    }
109
110    /**
111     * Gets the repository requiring authentication. If {@link #getProxy()} is not {@code null}, the data gathered by
112     * this authentication digest does not apply to the repository's host but rather the proxy.
113     *
114     * @return The repository to be contacted, never {@code null}.
115     */
116    public RemoteRepository getRepository() {
117        return repository;
118    }
119
120    /**
121     * Gets the proxy (if any) to be authenticated with.
122     *
123     * @return The proxy or {@code null} if authenticating directly with the repository's host.
124     */
125    public Proxy getProxy() {
126        return proxy;
127    }
128
129    /**
130     * Updates the digest with the specified strings.
131     *
132     * @param strings The strings to update the digest with, may be {@code null} or contain {@code null} elements.
133     */
134    public void update(String... strings) {
135        if (strings != null) {
136            for (String string : strings) {
137                if (string != null) {
138                    digest.update(string.getBytes(StandardCharsets.UTF_8));
139                }
140            }
141        }
142    }
143
144    /**
145     * Updates the digest with the specified characters.
146     *
147     * @param chars The characters to update the digest with, may be {@code null}.
148     */
149    @SuppressWarnings("checkstyle:magicnumber")
150    public void update(char... chars) {
151        if (chars != null) {
152            for (char c : chars) {
153                digest.update((byte) (c >> 8));
154                digest.update((byte) (c & 0xFF));
155            }
156        }
157    }
158
159    /**
160     * Updates the digest with the specified bytes.
161     *
162     * @param bytes The bytes to update the digest with, may be {@code null}.
163     */
164    public void update(byte... bytes) {
165        if (bytes != null) {
166            digest.update(bytes);
167        }
168    }
169
170    @SuppressWarnings("checkstyle:magicnumber")
171    private String digest() {
172        byte[] bytes = digest.digest();
173        StringBuilder buffer = new StringBuilder(bytes.length * 2);
174        for (byte aByte : bytes) {
175            int b = aByte & 0xFF;
176            if (b < 0x10) {
177                buffer.append('0');
178            }
179            buffer.append(Integer.toHexString(b));
180        }
181        return buffer.toString();
182    }
183}