001    /*
002     * Cumulus4j - Securing your data in the cloud - http://cumulus4j.org
003     * Copyright (C) 2011 NightLabs Consulting GmbH
004     *
005     * This program is free software: you can redistribute it and/or modify
006     * it under the terms of the GNU Affero General Public License as
007     * published by the Free Software Foundation, either version 3 of the
008     * License, or (at your option) any later version.
009     *
010     * This program is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU Affero General Public License for more details.
014     *
015     * You should have received a copy of the GNU Affero General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    package org.cumulus4j.keymanager.front.webapp;
019    
020    import java.io.ByteArrayInputStream;
021    import java.io.IOException;
022    import java.io.InputStreamReader;
023    import java.io.Reader;
024    import java.nio.CharBuffer;
025    import java.util.Arrays;
026    
027    import javax.servlet.http.HttpServletRequest;
028    import javax.ws.rs.WebApplicationException;
029    import javax.ws.rs.core.Context;
030    import javax.ws.rs.core.Response;
031    import javax.ws.rs.core.Response.Status;
032    
033    import org.cumulus4j.keymanager.front.shared.Error;
034    import org.cumulus4j.keystore.AuthenticationException;
035    import org.cumulus4j.keystore.KeyNotFoundException;
036    import org.cumulus4j.keystore.KeyStore;
037    import org.slf4j.Logger;
038    import org.slf4j.LoggerFactory;
039    
040    import com.sun.jersey.core.util.Base64;
041    
042    /**
043     * Abstract base class for all REST services of the key-server.
044     *
045     * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
046     */
047    public abstract class AbstractService
048    {
049            private static final Logger logger = LoggerFactory.getLogger(AbstractService.class);
050    
051            protected @Context HttpServletRequest request;
052    
053    //      protected @Context KeyStore keyStore;
054    
055            protected @Context KeyStoreManager keyStoreManager;
056    
057            /**
058             * Get the authentication information. This method does <b>not</b> verify, if the given authentication information
059             * is correct! It merely checks, if the client sent a 'Basic' authentication header. If it did not,
060             * this method throws a {@link WebApplicationException} with {@link Status#UNAUTHORIZED} or {@link Status#FORBIDDEN}.
061             * If it did, it extracts the information and puts it into an {@link Auth} instance.
062             * @return the {@link Auth} instance extracted from the client's headers. Never <code>null</code>.
063             * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header;
064             * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest'
065             * are not supported).
066             */
067            protected Auth getAuth()
068            throws WebApplicationException
069            {
070                    String authorizationHeader = request.getHeader("Authorization");
071                    if (authorizationHeader == null || authorizationHeader.isEmpty()) {
072                            logger.debug("getAuth: There is no 'Authorization' header. Replying with a Status.UNAUTHORIZED response asking for 'Basic' authentication.");
073    
074                            throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).header("WWW-Authenticate", "Basic realm=\"Cumulus4jKeyServer\"").build());
075                    }
076    
077                    logger.debug("getAuth: 'Authorization' header: {}", authorizationHeader);
078    
079                    if (!authorizationHeader.startsWith("Basic"))
080                            throw new WebApplicationException(Response.status(Status.FORBIDDEN).entity(new Error("Only 'Basic' authentication is supported!")).build());
081    
082                    String basicAuthEncoded = authorizationHeader.substring("Basic".length()).trim();
083                    byte[] basicAuthDecodedBA = Base64.decode(basicAuthEncoded);
084                    StringBuilder userNameSB = new StringBuilder();
085                    char[] password = null;
086    
087                    ByteArrayInputStream in = new ByteArrayInputStream(basicAuthDecodedBA);
088                    CharBuffer cb = CharBuffer.allocate(basicAuthDecodedBA.length + 1);
089                    try {
090                            Reader r = new InputStreamReader(in, "UTF-8");
091                            int charsReadTotal = 0;
092                            int charsRead;
093                            do {
094                                    charsRead = r.read(cb);
095    
096                                    if (charsRead > 0)
097                                            charsReadTotal += charsRead;
098                            } while (charsRead >= 0);
099                            cb.position(0);
100    
101                            while (cb.position() < charsReadTotal) {
102                                    char c = cb.get();
103                                    if (c == ':')
104                                            break;
105    
106                                    userNameSB.append(c);
107                            }
108    
109                            if (cb.position() < charsReadTotal) {
110                                    password = new char[charsReadTotal - cb.position()];
111                                    int idx = 0;
112                                    while (cb.position() < charsReadTotal)
113                                            password[idx++] = cb.get();
114                            }
115                    } catch (Exception e) {
116                            throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).entity(new Error(e)).build());
117                    } finally {
118                            // For extra safety: Overwrite all sensitive memory with 0.
119                            Arrays.fill(basicAuthDecodedBA, (byte)0);
120    
121                            cb.position(0);
122                            for (int i = 0; i < cb.capacity(); ++i)
123                                    cb.put((char)0);
124                    }
125    
126                    Auth auth = new Auth();
127                    auth.setUserName(userNameSB.toString());
128                    auth.setPassword(password);
129                    return auth;
130            }
131    
132            /**
133             * Get the {@link Auth} information via {@link #getAuth()} and verify, if they are valid. The validity is checked
134             * by trying to access the key-store.
135             * @param keyStoreID identifier of the key-store to work with.
136             * @return the {@link Auth} information via {@link #getAuth()}; never <code>null</code>.
137             * @throws WebApplicationException with {@link Status#UNAUTHORIZED}, if the client did not send an 'Authorization' header
138             * or if user-name / password is wrong;
139             * with {@link Status#FORBIDDEN}, if there is an 'Authorization' header, but no 'Basic' authentication header (other authentication modes, like e.g. 'Digest'
140             * are not supported); with {@link Status#INTERNAL_SERVER_ERROR}, if there was an {@link IOException}.
141             */
142            protected Auth authenticate(String keyStoreID)
143            throws WebApplicationException
144            {
145                    Auth auth = getAuth();
146                    try {
147                            KeyStore keyStore = keyStoreManager.getKeyStore(keyStoreID);
148                            keyStore.getKey(auth.getUserName(), auth.getPassword(), Long.MAX_VALUE);
149                    } catch (IOException e) {
150                            throw new WebApplicationException(Response.status(Status.INTERNAL_SERVER_ERROR).entity(new Error(e)).build());
151                    } catch (AuthenticationException e) {
152                            throw new WebApplicationException(Response.status(Status.UNAUTHORIZED).entity(new Error(e)).build());
153                    } catch (KeyNotFoundException e) {
154                            // ignore this - it's expected
155                            doNothing(); // Remove warning from PMD report: http://cumulus4j.org/pmd.html
156                    }
157                    return auth;
158            }
159    
160            private static final void doNothing() { }
161    }