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-256" );
102        }
103        catch ( NoSuchAlgorithmException e )
104        {
105            throw new IllegalStateException( e );
106        }
107    }
108
109    /**
110     * Gets the repository system session during which the authentication fingerprint is calculated.
111     * 
112     * @return The repository system session, never {@code null}.
113     */
114    public RepositorySystemSession getSession()
115    {
116        return session;
117    }
118
119    /**
120     * Gets the repository requiring authentication. If {@link #getProxy()} is not {@code null}, the data gathered by
121     * this authentication digest does not apply to the repository's host but rather the proxy.
122     * 
123     * @return The repository to be contacted, never {@code null}.
124     */
125    public RemoteRepository getRepository()
126    {
127        return repository;
128    }
129
130    /**
131     * Gets the proxy (if any) to be authenticated with.
132     * 
133     * @return The proxy or {@code null} if authenticating directly with the repository's host.
134     */
135    public Proxy getProxy()
136    {
137        return proxy;
138    }
139
140    /**
141     * Updates the digest with the specified strings.
142     * 
143     * @param strings The strings to update the digest with, may be {@code null} or contain {@code null} elements.
144     */
145    public void update( String... strings )
146    {
147        if ( strings != null )
148        {
149            for ( String string : strings )
150            {
151                if ( string != null )
152                {
153                    digest.update( string.getBytes( StandardCharsets.UTF_8 ) );
154                }
155            }
156        }
157    }
158
159    /**
160     * Updates the digest with the specified characters.
161     * 
162     * @param chars The characters to update the digest with, may be {@code null}.
163     */
164    @SuppressWarnings( "checkstyle:magicnumber" )
165    public void update( char... chars )
166    {
167        if ( chars != null )
168        {
169            for ( char c : chars )
170            {
171                digest.update( (byte) ( c >> 8 ) );
172                digest.update( (byte) ( c & 0xFF ) );
173            }
174        }
175    }
176
177    /**
178     * Updates the digest with the specified bytes.
179     * 
180     * @param bytes The bytes to update the digest with, may be {@code null}.
181     */
182    public void update( byte... bytes )
183    {
184        if ( bytes != null )
185        {
186            digest.update( bytes );
187        }
188    }
189
190    @SuppressWarnings( "checkstyle:magicnumber" )
191    private String digest()
192    {
193        byte[] bytes = digest.digest();
194        StringBuilder buffer = new StringBuilder( bytes.length * 2 );
195        for ( byte aByte : bytes )
196        {
197            int b = aByte & 0xFF;
198            if ( b < 0x10 )
199            {
200                buffer.append( '0' );
201            }
202            buffer.append( Integer.toHexString( b ) );
203        }
204        return buffer.toString();
205    }
206
207}