31 | * Writes server info to output stream 32 | */ 33 | public void info(OutputStream out) throws Exception { 34 | pairing.info(out); 35 | } 36 | 37 | /** 38 | * {@code /pair-setup} 39 | *
40 | * Writes EdDSA public key bytes to output stream 41 | */ 42 | public void pairSetup(OutputStream out) throws Exception { 43 | pairing.pairSetup(out); 44 | } 45 | 46 | /** 47 | * {@code /pair-verify} 48 | *
49 | * On first request writes curve25519 public key + encrypted signature bytes to output stream; 50 | * On second request verifies signature 51 | */ 52 | public void pairVerify(InputStream in, OutputStream out) throws Exception { 53 | pairing.pairVerify(in, out); 54 | } 55 | 56 | /** 57 | * Pair was verified successfully 58 | */ 59 | public boolean isPairVerified() { 60 | return pairing.isPairVerified(); 61 | } 62 | 63 | /** 64 | * {@code /fp-setup} 65 | *
66 | * Writes fp-setup response bytes to output stream 67 | */ 68 | public void fairPlaySetup(InputStream in, OutputStream out) throws Exception { 69 | fairplay.fairPlaySetup(in, out); 70 | } 71 | 72 | /** 73 | * Retrieves information about media stream from RTSP SETUP request 74 | * 75 | * @return null if there's no stream info 76 | */ 77 | public MediaStreamInfo rtspGetMediaStreamInfo(InputStream in) throws Exception { 78 | return rtsp.getMediaStreamInfo(in); 79 | } 80 | 81 | public int rtspSetParameterInfo(InputStream in) throws Exception { 82 | int volume = rtsp.getSetParameterVolume(in); 83 | return volume; 84 | } 85 | 86 | 87 | /** 88 | * {@code RTSP SETUP ENCRYPTION} 89 | *
90 | * Retrieves encrypted EAS key and IV 91 | */ 92 | public void rtspSetupEncryption(InputStream in) throws Exception { 93 | rtsp.setup(in); 94 | } 95 | 96 | /** 97 | * {@code RTSP SETUP VIDEO} 98 | *
99 | * Writes video event, data and timing ports info to output stream 100 | */ 101 | public void rtspSetupVideo(OutputStream out, int videoDataPort, int videoEventPort, int videoTimingPort) throws Exception { 102 | rtsp.setupVideo(out, videoDataPort, videoEventPort, videoTimingPort); 103 | } 104 | 105 | /** 106 | * {@code RTSP SETUP AUDIO} 107 | *
108 | * Writes audio control and data ports info to output stream
109 | */
110 | public void rtspSetupAudio(OutputStream out, int audioDataPort, int audioControlPort) throws Exception {
111 | rtsp.setupAudio(out, audioDataPort, audioControlPort);
112 | }
113 |
114 | public byte[] getFairPlayAesKey() {
115 | return fairplay.decryptAesKey(rtsp.getEncryptedAESKey());
116 | }
117 |
118 | /**
119 | * @return {@code true} if we got shared secret during pairing, ekey & stream connection id during RTSP SETUP
120 | */
121 | public boolean isFairPlayVideoDecryptorReady() {
122 | return pairing.getSharedSecret() != null && rtsp.getEncryptedAESKey() != null && rtsp.getStreamConnectionID() != null;
123 | }
124 |
125 | /**
126 | * @return {@code true} if we got shared secret during pairing, ekey & eiv during RTSP SETUP
127 | */
128 | public boolean isFairPlayAudioDecryptorReady() {
129 | return pairing.getSharedSecret() != null && rtsp.getEncryptedAESKey() != null && rtsp.getEiv() != null;
130 | }
131 |
132 | public void decryptVideo(byte[] video) throws Exception {
133 | if (fairPlayVideoDecryptor == null) {
134 | if (!isFairPlayVideoDecryptorReady()) {
135 | throw new IllegalStateException("FairPlayVideoDecryptor not ready!");
136 | }
137 | fairPlayVideoDecryptor = new FairPlayVideoDecryptor(getFairPlayAesKey(), pairing.getSharedSecret(), rtsp.getStreamConnectionID());
138 | }
139 | fairPlayVideoDecryptor.decrypt(video);
140 | }
141 |
142 | public void decryptAudio(byte[] audio, int audioLength) throws Exception {
143 | if (fairPlayAudioDecryptor == null) {
144 | if (!isFairPlayAudioDecryptorReady()) {
145 | throw new IllegalStateException("FairPlayAudioDecryptor not ready!");
146 | }
147 | fairPlayAudioDecryptor = new FairPlayAudioDecryptor(getFairPlayAesKey(), rtsp.getEiv(), pairing.getSharedSecret());
148 | }
149 | fairPlayAudioDecryptor.decrypt(audio, audioLength);
150 | }
151 |
152 | public void printPlist(String methodName, InputStream inputStream) {
153 | rtsp.printPlist(methodName, inputStream);
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/serezhka/jap2lib/Pairing.java:
--------------------------------------------------------------------------------
1 | package com.github.serezhka.jap2lib;
2 |
3 | import android.content.res.AssetManager;
4 | import android.util.Log;
5 |
6 | import com.cjx.airplayjavademo.MyApplication;
7 | import com.dd.plist.BinaryPropertyListWriter;
8 | import com.dd.plist.NSObject;
9 | import com.dd.plist.PropertyListParser;
10 |
11 | import net.i2p.crypto.eddsa.EdDSAEngine;
12 | import net.i2p.crypto.eddsa.EdDSAPublicKey;
13 | import net.i2p.crypto.eddsa.KeyPairGenerator;
14 | import net.i2p.crypto.eddsa.Utils;
15 | import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
16 | import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
17 | //import org.slf4j.Logger;
18 | //import org.slf4j.LoggerFactory;
19 | import org.whispersystems.curve25519.Curve25519;
20 | import org.whispersystems.curve25519.Curve25519KeyPair;
21 |
22 | import javax.crypto.BadPaddingException;
23 | import javax.crypto.Cipher;
24 | import javax.crypto.IllegalBlockSizeException;
25 | import javax.crypto.NoSuchPaddingException;
26 | import javax.crypto.spec.IvParameterSpec;
27 | import javax.crypto.spec.SecretKeySpec;
28 |
29 | import java.io.IOException;
30 | import java.io.InputStream;
31 | import java.io.OutputStream;
32 | import java.net.URL;
33 | import java.nio.charset.StandardCharsets;
34 | import java.security.*;
35 | import java.util.Arrays;
36 |
37 | class Pairing {
38 |
39 | // private static final Logger log = LoggerFactory.getLogger(Pairing.class);
40 |
41 | private final KeyPair keyPair;
42 |
43 | private byte[] edTheirs;
44 | private byte[] ecdhOurs;
45 | private byte[] ecdhTheirs;
46 | private byte[] ecdhSecret;
47 |
48 | private boolean pairVerified;
49 | private AssetManager am = MyApplication.getAppContext().getAssets();
50 |
51 |
52 | Pairing() {
53 | this.keyPair = new KeyPairGenerator().generateKeyPair();
54 | }
55 |
56 | void info(OutputStream out) throws Exception {
57 | InputStream is = am.open("info-response.xml");
58 | NSObject serverInfo = PropertyListParser.parse(is);
59 | BinaryPropertyListWriter.write(out, serverInfo);
60 | }
61 |
62 | void pairSetup(OutputStream out) throws IOException {
63 | out.write(((EdDSAPublicKey) keyPair.getPublic()).getAbyte());
64 | }
65 |
66 | @SuppressWarnings("ResultOfMethodCallIgnored")
67 | void pairVerify(InputStream request, OutputStream response) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, SignatureException, BadPaddingException, IllegalBlockSizeException, IOException {
68 | int flag = request.read();
69 | request.skip(3);
70 | if (flag > 0) {
71 | request.read(ecdhTheirs = new byte[32]);
72 | request.read(edTheirs = new byte[32]);
73 |
74 | Curve25519 curve25519 = Curve25519.getInstance(Curve25519.BEST);
75 | Curve25519KeyPair curve25519KeyPair = curve25519.generateKeyPair();
76 |
77 | ecdhOurs = curve25519KeyPair.getPublicKey();
78 | ecdhSecret = curve25519.calculateAgreement(ecdhTheirs, curve25519KeyPair.getPrivateKey());
79 | // log.info("Shared secret: " + Utils.bytesToHex(ecdhSecret));
80 |
81 | Cipher aesCtr128Encrypt = initCipher();
82 |
83 | byte[] dataToSign = new byte[64];
84 | System.arraycopy(ecdhOurs, 0, dataToSign, 0, 32);
85 | System.arraycopy(ecdhTheirs, 0, dataToSign, 32, 32);
86 |
87 | EdDSAEngine edDSAEngine = new EdDSAEngine();
88 | edDSAEngine.initSign(keyPair.getPrivate());
89 | byte[] signature = edDSAEngine.signOneShot(dataToSign);
90 |
91 | byte[] encryptedSignature = aesCtr128Encrypt.doFinal(signature);
92 |
93 | byte[] responseContent = new byte[ecdhOurs.length + encryptedSignature.length];
94 | System.arraycopy(ecdhOurs, 0, responseContent, 0, ecdhOurs.length);
95 | System.arraycopy(encryptedSignature, 0, responseContent, ecdhOurs.length, encryptedSignature.length);
96 |
97 | response.write(responseContent);
98 | } else {
99 | byte[] signature = new byte[64];
100 | request.read(signature);
101 |
102 | Cipher aesCtr128Encrypt = initCipher();
103 |
104 | byte[] sigBuffer = new byte[64];
105 | aesCtr128Encrypt.update(sigBuffer);
106 | sigBuffer = aesCtr128Encrypt.doFinal(signature);
107 |
108 | byte[] sigMessage = new byte[64];
109 | System.arraycopy(ecdhTheirs, 0, sigMessage, 0, 32);
110 | System.arraycopy(ecdhOurs, 0, sigMessage, 32, 32);
111 |
112 | EdDSAEngine edDSAEngine = new EdDSAEngine();
113 | EdDSAPublicKey edDSAPublicKey = new EdDSAPublicKey(new EdDSAPublicKeySpec(edTheirs, EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.ED_25519)));
114 | edDSAEngine.initVerify(edDSAPublicKey);
115 |
116 | pairVerified = edDSAEngine.verifyOneShot(sigMessage, sigBuffer);
117 | // log.info("Pair verified: " + pairVerified);
118 | }
119 | }
120 |
121 | boolean isPairVerified() {
122 | return pairVerified;
123 | }
124 |
125 | byte[] getSharedSecret() {
126 | return ecdhSecret;
127 | }
128 |
129 | private Cipher initCipher() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {
130 | MessageDigest sha512Digest = MessageDigest.getInstance("SHA-512");
131 | sha512Digest.update("Pair-Verify-AES-Key".getBytes(StandardCharsets.UTF_8));
132 | sha512Digest.update(ecdhSecret);
133 | byte[] sharedSecretSha512AesKey = Arrays.copyOfRange(sha512Digest.digest(), 0, 16);
134 |
135 | sha512Digest.update("Pair-Verify-AES-IV".getBytes(StandardCharsets.UTF_8));
136 | sha512Digest.update(ecdhSecret);
137 | byte[] sharedSecretSha512AesIV = Arrays.copyOfRange(sha512Digest.digest(), 0, 16);
138 |
139 | Cipher aesCtr128Encrypt = Cipher.getInstance("AES/CTR/NoPadding");
140 | aesCtr128Encrypt.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(sharedSecretSha512AesKey, "AES"), new IvParameterSpec(sharedSecretSha512AesIV));
141 | return aesCtr128Encrypt;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |