alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

Java example source code file (DigestMD5Client.java)

This example Java source code file (DigestMD5Client.java) is included in the alvinalexander.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Learn more about this Java project at its project page.

Java - Java tags/keywords

client, digest-md5, digest\-challenge, directive_key, error, invalid, ioexception, log, logging, namecallback, realmcallback, saslexception, security, string, unset, unsupportedencodingexception, util

The DigestMD5Client.java Java example source code

/*
 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.security.sasl.digest;

import java.security.NoSuchAlgorithmException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.StringTokenizer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Arrays;

import java.util.logging.Level;

import javax.security.sasl.*;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.UnsupportedCallbackException;

/**
  * An implementation of the DIGEST-MD5
  * (<a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831) SASL
  * (<a href="http://www.ietf.org/rfc/rfc2222.txt">RFC 2222) mechanism.
  *
  * The DIGEST-MD5 SASL mechanism specifies two modes of authentication.
  * - Initial Authentication
  * - Subsequent Authentication - optional, (currently unsupported)
  *
  * Required callbacks:
  * - RealmChoiceCallback
  *    shows user list of realms server has offered; handler must choose one
  *    from list
  * - RealmCallback
  *    shows user the only realm server has offered or none; handler must
  *    enter realm to use
  * - NameCallback
  *    handler must enter username to use for authentication
  * - PasswordCallback
  *    handler must enter password for username to use for authentication
  *
  * Environment properties that affect behavior of implementation:
  *
  * javax.security.sasl.qop
  *    quality of protection; list of auth, auth-int, auth-conf; default is "auth"
  * javax.security.sasl.strength
  *    auth-conf strength; list of high, medium, low; default is highest
  *    available on platform ["high,medium,low"].
  *    high means des3 or rc4 (128); medium des or rc4-56; low is rc4-40;
  *    choice of cipher depends on its availablility on platform
  * javax.security.sasl.maxbuf
  *    max receive buffer size; default is 65536
  * javax.security.sasl.sendmaxbuffer
  *    max send buffer size; default is 65536; (min with server max recv size)
  *
  * com.sun.security.sasl.digest.cipher
  *    name a specific cipher to use; setting must be compatible with the
  *    setting of the javax.security.sasl.strength property.
  *
  * @see <a href="http://www.ietf.org/rfc/rfc2222.txt">RFC 2222
  * - Simple Authentication and Security Layer (SASL)
  * @see <a href="http://www.ietf.org/rfc/rfc2831.txt">RFC 2831
  * - Using Digest Authentication as a SASL Mechanism
  * @see <a href="http://java.sun.com/products/jce">Java(TM)
  * Cryptography Extension 1.2.1 (JCE)</a>
  * @see <a href="http://java.sun.com/products/jaas">Java(TM)
  * Authentication and Authorization Service (JAAS)</a>
  *
  * @author Jonathan Bruce
  * @author Rosanna Lee
  */
final class DigestMD5Client extends DigestMD5Base implements SaslClient {
    private static final String MY_CLASS_NAME = DigestMD5Client.class.getName();

    // Property for specifying cipher explicitly
    private static final String CIPHER_PROPERTY =
        "com.sun.security.sasl.digest.cipher";

    /* Directives encountered in challenges sent by the server. */
    private static final String[] DIRECTIVE_KEY = {
        "realm",      // >= 0 times
        "qop",        // atmost once; default is "auth"
        "algorithm",  // exactly once
        "nonce",      // exactly once
        "maxbuf",     // atmost once; default is 65536
        "charset",    // atmost once; default is ISO 8859-1
        "cipher",     // exactly once if qop is "auth-conf"
        "rspauth",    // exactly once in 2nd challenge
        "stale",      // atmost once for in subsequent auth (not supported)
    };

