    001    /**
    002     * Copyright (C) 2010, Skype Limited
    003     *
    004     * All intellectual property rights, including but not limited to copyrights,
    005     * trademarks and patents, as well as know how and trade secrets contained in,
    006     * relating to, or arising from the internet telephony software of
    007     * Skype Limited (including its affiliates, "Skype"), including without
    008     * limitation this source code, Skype API and related material of such
    009     * software proprietary to Skype and/or its licensors ("IP Rights") are and
    010     * shall remain the exclusive property of Skype and/or its licensors.
    011     * The recipient hereby acknowledges and agrees that any unauthorized use of
    012     * the IP Rights is a violation of intellectual property laws.
    013     *
    014     * Skype reserves all rights and may take legal action against infringers of
    015     * IP Rights.
    016     *
    017     * The recipient agrees not to remove, obscure, make illegible or alter any
    018     * notices or indications of the IP Rights and/or Skype's rights and
    019     * ownership thereof.
    020     */
    021
    022    package com.skype.ipc;
    023
    024    import java.io.ByteArrayInputStream;
    025    import java.io.File;
    026    import java.io.FileNotFoundException;
    027    import java.io.FileOutputStream;
    028    import java.io.IOException;
    029    import java.net.Socket;
    030    import java.nio.ByteBuffer;
    031    import java.security.KeyManagementException;
    032    import java.security.NoSuchAlgorithmException;
    033    import java.security.Principal;
    034    import java.security.PrivateKey;
    035    import java.security.cert.CertificateEncodingException;
    036    import java.security.cert.CertificateException;
    037    import java.security.cert.X509Certificate;
    038    import java.util.LinkedList;
    039    import java.lang.reflect.Method;
    040
    041    import javax.net.ssl.KeyManager;
    042    import javax.net.ssl.SSLContext;
    043    import javax.net.ssl.SSLEngine;
    044    import javax.net.ssl.SSLEngineResult;
    045    import javax.net.ssl.SSLEngineResult.HandshakeStatus;
    046    import javax.net.ssl.SSLEngineResult.Status;
    047    import javax.net.ssl.SSLException;
    048    import javax.net.ssl.SSLParameters;
    049    import javax.net.ssl.SSLSession;
    050    import javax.net.ssl.TrustManager;
    051    import javax.net.ssl.X509ExtendedKeyManager;
    052    import javax.net.ssl.X509TrustManager;
    053    import java.util.Arrays;
    054    import com.skype.util.Log;
    055
    056    /**
    057     * @author kcjones
    058     *
    059     */
    060    public class TLSServerTransport implements Transport
    061    {
    062            private static final String TAG = "TLSServerTransport";
    063            private static int MAXBUF;
    064
    065            private boolean connected = false;
    066            private Transport transport = null;
    067            private SSLEngine engine = null;
    068            private SSLContext context = null;
    069        private FileOutputStream logFileOut = null;
    070        private FileOutputStream logFileIn = null;
    071        private boolean isLogging = false;
    072
    073            private X509ExtendedKeyManager keyManager = null;
    074            private TrustManager trustManager = null;
    075
    076            private LinkedList<byte[]> decrypted_chunks = new LinkedList<byte[]>();
    077            private ByteArrayInputStream decryptedStream = new ByteArrayInputStream(new byte[0]);
    078        private ByteBuffer raw_source = null;
    079
    080        public TLSServerTransport(Transport t, X509Certificate cert, PrivateKey privKey)
    081        {
    082            transport = t;
    083
    084            trustManager = new dummyTrustManager();
    085            keyManager = new dummyKeyManager(cert,privKey);
    086            decrypted_chunks.clear();
    087        }
    088
    089            protected void finalize() throws Throwable
    090            {
    091                super.finalize();
    092                if (isLogging) {
    093                    logFileOut.flush();
    094                    logFileOut.close();
    095                    logFileIn.flush();
    096                    logFileIn.close();
    097                }
    098            }
    099
    100            @Override
    101            public void startLogging(String logFileName)
    102            {
    103                assert(!isLogging);
    104
    105            if (logFileName != null && ! logFileName.isEmpty()) {
    106                try {
    107                    logFileOut = new FileOutputStream(new File(logFileName + ".out.bin"), false);
    108                    logFileIn  = new FileOutputStream(new File(logFileName + ".in.bin"), false);
    109                    isLogging = true;
    110                    Log.d(TAG, "Transport logging started");
    111                }
    112                catch (FileNotFoundException e) {
    113                    Log.e(TAG, "Unable to open transport log files using:" + logFileName);
    114                }
    115            }
    116            }
    117
    118            void do_handshake() throws SSLException, IOException
    119            {
    120                    byte leftover_bytes[] = new byte[]{};
    121
    122                    Log.d(TAG, "do_handshake()");
    123                    SSLEngineResult engineResult = null;
    124                    HandshakeStatus result =  HandshakeStatus.NEED_UNWRAP;
    125                    while(true) {
    126                            switch(result) {
    127                            case FINISHED:
    128                                    Log.d(TAG, result.toString());
    129                                    return;
    130                            case NEED_TASK:
    131                                    Log.d(TAG, result.toString());
    132                                    Runnable task;
    133                                    while((task=engine.getDelegatedTask()) != null) {
    134                                            task.run();
    135                                    }
    136                                    break;
    137                            case NEED_UNWRAP: {
    138
    139                                    byte [] msg = new byte [16384];
    140                                    int idx = 0;
    141                                    Log.d(TAG, result.toString());
    142
    143                                    if (leftover_bytes.length == 0) {
    144                                            try {
    145                                                    transport.peek();
    146                                                    while(transport.hasMore() ) {
    147                                                            msg[idx++] = (byte) transport.read();
    148                                                    }
    149                                            }
    150                                            catch (IOException e) {
    151                                                    Log.e(TAG, "Read exception:" + e.getMessage() + ":" + e.getCause());
    152                                                    throw e;
    153                                            }
    154                                    }
    155                                    else {
    156                                            idx = leftover_bytes.length;
    157                                            System.arraycopy(leftover_bytes, 0, msg, 0, idx);
    158                                            leftover_bytes = new byte[]{};
    159                                    }
    160
    161                                    ByteBuffer src0 = ByteBuffer.wrap(msg);
    162                                    ByteBuffer dst0 = ByteBuffer.allocate(16384);
    163                                    engineResult = engine.unwrap(src0, dst0);
    164                                    result = engineResult.getHandshakeStatus();
    165                                    int bytesConsumed = engineResult.bytesConsumed();
    166
    167                                    int sum = 0; //check if we had trailing zeroes
    168                                    while(bytesConsumed < idx)
    169                                            sum |= msg[bytesConsumed++];
    170
    171                                    int bytesLeftOver = idx - engineResult.bytesConsumed();
    172                                    if (sum != 0 && bytesLeftOver > 0 ) { //more than one message, keep a copy
    173                                            leftover_bytes = new byte[bytesLeftOver];
    174                                            System.arraycopy(msg, engineResult.bytesConsumed(),
    175                                                            leftover_bytes, 0, bytesLeftOver);
    176                                    }
    177                                    break;
    178                            }
    179                            case NEED_WRAP: {
    180                                    Log.d(TAG, result.toString());
    181                                    ByteBuffer EMPTY = ByteBuffer.allocate(0);
    182                                    ByteBuffer dst1 = ByteBuffer.allocate(65536);
    183                                    engineResult = engine.wrap(EMPTY, dst1);
    184                                    byte [] bytes2send = new byte[engineResult.bytesProduced()];
    185                                    dst1.rewind();
    186                                    dst1.get(bytes2send);
    187                                    transport.write(bytes2send.length , bytes2send);
    188                                    break;
    189                            }
    190                            case NOT_HANDSHAKING:
    191                                    Log.d(TAG, "discarding " + leftover_bytes.length + " bytes");
    192                                    if (leftover_bytes.length > 0) {
    193                                            ByteBuffer s = ByteBuffer.wrap(leftover_bytes);
    194                                            Log.hexdump(s.array());
    195                                            leftover_bytes = new byte[] {};
    196                                    }
    197                                    Log.d(TAG, result.toString());
    198                                    return;
    199                            }
    200                            result = engine.getHandshakeStatus();
    201
    202                    }
    203            }
    204
    205            void checkConnected() throws IOException
    206            {
    207                    if (!connected)
    208                            throw new IOException("TLSServerTransport not connected!");
    209            }
    210
    211            @Override
    212            public boolean connect() throws IOException
    213            {
    214                    Log.d(TAG, "connect()");
    215
    216                    if (!transport.connect())
    217                            return false;
    218
    219                    try {
    220                            context = SSLContext.getInstance("TLS");
    221                    } catch (NoSuchAlgorithmException e) {
    222                            throw new IOException("TLSServerTransport:IOException:" + e.getMessage() );
    223                    }
    224                    KeyManager [] km = { keyManager };
    225                    TrustManager [] tm = { trustManager };
    226                    try {
    227                            context.init(km, tm, null);
    228                    } catch (KeyManagementException e) {
    229                            throw new IOException("TLSServerTransport:KeyManagementException:" + e.getMessage() );
    230                    }
    231                    engine = context.createSSLEngine();
    232                    engine.setUseClientMode(false);
    233                    engine.setNeedClientAuth(false);
    234                    engine.setWantClientAuth(false);
    235
    236                    try {
    237                            @SuppressWarnings("rawtypes")
    238                Class c = Class.forName("javax.net.ssl.SSLEngine");
    239                            for (Method m : c.getDeclaredMethods() ) {
    240                                    if (m.getName() == "getSSLParameters") { //this is not supported on Android < 2.3
    241                                            SSLParameters params = engine.getSSLParameters();
    242                                            params.setCipherSuites(new String [] {
    243                                                    "TLS_RSA_WITH_AES_128_CBC_SHA"
    244                                            });
    245                                            engine.setSSLParameters(params);
    246                                            }
    247                                    }
    248                            }
    249                    catch (ClassNotFoundException e) { //shouldnt ever happen
    250                    }
    251
    252                    try {
    253                            do_handshake();
    254                    }
    255                    catch (SSLException e) {
    256                        throw new IOException("TLSServerTransport:handshake_failed:" + e.getMessage(), e);
    257                    }
    258                    catch (IOException e) {
    259                        throw new IOException("TLSServerTransport:handshake_failed:" + e.getMessage(), e);
    260                    }
    261                    connected = true;
    262
    263                    X509Certificate cert = keyManager.getCertificateChain("dummyServer")[0];
    264                    byte[] certBytes = null;
    265                    try {
    266                            certBytes = cert.getEncoded();
    267                    } catch (CertificateEncodingException e) {
    268                            e.printStackTrace();
    269                    }
    270                    String lenStr = String.format("%08X", certBytes.length);
    271                    byte [] lenBytes = lenStr.getBytes();
    272                    write(lenBytes.length,lenBytes);
    273                    write(certBytes.length,certBytes);
    274
    275                    return true;
    276            }
    277
    278            @Override
    279            public void disconnect() throws IOException
    280            {
    281                    checkConnected();
    282
    283                    transport.disconnect();
    284            }
    285
    286            @Override
    287            public boolean hasMore() throws IOException
    288            {
    289                    // never used, according to KCJ
    290                    assert(false);
    291
    292                    return false;
    293            }
    294
    295            @Override
    296            public boolean isConnected() throws IOException
    297            {
    298                    return transport.isConnected();
    299            }
    300
    301            @Override
    302            public int peek() throws IOException
    303            {
    304                    if (decryptedStream.available() == 0) {
    305                            // this will block until bytes arrive, but we return a decrypted byte after this returns
    306                            transport.peek();
    307                            readMore();
    308                    }
    309
    310                    decryptedStream.mark(1);
    311                    int b = decryptedStream.read();
    312                    decryptedStream.reset();
    313                    return b;
    314            }
    315
    316            @Override
    317            public int read() throws IOException
    318            {
    319                    if (decryptedStream.available() > 0)
    320                            return decryptedStream.read();
    321
    322                    byte b[] = new byte[]{'0'};
    323                    read(1,b);
    324
    325                    return b[0];
    326            }
    327
    328            @Override
    329            public synchronized int read(int numBytes, byte[] bytes) throws IOException
    330            {
    331                    checkConnected();
    332
    333                    int len, offset = 0, remaining = numBytes;
    334                    while ((len = decryptedStream.available()) < remaining) {
    335                            if (len > 0) {
    336                                    decryptedStream.read(bytes, offset, len);
    337                                    offset += len;
    338                                    remaining -= len;
    339                            }
    340
    341                            readMore();
    342                    }
    343
    344                    decryptedStream.read(bytes, offset, remaining);
    345                    return numBytes;
    346            }
    347
    348            @Override
    349            public int read(int numBytes, byte[] bytes, boolean needNumBytes) throws IOException
    350            {
    351                    int bytesRead = read(numBytes, bytes);
    352                    return bytesRead;
    353            }
    354
    355            /**
    356             * drain all input from transport stream and decrypt using engine.
    357             *
    358             * @param raw_bytes - buffer to accept unwrapped bytes read from socket
    359             * @throws IOException - when read buffer is too small, or other socket read exceptions thrown
    360             */
    361            protected void readMore() throws IOException
    362            {
    363                    assert(decryptedStream.available() == 0);
    364                    if (decrypted_chunks.isEmpty()) {
    365                            readChunks();
    366                    }
    367                    // either we find soemthing to read, or throw an exception
    368                    assert( ! decrypted_chunks.isEmpty());
    369                    byte [] chunk = decrypted_chunks.removeFirst();
    370                    decryptedStream = new ByteArrayInputStream(chunk);
    371            }
    372
    373            protected void readChunks() throws IOException
    374            {
    375                    // allocate byte buffers to match session parameters exactly
    376                    if (raw_source == null) {
    377                            SSLSession s = engine.getSession();
    378                            MAXBUF = s.getPacketBufferSize();
    379                            if (s.getApplicationBufferSize() > MAXBUF) {
    380                                    MAXBUF = s.getApplicationBufferSize();
    381                            }
    382                            Log.d(TAG, "SSLSession packet size:" + MAXBUF);
    383                            raw_source = ByteBuffer.allocate(MAXBUF);
    384
    385                    }
    386
    387                    int pos = raw_source.position();
    388    //                      System.err.println("reading "+pos);
    389                    do {
    390                            raw_source.put((byte)transport.read());
    391                    } while( transport.hasMore() && raw_source.position() < MAXBUF );
    392
    393                    if (pos == raw_source.position()) {
    394    //                      System.err.println("nothing read");
    395                            Log.d(TAG, "Nothing read.");
    396                            return;
    397                    }
    398    //              System.err.println("read "+raw_source.position());
    399
    400                    ByteBuffer dst = ByteBuffer.allocate(MAXBUF);
    401                    boolean unwrap_ok = false;
    402                    int loop = 0;
    403                    do {
    404                            SSLEngineResult engineResult;
    405                            try {
    406                                    raw_source.flip();
    407    //                              System.err.println("unwrapping "+raw_source.remaining());
    408                                    engineResult = engine.unwrap(raw_source, dst);
    409                                    raw_source.compact();
    410    //                              System.err.println("unwrapped "+raw_source.position()+" "+raw_source.limit());
    411                            } catch (SSLException e) {
    412                                    String msg = "unwrap failed at bytes "+engine.getSession().getCipherSuite();
    413                                    System.err.println(msg);
    414                                    Log.d(TAG, msg);
    415                                    throw e;
    416                            }
    417
    418                            SSLEngineResult.Status unwrap_status = engineResult.getStatus();
    419                            //Log.d(TAG, "unwrap status:" + unwrap_status);
    420                            unwrap_ok = unwrap_status == SSLEngineResult.Status.OK;
    421                            if (unwrap_ok || unwrap_status == SSLEngineResult.Status.BUFFER_UNDERFLOW)
    422                            {
    423                                    int l = engineResult.bytesProduced();
    424                                    //Log.d(TAG, "decrypted length:" + l);
    425                                    if (l > 0 || loop == 0) {
    426                                            loop++;
    427                                            byte [] chunk = new byte[l];
    428                                            dst.rewind();
    429                                            dst.get(chunk, 0, l);
    430                                            dst.clear();
    431                                            if (isLogging)
    432                                                    logFileIn.write(chunk);
    433
    434                                            decrypted_chunks.addLast(chunk);
    435                                    }
    436    //                                      if (!unwrap_ok)
    437    //                                              System.err.println("underflow, adding chunk with "+l+" bytes, remains "+raw_source.position());
    438    //                                      else
    439    //                                              System.err.println("ok, adding chunk with "+l+" bytes, remains "+raw_source.position());
    440                            }
    441                            else {
    442                                    throw new IOException("Unrecoverable TLS error: " + unwrap_status);
    443                            }
    444                    } while (unwrap_ok && raw_source.position() > 0);
    445    //              System.err.println("end reading");
    446            }
    447
    448            private void dumpSourceBuffer(String tag)
    449            {
    450                int pos = raw_source.position();
    451                int lim =raw_source.limit();
    452            Log.e(TAG, "---" + tag + " position:" + pos + " limit:" +  lim);
    453
    454            if (raw_source.hasArray() && pos < lim) {
    455                byte [] src = raw_source.array();
    456                byte [] section = new byte[lim - pos];
    457                System.arraycopy(src, pos, section, 0, lim - pos);
    458                Log.hexdump(section);
    459            }
    460            }
    461
    462            @Override
    463            public boolean write(byte b) throws IOException
    464            {
    465                    Log.e(TAG, "FAIL! write(" + b + ")");
    466                    throw new IOException("Invalid use of TLS transport");
    467            }
    468
    469            @Override
    470            public boolean write(int numBytes, byte[] bytes)
    471            throws IOException
    472            {
    473                    checkConnected();
    474
    475                    if (isLogging) {
    476                            logFileOut.write(bytes, 0, numBytes);
    477                    }
    478
    479                    SSLSession s            = engine.getSession();
    480                    int begin = 0;
    481                    int packetBufferSize        = s.getPacketBufferSize();
    482                    ByteBuffer dst = ByteBuffer.allocate(packetBufferSize);
    483                    ByteBuffer src = ByteBuffer.wrap(bytes);
    484                    while (numBytes > 0) {
    485                            SSLEngineResult engineResult = engine.wrap(src, dst);
    486                            if (engineResult.getStatus() != SSLEngineResult.Status.OK)  {
    487                                    throw new IOException("error when wrapping");
    488                            }
    489                            numBytes -= engineResult.bytesConsumed();
    490    //                              System.err.println("write, remains "+numBytes);
    491                            transport.write(dst.position(),  dst.array());
    492                            dst.clear();
    493                    }
    494
    495                    return false;
    496            }
    497
    498            public class dummyKeyManager extends X509ExtendedKeyManager {
    499                    X509Certificate cert;
    500                    PrivateKey privKey;
    501
    502                    public dummyKeyManager(X509Certificate _cert,PrivateKey _privKey) {
    503                            privKey = _privKey;
    504                            cert = _cert;
    505                    }
    506
    507                    @Override
    508                    public String chooseClientAlias(String[] arg0, Principal[] arg1, Socket arg2) {
    509                            Log.d(TAG, "chooseClientAlias");
    510                            return null;
    511                    }
    512
    513                    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
    514                            Log.d(TAG, "chooseEngineServerAlias " + keyType);
    515                            if (keyType == "RSA") {
    516                                    return "dummyServer";
    517                            }
    518                            return null;
    519                    }
    520
    521                    @Override
    522                    public String chooseServerAlias(String keyType, Principal[] issuers, Socket arg2) {
    523                            Log.d(TAG, "chooseServerAlias");
    524                            return null;
    525                    }
    526
    527                    @Override
    528                    public X509Certificate[] getCertificateChain(String arg0) {
    529                            Log.d(TAG, "getCertificateChain");
    530                            return new X509Certificate[] { cert };
    531                    }
    532
    533                    @Override
    534                    public String[] getClientAliases(String arg0, Principal[] arg1) {
    535                            Log.d(TAG, "getClientAliases");
    536                            return null;
    537                    }
    538
    539                    @Override
    540                    public PrivateKey getPrivateKey(String arg0) {
    541                            Log.d(TAG, "getPrivateKey");
    542                            return privKey;
    543                    }
    544
    545                    @Override
    546                    public String[] getServerAliases(String arg0, Principal[] arg1) {
    547                            Log.d(TAG, "getServerAliases");
    548                            return null;
    549                    }
    550            }
    551
    552            /*
    553             * This class should never be called
    554             * */
    555            public class dummyTrustManager implements X509TrustManager {
    556                    @Override
    557                    public void checkClientTrusted(X509Certificate[] arg0, String arg1)
    558                    throws CertificateException {
    559                            throw new CertificateException("dummyTrustManager:checkClientTrusted called");
    560                    }
    561
    562                    @Override
    563                    public void checkServerTrusted(X509Certificate[] arg0, String arg1)
    564                    throws CertificateException {
    565                            throw new CertificateException("dummyTrustManagercheckServerTrusted called");
    566                    }
    567
    568                    @Override
    569                    public X509Certificate[] getAcceptedIssuers() {
    570                            Log.d(TAG, "dummyTrustManager:getAcceptedIssuers");
    571                            return null;
    572                    }
    573            }
    574    }