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.File;
025    import java.io.FileNotFoundException;
026    import java.io.FileOutputStream;
027    import java.io.IOException;
028    import java.net.Socket;
029    import java.nio.ByteBuffer;
030    import java.security.KeyManagementException;
031    import java.security.NoSuchAlgorithmException;
032    import java.security.Principal;
033    import java.security.PrivateKey;
034    import java.security.cert.CertificateEncodingException;
035    import java.security.cert.CertificateException;
036    import java.security.cert.X509Certificate;
037    import java.util.LinkedList;
038    import java.lang.reflect.Method;
039    
040    import javax.net.ssl.KeyManager;
041    import javax.net.ssl.SSLContext;
042    import javax.net.ssl.SSLEngine;
043    import javax.net.ssl.SSLEngineResult;
044    import javax.net.ssl.SSLEngineResult.HandshakeStatus;
045    import javax.net.ssl.SSLEngineResult.Status;
046    import javax.net.ssl.SSLException;
047    import javax.net.ssl.SSLParameters;
048    import javax.net.ssl.SSLSession;
049    import javax.net.ssl.TrustManager;
050    import javax.net.ssl.X509ExtendedKeyManager;
051    import javax.net.ssl.X509TrustManager;
052    
053    import com.skype.util.Log;
054    
055    /**
056     * @author kcjones
057     *
058     */
059    public class TLSServerTransport implements Transport
060    {
061            private static final String TAG = "TLSServerTransport";
062            private static int MAXBUF;
063            
064            private boolean connected = false;
065            private Transport transport = null;
066            private SSLEngine engine = null;
067            private SSLContext context = null;
068        private FileOutputStream logFileOut = null;
069        private FileOutputStream logFileIn = null;
070        private boolean isLogging = false;
071    
072            private X509ExtendedKeyManager keyManager = null;
073            private TrustManager trustManager = null;
074    
075            private byte decrypted_bytes[] = new byte[]{};
076            private LinkedList<byte[]> decrypted_chunks = new LinkedList<byte[]>();
077        private ByteBuffer raw_source = null;
078    
079        public TLSServerTransport(Transport t, X509Certificate cert, PrivateKey privKey)
080        {
081            transport = t;
082    
083            trustManager = new dummyTrustManager();
084            keyManager = new dummyKeyManager(cert,privKey);
085            decrypted_chunks.clear();
086        }
087            
088            protected void finalize() throws Throwable
089            {
090                super.finalize();
091                if (isLogging) {
092                    logFileOut.flush();
093                    logFileOut.close();
094                    logFileIn.flush();
095                    logFileIn.close();
096                }
097            }
098    
099            @Override
100            public void startLogging(String logFileName)
101            {
102                assert(!isLogging);
103                
104            if (logFileName != null && ! logFileName.isEmpty()) {
105                try {
106                    logFileOut = new FileOutputStream(new File(logFileName + ".out.bin"), false);
107                    logFileIn  = new FileOutputStream(new File(logFileName + ".in.bin"), false);
108                    isLogging = true;
109                    Log.d(TAG, "Transport logging started");
110                }
111                catch (FileNotFoundException e) {
112                    Log.e(TAG, "Unable to open transport log files using:" + logFileName);
113                }
114            }
115            }
116            
117            void do_handshake() throws SSLException, IOException 
118            {
119                    
120                    Log.d(TAG, "do_handshake()");
121                    SSLEngineResult engineResult = null;
122                    HandshakeStatus result =  HandshakeStatus.NEED_UNWRAP;
123                    while(true) {
124                        switch(result) {
125                        case FINISHED:
126                            Log.d(TAG, result.toString());
127                            return;
128                        case NEED_TASK:
129                            Log.d(TAG, result.toString());
130                            Runnable task;
131                            while((task=engine.getDelegatedTask()) != null) {
132                                task.run();
133                            }
134                            break;
135                        case NEED_UNWRAP: {
136    
137                            byte [] msg = new byte [16384];
138                            int idx = 0;
139                            Log.d(TAG, result.toString());
140    
141                            if (decrypted_bytes.length == 0) {
142                                try {
143                                    transport.peek();
144                                    while(transport.hasMore() ) {
145                                        msg[idx++] = (byte) transport.read();
146                                    }
147                                }
148                                catch (IOException e) {
149                                    Log.e(TAG, "Read exception:" + e.getMessage() + ":" + e.getCause());
150                                    throw e;
151                                }
152                            }
153                            else {
154                                idx = decrypted_bytes.length;
155                                System.arraycopy(decrypted_bytes, 0, msg, 0, idx);
156                                decrypted_bytes = new byte[]{};
157                            }
158    
159                            ByteBuffer src0 = ByteBuffer.wrap(msg);
160                            ByteBuffer dst0 = ByteBuffer.allocate(16384);
161                            engineResult = engine.unwrap(src0, dst0);
162                            result = engineResult.getHandshakeStatus();
163                            int bytesConsumed = engineResult.bytesConsumed();
164    
165                            int sum = 0; //check if we had trailing zeroes
166                            while(bytesConsumed < idx)
167                                sum |= msg[bytesConsumed++];
168    
169                            int bytesLeftOver = idx - engineResult.bytesConsumed();
170                            if (sum != 0 && bytesLeftOver > 0 ) { //more than one message, keep a copy
171                                decrypted_bytes = new byte[bytesLeftOver];
172                                System.arraycopy(msg, engineResult.bytesConsumed(), 
173                                        decrypted_bytes, 0, bytesLeftOver);
174                            }
175                            break;
176                        }
177                        case NEED_WRAP: {
178                            Log.d(TAG, result.toString());
179                            ByteBuffer EMPTY = ByteBuffer.allocate(0);
180                            ByteBuffer dst1 = ByteBuffer.allocate(65536);
181                            engineResult = engine.wrap(EMPTY, dst1);
182                            byte [] bytes2send = new byte[engineResult.bytesProduced()];
183                            dst1.rewind();
184                            dst1.get(bytes2send);
185                            transport.write(bytes2send.length , bytes2send);
186                            break;
187                        }
188                        case NOT_HANDSHAKING:
189                            Log.d(TAG, "discarding " + decrypted_bytes.length + " bytes");
190                            if (decrypted_bytes.length > 0) {
191                                ByteBuffer s = ByteBuffer.wrap(decrypted_bytes);
192                                Log.hexdump(s.array());
193                                decrypted_bytes = new byte[] {};
194                            }
195                            Log.d(TAG, result.toString());
196                            return;
197                        }
198                        result = engine.getHandshakeStatus();
199    
200            }
201            }
202    
203            void checkConnected() throws IOException
204            {
205                    if (!connected) 
206                            throw new IOException("TLSServerTransport not connected!");
207            }
208    
209            @Override
210            public boolean connect() throws IOException
211            {
212                    Log.d(TAG, "connect()");
213    
214                    if (!transport.connect()) 
215                            return false;
216    
217                    try {
218                            context = SSLContext.getInstance("TLS");
219                    } catch (NoSuchAlgorithmException e) {
220                            throw new IOException("TLSServerTransport:IOException:" + e.getMessage() );
221                    }
222                    KeyManager [] km = { keyManager };
223                    TrustManager [] tm = { trustManager };
224                    try {
225                            context.init(km, tm, null);
226                    } catch (KeyManagementException e) {
227                            throw new IOException("TLSServerTransport:KeyManagementException:" + e.getMessage() );
228                    }
229                    engine = context.createSSLEngine();
230                    engine.setUseClientMode(false);
231                    engine.setNeedClientAuth(false);
232                    engine.setWantClientAuth(false);
233    
234                    try {
235                            @SuppressWarnings("rawtypes")
236                Class c = Class.forName("javax.net.ssl.SSLEngine");
237                            for (Method m : c.getDeclaredMethods() ) {
238                                    if (m.getName() == "getSSLParameters") { //this is not supported on Android < 2.3
239                                            SSLParameters params = engine.getSSLParameters();
240                                            params.setCipherSuites(new String [] { 
241                                                    "TLS_RSA_WITH_AES_128_CBC_SHA"
242                                            });
243                                            engine.setSSLParameters(params);
244                                            }
245                                    }
246                            }
247                    catch (ClassNotFoundException e) { //shouldnt ever happen
248                    }
249    
250                    try {
251                            do_handshake();
252                    } 
253                    catch (SSLException e) {
254                        throw new IOException("TLSServerTransport:handshake_failed:" + e.getMessage(), e);
255                    }
256                    catch (IOException e) {
257                        throw new IOException("TLSServerTransport:handshake_failed:" + e.getMessage(), e);
258                    }
259                    connected = true;
260    
261                    X509Certificate cert = keyManager.getCertificateChain("dummyServer")[0];
262                    byte[] certBytes = null;
263                    try {
264                            certBytes = cert.getEncoded();
265                    } catch (CertificateEncodingException e) {
266                            e.printStackTrace();
267                    }
268                    String lenStr = String.format("%08X", certBytes.length);
269                    byte [] lenBytes = lenStr.getBytes();
270                    write(lenBytes.length,lenBytes);
271                    write(certBytes.length,certBytes);
272    
273                    return true;
274            }
275    
276            @Override
277            public void disconnect() throws IOException
278            {
279                    checkConnected();
280                    
281                    transport.disconnect();
282            }
283    
284            @Override
285            public boolean hasMore() throws IOException 
286            {
287                    checkConnected();
288                    Log.d(TAG, "hasMore() bytes:" + decrypted_bytes.length + " chunks:" + decrypted_chunks.size());
289                    
290                    return (decrypted_bytes.length > 0) || decrypted_chunks.size() > 0 || transport.hasMore();
291            }
292    
293            @Override
294            public boolean isConnected() throws IOException
295            {
296                    return transport.isConnected();
297            }
298    
299            @Override
300            public int peek() throws IOException
301            {
302    //          Log.d(TAG, "TLSServerTransport::peek() called");
303                checkConnected();
304    
305                if (decrypted_chunks.isEmpty()) {
306                    readChunks();
307                }
308                if (decrypted_chunks.isEmpty())
309                {
310                    Log.d(TAG, "TLS Peek - nothing to peek at...");
311                    return 0;
312                }
313    //          Log.d(TAG, "TLS Peek: " + Integer.toHexString(decrypted_chunks.getFirst()[0] & 0xFF) +
314    //                  " " + (char)decrypted_chunks.getFirst()[0] +
315    //                  " " + (int)(decrypted_chunks.getFirst()[0] & 0xFF));
316                return decrypted_chunks.getFirst()[0];
317            }
318    
319            @Override
320            public int read() throws IOException
321            {
322                    checkConnected();
323                    byte b[] = new byte[]{'0'};
324                    read(1,b);
325    //              Log.d(TAG, "returning byte:" + b);
326                    return b[0];
327            }
328    
329            @Override
330            public int read(int numBytes, byte[] bytes) throws IOException
331            {
332                    checkConnected();
333    
334                    int num_waits = 0;
335                    final int MAXWAITS = 10;
336                    final int WAITMILIS = 50; // how long should we wait?
337                    int read = 0;
338    
339                    while (read < numBytes) {
340                            while (decrypted_chunks.isEmpty())
341                            {
342                                readChunks();
343    
344                                if (decrypted_chunks.isEmpty()) {
345                                    if (++num_waits > MAXWAITS)
346                                        throw new IOException("timeout waiting for transport input");
347    
348                                    try {
349                                        Log.d(TAG, "Waiting for data on transport take:" + num_waits);
350                                        wait(WAITMILIS);
351                                    }
352                                    catch (InterruptedException e) {}
353                                }
354                            }
355    
356                            byte [] chunk = decrypted_chunks.removeFirst();
357    
358                            if ((read+chunk.length) <= numBytes) {
359                                    // copy decrypted bytes into caller's byte array
360                                    System.arraycopy(chunk, 0, bytes, read, chunk.length);
361                                    read += chunk.length;
362                            } else {
363                                // unfortunately, our readers do not request the input in chunks
364                                // instead they read 1 byte increments which means we have to pop
365                                // the top chunk of input, peel one byte off, and push the remains back onto the list.
366                                // sub-optimal but works for now.
367                                System.arraycopy(chunk, 0, bytes, read, numBytes-read);
368                                byte [] chunkless = new byte[chunk.length - numBytes+read];
369                                System.arraycopy(chunk, numBytes-read, chunkless, 0, chunk.length - numBytes+read);
370                                decrypted_chunks.addFirst(chunkless);
371                                read = numBytes;
372                            }
373                    }
374                    return numBytes;
375            }
376    
377            @Override
378            public int read(int numBytes, byte[] bytes, boolean needNumBytes) throws IOException {
379                    Log.d(TAG, "TLSServerTransport::read(3) called");
380                    int bytesRead = read(numBytes, bytes);
381                    if ((bytesRead != numBytes) && (needNumBytes))
382                            throw new IOException("TLSServerTransport::read() - read less than required number of bytes");
383                    return bytesRead;
384            }
385    
386            /**
387             * drain all input from transport stream and decrypt using engine.
388             * 
389             * @param raw_bytes - buffer to accept unwrapped bytes read from socket
390             * @throws IOException - when read buffer is too small, or other socket read exceptions thrown
391             */
392            protected void readChunks() throws IOException
393            {
394    
395                    boolean hadLeftovers = false;
396    
397                    // allocate byte buffers to match session parameters exactly
398                    if (raw_source == null) {
399                        SSLSession s = engine.getSession();
400                        MAXBUF = s.getPacketBufferSize();
401                        if (s.getApplicationBufferSize() > MAXBUF) {
402                            MAXBUF = s.getApplicationBufferSize();
403                        }
404                        Log.d(TAG, "SSLSession packet size:" + MAXBUF);
405                        raw_source = ByteBuffer.allocate(MAXBUF);
406                    }
407    
408                    int pos = raw_source.position();
409                    if (pos > 0) {
410                        Log.d(TAG, "UNDERFLOW -contining from position:" + pos);
411                        hadLeftovers = true;
412                    }
413    
414                    do {
415                        raw_source.put((byte)transport.read());
416                    } while( transport.hasMore() &&
417                            raw_source.position() < MAXBUF );
418    
419                    if (pos == raw_source.position()) {
420                        Log.d(TAG, "Nothing read.");
421                        return;
422                    }
423                    Log.d(TAG, "readChunks() done reading at position:" + raw_source.position());
424    
425    
426                    ByteBuffer dst = ByteBuffer.allocate(MAXBUF); 
427                    boolean unwrapped;
428                    int loop = 0;
429                    do {
430    
431                            int todecode = raw_source.remaining();
432                            SSLEngineResult engineResult;
433                            try {
434                                    raw_source.flip();
435                                    engineResult = engine.unwrap(raw_source, dst);
436                                    raw_source.compact();
437                            } catch (SSLException e) {
438                                    String msg = "unwrap failed at loop " + loop + " with todecode " + todecode + "bytes "+engine.getSession().getCipherSuite();
439                                    System.err.println(msg);
440                                    Log.d(TAG, msg);
441                                    throw e;
442                            }
443    
444                        SSLEngineResult.Status unwrap_status = engineResult.getStatus();
445                        Log.d(TAG, "unwrap status:" + unwrap_status);
446    
447                        unwrapped = (unwrap_status == SSLEngineResult.Status.OK);
448                        dst.rewind();
449                        if (unwrapped || unwrap_status == SSLEngineResult.Status.BUFFER_UNDERFLOW)
450                        {
451                            int l = engineResult.bytesProduced();
452                            if (l > 0) {
453                                 Log.d(TAG, "decrypted length:" + l);
454                                 byte [] chunk = new byte[l];
455                                 dst.get(chunk, 0, l);
456    
457                                 Log.d(TAG, "\nchunk read is :");
458                                 Log.hexdump(chunk);
459                                 if (isLogging)
460                                     logFileIn.write(chunk);
461    
462                                 decrypted_chunks.addLast(chunk);
463                            }
464                        }
465                        else if (unwrap_status != Status.BUFFER_UNDERFLOW) {
466                            throw new IOException("Unrecoverable TLS error: " + unwrap_status);
467                        }
468                        else {
469                            Log.d(TAG, "UNDERFLOW? reseting raw_source for next time.");
470                        }
471                        dst.rewind();
472                    } while (unwrapped && raw_source.hasRemaining());
473    
474                Log.d(TAG, "readChunks() done, hasRemaining:" + raw_source.hasRemaining());
475    
476                if (!raw_source.hasRemaining()) {
477                    raw_source.clear();
478                }
479            }
480            
481            private void dumpSourceBuffer(String tag)
482            {
483                int pos = raw_source.position();
484                int lim =raw_source.limit();
485            Log.e(TAG, "---" + tag + " position:" + pos + " limit:" +  lim);
486            
487            if (raw_source.hasArray() && pos < lim) {
488                byte [] src = raw_source.array();
489                byte [] section = new byte[lim - pos];
490                System.arraycopy(src, pos, section, 0, lim - pos);
491                Log.hexdump(section);
492            }
493            }
494    
495            @Override
496            public boolean write(byte b) throws IOException
497            {
498                    Log.e(TAG, "FAIL! write(" + b + ")");
499                    throw new IOException("Invalid use of TLS transport");
500            }
501    
502            @Override
503            public boolean write(int numBytes, byte[] bytes)
504            throws IOException
505            {
506    //          synchronized (sslEngineLock) {
507    
508                    // Log.d(TAG, "write(" + numBytes +"):");
509                    // Log.hexdump(bytes);
510    
511                    checkConnected();
512    
513                    if (isLogging) {
514                        logFileOut.write(bytes, 0, numBytes);
515                    }
516                    SSLSession s            = engine.getSession();
517                    int begin = 0;
518                    int applicationBufferSize   = s.getApplicationBufferSize();
519                    int packetBufferSize        = s.getPacketBufferSize();
520                    ByteBuffer dst = ByteBuffer.allocate(packetBufferSize);
521                    while (numBytes > 0) {
522                            ByteBuffer src = ByteBuffer.wrap(bytes,begin,numBytes > applicationBufferSize ? applicationBufferSize : numBytes);
523                            SSLEngineResult engineResult = engine.wrap(src, dst);
524                            byte [] bytes2send = new byte[engineResult.bytesProduced()];
525                            dst.rewind();
526                            dst.get(bytes2send);
527                            numBytes -= engineResult.bytesConsumed();
528                            begin    += engineResult.bytesConsumed();
529                            transport.write(bytes2send.length, bytes2send);
530                            dst.clear();
531                    }
532    
533    //          }
534                
535                return false;
536            }
537    
538            public class dummyKeyManager extends X509ExtendedKeyManager {
539                    X509Certificate cert;
540                    PrivateKey privKey;
541    
542                    public dummyKeyManager(X509Certificate _cert,PrivateKey _privKey) {
543                            privKey = _privKey;
544                            cert = _cert;
545                    }
546    
547                    @Override
548                    public String chooseClientAlias(String[] arg0, Principal[] arg1, Socket arg2) {
549                            Log.d(TAG, "chooseClientAlias");
550                            return null;
551                    }
552    
553                    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
554                            Log.d(TAG, "chooseEngineServerAlias " + keyType);
555                            if (keyType == "RSA") {
556                                    return "dummyServer";
557                            }
558                            return null;
559                    }
560    
561                    @Override
562                    public String chooseServerAlias(String keyType, Principal[] issuers, Socket arg2) {
563                            Log.d(TAG, "chooseServerAlias");
564                            return null;
565                    }
566    
567                    @Override
568                    public X509Certificate[] getCertificateChain(String arg0) {
569                            Log.d(TAG, "getCertificateChain");
570                            return new X509Certificate[] { cert };
571                    }
572    
573                    @Override
574                    public String[] getClientAliases(String arg0, Principal[] arg1) {
575                            Log.d(TAG, "getClientAliases");
576                            return null;
577                    }
578    
579                    @Override
580                    public PrivateKey getPrivateKey(String arg0) {
581                            Log.d(TAG, "getPrivateKey");
582                            return privKey;
583                    }
584    
585                    @Override
586                    public String[] getServerAliases(String arg0, Principal[] arg1) {
587                            Log.d(TAG, "getServerAliases");
588                            return null;
589                    }
590            }
591    
592            /* 
593             * This class should never be called
594             * */
595            public class dummyTrustManager implements X509TrustManager {
596                    @Override
597                    public void checkClientTrusted(X509Certificate[] arg0, String arg1)
598                    throws CertificateException {
599                            throw new CertificateException("dummyTrustManager:checkClientTrusted called");
600                    }
601    
602                    @Override
603                    public void checkServerTrusted(X509Certificate[] arg0, String arg1)
604                    throws CertificateException {
605                            throw new CertificateException("dummyTrustManagercheckServerTrusted called");
606                    }
607    
608                    @Override
609                    public X509Certificate[] getAcceptedIssuers() {
610                            Log.d(TAG, "dummyTrustManager:getAcceptedIssuers");
611                            return null;
612                    }
613            }
614    
615    
616    }