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    }