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