    /* Indices into DIRECTIVE_KEY */
    private static final int REALM = 0;
    private static final int QOP = 1;
    private static final int ALGORITHM = 2;
    private static final int NONCE = 3;
    private static final int MAXBUF = 4;
    private static final int CHARSET = 5;
    private static final int CIPHER = 6;
    private static final int RESPONSE_AUTH = 7;
    private static final int STALE = 8;

    private int nonceCount; // number of times nonce has been used/seen

    /* User-supplied/generated information */
    private String specifiedCipher;  // cipher explicitly requested by user
    private byte[] cnonce;        // client generated nonce
    private String username;
    private char[] passwd;
    private byte[] authzidBytes;  // byte repr of authzid

    /**
      * Constructor for DIGEST-MD5 mechanism.
      *
      * @param authzid A non-null String representing the principal
      * for which authorization is being granted..
      * @param digestURI A non-null String representing detailing the
      * combined protocol and host being used for authentication.
      * @param props The possibly null properties to be used by the SASL
      * mechanism to configure the authentication exchange.
      * @param cbh The non-null CallbackHanlder object for callbacks
      * @throws SaslException if no authentication ID or password is supplied
      */
    DigestMD5Client(String authzid, String protocol, String serverName,
        Map<String, ?> props, CallbackHandler cbh) throws SaslException {

        super(props, MY_CLASS_NAME, 2, protocol + "/" + serverName, cbh);

        // authzID can only be encoded in UTF8 - RFC 2222
        if (authzid != null) {
            this.authzid = authzid;
            try {
                authzidBytes = authzid.getBytes("UTF8");

            } catch (UnsupportedEncodingException e) {
                throw new SaslException(
                    "DIGEST-MD5: Error encoding authzid value into UTF-8", e);
            }
        }

        if (props != null) {
            specifiedCipher = (String)props.get(CIPHER_PROPERTY);

            logger.log(Level.FINE, "DIGEST60:Explicitly specified cipher: {0}",
                specifiedCipher);
        }
   }

    /**
     * DIGEST-MD5 has no initial response
     *
     * @return false
     */
    public boolean hasInitialResponse() {
        return false;
    }

    /**
     * Process the challenge data.
     *
     * The server sends a digest-challenge which the client must reply to
     * in a digest-response. When the authentication is complete, the
     * completed field is set to true.
     *
     * @param challengeData A non-null byte array containing the challenge
     * data from the server.
     * @return A possibly null byte array containing the response to
     * be sent to the server.
     *
     * @throws SaslException If the platform does not have MD5 digest support
     * or if the server sends an invalid challenge.
     */
    public byte[] evaluateChallenge(byte[] challengeData) throws SaslException {

        if (challengeData.length > MAX_CHALLENGE_LENGTH) {
            throw new SaslException(
                "DIGEST-MD5: Invalid digest-challenge length. Got:  " +
                challengeData.length + " Expected < " + MAX_CHALLENGE_LENGTH);
        }

        /* Extract and process digest-challenge */
        byte[][] challengeVal;

        switch (step) {
        case 2:
            /* Process server's first challenge (from Step 1) */
            /* Get realm, qop, maxbuf, charset, algorithm, cipher, nonce
               directives */
            List<byte[]> realmChoices = new ArrayList(3);
            challengeVal = parseDirectives(challengeData, DIRECTIVE_KEY,
                realmChoices, REALM);

            try {
                processChallenge(challengeVal, realmChoices);
                checkQopSupport(challengeVal[QOP], challengeVal[CIPHER]);
                ++step;
                return generateClientResponse(challengeVal[CHARSET]);
            } catch (SaslException e) {
                step = 0;
                clearPassword();
                throw e; // rethrow
            } catch (IOException e) {
                step = 0;
                clearPassword();
                throw new SaslException("DIGEST-MD5: Error generating " +
                    "digest response-value", e);
            }

        case 3:
            try {
                /* Process server's step 3 (server response to digest response) */
                /* Get rspauth directive */
                challengeVal = parseDirectives(challengeData, DIRECTIVE_KEY,
                    null, REALM);
                validateResponseValue(challengeVal[RESPONSE_AUTH]);


                /* Initialize SecurityCtx implementation */
                if (integrity && privacy) {
                    secCtx = new DigestPrivacy(true /* client */);
                } else if (integrity) {
                    secCtx = new DigestIntegrity(true /* client */);
                }

                return null; // Mechanism has completed.
            } finally {
                clearPassword();
                step = 0;  // Set to invalid state
                completed = true;
            }

        default:
            // No other possible state
            throw new SaslException("DIGEST-MD5: Client at illegal state");
        }
    }


   /**
    * Record information from the challengeVal array into variables/fields.
    * Check directive values that are multi-valued and ensure that mandatory
    * directives not missing from the digest-challenge.
    *
    * @throws SaslException if a sasl is a the mechanism cannot
    * correcly handle a callbacks or if a violation in the
    * digest challenge format is detected.
    */
    private void processChallenge(byte[][] challengeVal, List<byte[]> realmChoices)
        throws SaslException, UnsupportedEncodingException {

        /* CHARSET: optional atmost once */
        if (challengeVal[CHARSET] != null) {
            if (!"utf-8".equals(new String(challengeVal[CHARSET], encoding))) {
                throw new SaslException("DIGEST-MD5: digest-challenge format " +
                    "violation. Unrecognised charset value: " +
                    new String(challengeVal[CHARSET]));
            } else {
                encoding = "UTF8";
                useUTF8 = true;
            }
        }

        /* ALGORITHM: required exactly once */
        if (challengeVal[ALGORITHM] == null) {
            throw new SaslException("DIGEST-MD5: Digest-challenge format " +
                "violation: algorithm directive missing");
        } else if (!"md5-sess".equals(new String(challengeVal[ALGORITHM], encoding))) {
            throw new SaslException("DIGEST-MD5: Digest-challenge format " +
                "violation. Invalid value for 'algorithm' directive: " +
                challengeVal[ALGORITHM]);
        }

        /* NONCE: required exactly once */
        if (challengeVal[NONCE] == null) {
            throw new SaslException("DIGEST-MD5: Digest-challenge format " +
                "violation: nonce directive missing");
        } else {
            nonce = challengeVal[NONCE];
        }

        try {
            /* REALM: optional, if multiple, stored in realmChoices */
            String[] realmTokens = null;

            if (challengeVal[REALM] != null) {
                if (realmChoices == null || realmChoices.size() <= 1) {
                    // Only one realm specified
                    negotiatedRealm = new String(challengeVal[REALM], encoding);
                } else {
                    realmTokens = new String[realmChoices.size()];
                    for (int i = 0; i < realmTokens.length; i++) {
                        realmTokens[i] =
                            new String(realmChoices.get(i), encoding);
                    }
                }
            }

            NameCallback ncb = authzid == null ?
                new NameCallback("DIGEST-MD5 authentication ID: ") :
                new NameCallback("DIGEST-MD5 authentication ID: ", authzid);
            PasswordCallback pcb =
                new PasswordCallback("DIGEST-MD5 password: ", false);

            if (realmTokens == null) {
                // Server specified <= 1 realm
                // If 0, RFC 2831: the client SHOULD solicit a realm from the user.
                RealmCallback tcb =
                    (negotiatedRealm == null? new RealmCallback("DIGEST-MD5 realm: ") :
                        new RealmCallback("DIGEST-MD5 realm: ", negotiatedRealm));

                cbh.handle(new Callback[] {tcb, ncb, pcb});

                /* Acquire realm from RealmCallback */
                negotiatedRealm = tcb.getText();
                if (negotiatedRealm == null) {
                    negotiatedRealm = "";
                }
            } else {
                RealmChoiceCallback ccb = new RealmChoiceCallback(
                    "DIGEST-MD5 realm: ",
                    realmTokens,
                    0, false);
                cbh.handle(new Callback[] {ccb, ncb, pcb});

                // Acquire realm from RealmChoiceCallback
                int[] selected = ccb.getSelectedIndexes();
                if (selected == null
                        || selected[0] < 0
                        || selected[0] >= realmTokens.length) {
                    throw new SaslException("DIGEST-MD5: Invalid realm chosen");
                }
                negotiatedRealm = realmTokens[selected[0]];
            }

            passwd = pcb.getPassword();
            pcb.clearPassword();
            username = ncb.getName();

        } catch (SaslException se) {
            throw se;

        } catch (UnsupportedCallbackException e) {
            throw new SaslException("DIGEST-MD5: Cannot perform callback to " +
                "acquire realm, authentication ID or password", e);

        } catch (IOException e) {
            throw new SaslException(
                "DIGEST-MD5: Error acquiring realm, authentication ID or password", e);
        }

        if (username == null || passwd == null) {
            throw new SaslException(
                "DIGEST-MD5: authentication ID and password must be specified");
        }

        /* MAXBUF: optional atmost once */
        int srvMaxBufSize =
            (challengeVal[MAXBUF] == null) ? DEFAULT_MAXBUF
            : Integer.parseInt(new String(challengeVal[MAXBUF], encoding));
        sendMaxBufSize =
            (sendMaxBufSize == 0) ? srvMaxBufSize
            : Math.min(sendMaxBufSize, srvMaxBufSize);
    }

    /**
     * Parses the 'qop' directive. If 'auth-conf' is specified by
     * the client and offered as a QOP option by the server, then a check
     * is client-side supported ciphers is performed.
     *
     * @throws IOException
     */
    private void checkQopSupport(byte[] qopInChallenge, byte[] ciphersInChallenge)
        throws IOException {

        /* QOP: optional; if multiple, merged earlier */
        String qopOptions;

        if (qopInChallenge == null) {
            qopOptions = "auth";
        } else {
            qopOptions = new String(qopInChallenge, encoding);
        }

        // process
        String[] serverQopTokens = new String[3];
        byte[] serverQop = parseQop(qopOptions, serverQopTokens,
            true /* ignore unrecognized tokens */);
        byte serverAllQop = combineMasks(serverQop);

        switch (findPreferredMask(serverAllQop, qop)) {
        case 0:
            throw new SaslException("DIGEST-MD5: No common protection " +
                "layer between client and server");

        case NO_PROTECTION:
            negotiatedQop = "auth";
            // buffer sizes not applicable
            break;

        case INTEGRITY_ONLY_PROTECTION:
            negotiatedQop = "auth-int";
            integrity = true;
            rawSendSize = sendMaxBufSize - 16;
            break;

        case PRIVACY_PROTECTION:
            negotiatedQop = "auth-conf";
            privacy = integrity = true;
            rawSendSize = sendMaxBufSize - 26;
            checkStrengthSupport(ciphersInChallenge);
            break;
        }

        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "DIGEST61:Raw send size: {0}",
                new Integer(rawSendSize));
        }
     }

    /**
     * Processes the 'cipher' digest-challenge directive. This allows the
     * mechanism to check for client-side support against the list of
     * supported ciphers send by the server. If no match is found,
     * the mechanism aborts.
     *
     * @throws SaslException If an error is encountered in processing
     * the cipher digest-challenge directive or if no client-side
     * support is found.
     */
    private void checkStrengthSupport(byte[] ciphersInChallenge)
        throws IOException {

        /* CIPHER: required exactly once if qop=auth-conf */
        if (ciphersInChallenge == null) {
            throw new SaslException("DIGEST-MD5: server did not specify " +
                "cipher to use for 'auth-conf'");
        }

        // First determine ciphers that server supports
        String cipherOptions = new String(ciphersInChallenge, encoding);
        StringTokenizer parser = new StringTokenizer(cipherOptions, ", \t\n");
        int tokenCount = parser.countTokens();
        String token = null;
        byte[] serverCiphers = { UNSET,
                                 UNSET,
                                 UNSET,
                                 UNSET,
                                 UNSET };
        String[] serverCipherStrs = new String[serverCiphers.length];

        // Parse ciphers in challenge; mark each that server supports
        for (int i = 0; i < tokenCount; i++) {
            token = parser.nextToken();
            for (int j = 0; j < CIPHER_TOKENS.length; j++) {
                if (token.equals(CIPHER_TOKENS[j])) {
                    serverCiphers[j] |= CIPHER_MASKS[j];
                    serverCipherStrs[j] = token; // keep for replay to server
                    logger.log(Level.FINE, "DIGEST62:Server supports {0}", token);
                }
            }
        }

        // Determine which ciphers are available on client
        byte[] clntCiphers = getPlatformCiphers();

        // Take intersection of server and client supported ciphers
        byte inter = 0;
        for (int i = 0; i < serverCiphers.length; i++) {
            serverCiphers[i] &= clntCiphers[i];
            inter |= serverCiphers[i];
        }

        if (inter == UNSET) {
            throw new SaslException(
                "DIGEST-MD5: Client supports none of these cipher suites: " +
                cipherOptions);
        }

        // now have a clear picture of user / client; client / server
        // cipher options. Leverage strength array against what is
        // supported to choose a cipher.
        negotiatedCipher = findCipherAndStrength(serverCiphers, serverCipherStrs);

        if (negotiatedCipher == null) {
            throw new SaslException("DIGEST-MD5: Unable to negotiate " +
                "a strength level for 'auth-conf'");
        }
        logger.log(Level.FINE, "DIGEST63:Cipher suite: {0}", negotiatedCipher);
    }

    /**
     * Steps through the ordered 'strength' array, and compares it with
     * the 'supportedCiphers' array. The cipher returned represents
     * the best possible cipher based on the strength preference and the
     * available ciphers on both the server and client environments.
     *
     * @param tokens The array of cipher tokens sent by server
     * @return The agreed cipher.
     */
    private String findCipherAndStrength(byte[] supportedCiphers,
        String[] tokens) {
        byte s;
        for (int i = 0; i < strength.length; i++) {
            if ((s=strength[i]) != 0) {
                for (int j = 0; j < supportedCiphers.length; j++) {

                    // If user explicitly requested cipher, then it
                    // must be the one we choose

                    if (s == supportedCiphers[j] &&
                        (specifiedCipher == null ||
                            specifiedCipher.equals(tokens[j]))) {
                        switch (s) {
                        case HIGH_STRENGTH:
                            negotiatedStrength = "high";
                            break;
                        case MEDIUM_STRENGTH:
                            negotiatedStrength = "medium";
                            break;
                        case LOW_STRENGTH:
                            negotiatedStrength = "low";
                            break;
                        }

                        return tokens[j];
                    }
                }
            }
        }

        return null;  // none found
    }

    /**
     * Returns digest-response suitable for an initial authentication.
     *
     * The following are qdstr-val (quoted string values) as per RFC 2831,
     * which means that any embedded quotes must be escaped.
     *    realm-value
     *    nonce-value
     *    username-value
     *    cnonce-value
     *    authzid-value
     * @returns <tt>digest-response in a byte array
     * @throws SaslException if there is an error generating the
     * response value or the cnonce value.
     */
    private byte[] generateClientResponse(byte[] charset) throws IOException {

        ByteArrayOutputStream digestResp = new ByteArrayOutputStream();

        if (useUTF8) {
            digestResp.write("charset=".getBytes(encoding));
            digestResp.write(charset);
            digestResp.write(',');
        }

        digestResp.write(("username=\"" +
            quotedStringValue(username) + "\",").getBytes(encoding));

        if (negotiatedRealm.length() > 0) {
            digestResp.write(("realm=\"" +
                quotedStringValue(negotiatedRealm) + "\",").getBytes(encoding));
        }

        digestResp.write("nonce=\"".getBytes(encoding));
        writeQuotedStringValue(digestResp, nonce);
        digestResp.write('"');
        digestResp.write(',');

        nonceCount = getNonceCount(nonce);
        digestResp.write(("nc=" +
            nonceCountToHex(nonceCount) + ",").getBytes(encoding));

        cnonce = generateNonce();
        digestResp.write("cnonce=\"".getBytes(encoding));
        writeQuotedStringValue(digestResp, cnonce);
        digestResp.write("\",".getBytes(encoding));
        digestResp.write(("digest-uri=\"" + digestUri + "\",").getBytes(encoding));

        digestResp.write("maxbuf=".getBytes(encoding));
        digestResp.write(String.valueOf(recvMaxBufSize).getBytes(encoding));
        digestResp.write(',');

        try {
            digestResp.write("response=".getBytes(encoding));
            digestResp.write(generateResponseValue("AUTHENTICATE",
                digestUri, negotiatedQop, username,
                negotiatedRealm, passwd, nonce, cnonce,
                nonceCount, authzidBytes));
            digestResp.write(',');
        } catch (Exception e) {
            throw new SaslException(
                "DIGEST-MD5: Error generating response value", e);
        }

        digestResp.write(("qop=" + negotiatedQop).getBytes(encoding));

        if (negotiatedCipher != null) {
            digestResp.write((",cipher=\"" + negotiatedCipher + "\"").getBytes(encoding));
        }

        if (authzidBytes != null) {
            digestResp.write(",authzid=\"".getBytes(encoding));
            writeQuotedStringValue(digestResp, authzidBytes);
            digestResp.write("\"".getBytes(encoding));
        }

        if (digestResp.size() > MAX_RESPONSE_LENGTH) {
            throw new SaslException ("DIGEST-MD5: digest-response size too " +
                "large. Length: "  + digestResp.size());
        }
        return digestResp.toByteArray();
     }


    /**
     * From RFC 2831, Section 2.1.3: Step Three
     * [Server] sends a message formatted as follows:
     *     response-auth = "rspauth" "=" response-value
     * where response-value is calculated as above, using the values sent in
     * step two, except that if qop is "auth", then A2 is
     *
     *  A2 = { ":", digest-uri-value }
     *
     * And if qop is "auth-int" or "auth-conf" then A2 is
     *
     *  A2 = { ":", digest-uri-value, ":00000000000000000000000000000000" }
     */
    private void validateResponseValue(byte[] fromServer) throws SaslException {
        if (fromServer == null) {
            throw new SaslException("DIGEST-MD5: Authenication failed. " +
                "Expecting 'rspauth' authentication success message");
        }

        try {
            byte[] expected = generateResponseValue("",
                digestUri, negotiatedQop, username, negotiatedRealm,
                passwd, nonce, cnonce,  nonceCount, authzidBytes);
            if (!Arrays.equals(expected, fromServer)) {
                /* Server's rspauth value does not match */
                throw new SaslException(
                    "Server's rspauth value does not match what client expects");
            }
        } catch (NoSuchAlgorithmException e) {
            throw new SaslException(
                "Problem generating response value for verification", e);
        } catch (IOException e) {
            throw new SaslException(
                "Problem generating response value for verification", e);
        }
    }

    /**
     * Returns the number of requests (including current request)
     * that the client has sent in response to nonceValue.
     * This is 1 the first time nonceValue is seen.
     *
     * We don't cache nonce values seen, and we don't support subsequent
     * authentication, so the value is always 1.
     */
    private static int getNonceCount(byte[] nonceValue) {
        return 1;
    }

    private void clearPassword() {
        if (passwd != null) {
            for (int i = 0; i < passwd.length; i++) {
                passwd[i] = 0;
            }
            passwd = null;
        }
    }
}

Other Java examples (source code examples)

Here is a short list of links related to this Java DigestMD5Client.java source code file:

... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2024 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.