From 9c00af795c9e6cbcf2c0f8ecf81a4f4eb2f3a1da Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 9 Sep 2018 15:39:20 +0200 Subject: [PATCH 01/30] Playing around with Smack OMEMO Sending not implemented yet; Receiving works only if user subscription was granted. The way the OMEMO API and our own application is designed, it's not easy to build something abstracted following the Coder interface. But since Smack 4.4 will dismantle the current OMEMO API... Signed-off-by: Daniele Ricci --- app/build.gradle | 5 + app/libs/README.md | 2 + app/libs/smack-omemo-4.3.0.jar | Bin 0 -> 102008 bytes app/libs/smack-omemo-signal-4.3.0.jar | Bin 0 -> 15436 bytes app/proguard.cfg | 5 + app/src/main/java/org/kontalk/Kontalk.java | 4 +- .../org/kontalk/client/SmackInitializer.java | 15 ++ .../main/java/org/kontalk/crypto/Coder.java | 3 + .../java/org/kontalk/crypto/OmemoCoder.java | 66 ++++++++ .../msgcenter/MessageCenterService.java | 31 ++++ .../service/msgcenter/MessageListener.java | 44 ++++- app/src/main/res/raw/service.providers | 159 ++++++++++++++++++ 12 files changed, 328 insertions(+), 6 deletions(-) create mode 100644 app/libs/README.md create mode 100644 app/libs/smack-omemo-4.3.0.jar create mode 100644 app/libs/smack-omemo-signal-4.3.0.jar create mode 100644 app/src/main/java/org/kontalk/crypto/OmemoCoder.java diff --git a/app/build.gradle b/app/build.gradle index 3a08e49a5..8602fa897 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,7 @@ android { versionName project.versionName targetSdkVersion project.targetSdkVersion minSdkVersion project.minSdkVersion + multiDexEnabled true resConfigs "en", "de", "fr", "it", "es", "ca", "cs", "el", "fa", "gl", "ja", "nl", "pt", "pt-rBR", "ru", "sr", "zh-rCN", "ar", "hi", "tr", "nb-rNO" resValue "string", "application_id", applicationId resValue "string", "account_type", applicationId + '.account' @@ -126,6 +127,7 @@ dependencies { exclude group: 'net.sf.kxml' } + implementation 'com.android.support:multidex:1.0.3' implementation "com.android.support:appcompat-v7:$appcompatVersion" implementation "com.android.support:design:$appcompatVersion" implementation "com.android.support:gridlayout-v7:$appcompatVersion" @@ -156,6 +158,9 @@ dependencies { implementation "org.igniterealtime.smack:smack-tcp:$smackVersion" implementation "org.igniterealtime.smack:smack-experimental:$smackVersion" implementation "org.igniterealtime.smack:smack-android:$smackVersion" + implementation fileTree(dir: 'libs', include: "smack-omemo-${smackVersion}.jar") + implementation fileTree(dir: 'libs', include: "smack-omemo-signal-${smackVersion}.jar") + implementation 'org.whispersystems:signal-protocol-java:2.4.0' implementation 'com.github.machinarius:preferencefragment:0.1.1' implementation 'com.afollestad.material-dialogs:core:0.9.6.0' implementation 'com.afollestad.material-dialogs:commons:0.9.6.0' diff --git a/app/libs/README.md b/app/libs/README.md new file mode 100644 index 000000000..8c119dc93 --- /dev/null +++ b/app/libs/README.md @@ -0,0 +1,2 @@ +Temporary place for the libraries I couldn't automatically reference from Gradle. +This folder will disappear before release. diff --git a/app/libs/smack-omemo-4.3.0.jar b/app/libs/smack-omemo-4.3.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..9c85001864201abf927e88f84c606607ce2ba969 GIT binary patch literal 102008 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 J4p{Rb{vQ>$hG_r* literal 0 HcmV?d00001 diff --git a/app/libs/smack-omemo-signal-4.3.0.jar b/app/libs/smack-omemo-signal-4.3.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..2e062ae5c7ca12b4f77358d7c2854b04d8bd50ce GIT binary patch literal 15436 zcmbWe1C*p&wk@2tZQHhOXI8S(ww+aJ+qP{Rk#?nRo0X_n=kMm37J)nO-<;B&6>7^AU7(s!Q{>2b$ z6mg#Wx1|CE5D?lw8_EkSNK1&Tsxin*B&*5Ct}!8XOOAgFb1D-ePeDzI4QQk5)OOWb zvuZsn4jm^GwK^Lrw_Ulp=9Px3g9Y=-PJiZO^|LplJA|Dbc>|VlPLILMRa)=RD~<=c z_Dq~C-r2=W=^Krwq2N!udS5=Q*#TOz7MeZT zAD~Caybj8V!746wVK?{hklgo7hBY3)6?#T&hx}3nj%=?1)WBhu5{eNtOeWHay0|Eb zNcZk9ce&;`sEwQ!@W*mQlsc9W@rpj*WuC*a;@T3;iQRaTcd>-?9p8Zs)Wv@{k-@

fnk@y&1jEE);pk%km?dKU0XtZ*K-8lN;0*X2IsT@D>qgE8i93zD4QFy?Y6cx zPLvem-)_};Oz>f`Xx&_sK#a(DhM9pso88|m9oP^R`R3}o{*$2?i#s^F;~+#SKm%`9 zcovolpTP6QKa*1P9+a@RwfgP7c`$=rExPO1Lc%b9t%E2I+q7G-aHdHF-x&7>?lHRt z`ZQIw#_NU57yMLo=kCW@fBYwwULval%lTDta- zzR6~byz@h!HiOM?FzYJbGyKVlN>_uSse{J(_bKieSx-Nwq&&D7c6 z%*EZ%$@Jd>u>L!Mv#p`Awa3535&RpRy{)OO{l7(%|6kG0mgaVbHvh#5kBS|2-0#0E zJ{b@Y|NpBK)&I|z;vcJ0HFb8jw6|k0wlQ>ezSM&DKn0+G0yqdfyA7B)gC&#_#z!>C z@Td*&`Z)3oaIFH%^DP@vAiRZ_XeyJVq2;AzuPUE~W68QVXzAmKLT4eois@}nHs?t# zGdbuV>`szHihLV86HIk98{Tex$DV<6-M1!t_H#Myx4PzXJZ|RzKy)U>Ff}6ruWjl3 zDI`^c)pw4W0X`ekj-dg(j;R5ij);J67_KcE{a$$$dqxT9t^Qku7ny+8xlByrfqpN#o2?$|VAM~1?bM@K9j_HajXZ4v8aek+ zf=C$FCDcYS^GU*`btA9N1Gpfuq50~Z#{zRtEm%?EXk-*P@<%T*wh`X-pmu3%*aa!e zZsf%Q`i%049$rm@_^%U(QIFs)S`wQ;UeOl5JWw%Vob2C zRRAdL5<~L=7=s0KFDdz&*X{-=!V1%7X)Lpa{L0If7h_q+%E^u0(HV9zL}($$QCb<~ z@3`{9Rq`YjY57@T2R|ccM~AaC$D{|K>!V8yv4u&yXwX=sxgw*WAU<5#arbQv`MDa} zYhxruc5QekgY^p~d8te)Eq5brk~Ff6IP3wzV`!M5`-hlVm;&wFA1FL2P>NgQk^SG8 zUnB|NS&Dw-QI9qihUU}jh?#1zp;W=~rP1L;ghhlAf#VO3$72e!Xj&)WNZo=VLuc?S zSNg-LjINW5C)=-|HRw_v3N;NeSgZ2kq&$Ytr0|Qp)$z zcIYmBS8Hdg1Nxyhb&!$`uf0f*NEtK~_lh?DDuvg@YRNBKR#e5id&pI{?N?q&8TU1e zep>m^z%*({&Q0dMPNLX%vnDT0TcNmvO12=(#nO;p>MnNro< zp+XkV!<9PqzPLE39~I#;v)Zq3ud>4wHX3sQ^eWr2CoD<41~G({T*0{x?`p#{4y1$V zr20UXPscPlL#i}>{`wU!=s9L>PD$xg!47`k!svc#VutWrJIvxc|T z-$4~+&vKKzKWILKzfEj8(|iUVt7i?u(zNVRL+IB?iw{S>Fb-C^YEzlk=(O^5twYx_ z6$Y)}RhUF0??s>q)Ots7brJi%xfv!*6RUW_vC?)=HI93jW^ zoV`a&BraW21SEk(P#hU*#uc8j4G!8rP9aZ3Ef`Q?(|urQ#lu~%x|Ud`eSP*Raa9}^ z3!SB!r0vS0rlB>XF*jD{K$uY#7k8YLAv+{o@+f5X$XHz}{@P^SpOY_KXQElIEkofI zHyzU-K(mJAqQ@3Nc&5gYBkw>QTjynvmX@Dm)OJnof{m}uJJu{<%V9i9oy3D0?LS0!djfcE@^UmEp z*Ecx9z5M$So5Wf{OPWbSC}%ci58cycv3=}>mQ9^|F~z>=%Vef-NZlzj3vMGVkw_=I z7A@Sn97zY*?Hzq(1`cSnHFk;l&lHk$sG+8NUo&D*HX-xY>#q67z;m?Y35h(%DTupt zjOYj`PxBr|(Cs+l)}i=R=Q~@k6KTYm2k;QoF-qh$`JO2EM&PGJEpseWf(eHZ2y2&m z)+kbEP`iHgRS<(6$Z!!yUtQP*co!!M3mtq=>z}YB6^IQKWW4o7V{z$gQK*FVI9CLV za9>QvWCuUL8`zT}mfOq?IId~uSXjBH9#wD#j#&0Y^^&YW7q!hJ#|P5)^5r!r?gGi4`tM&!QKow5F1rk-5tqzKZfMgvw2h@aV#SHw@_#L>fM= z1&$sageZ`U%K!CUMWEK`^C3W>E^u)IAi*ucCj=y3wA(C3@5D71WY$-5k|C8rF77U# zsndTxD|45y7GX=(cOI^xs=JJ|j2kvR*vt`gu8X%G-uPWRc4#iNtDj06>KOx9ynQfL zWLG->rbx*{7ww~kAJoR0O+p-vzt}c%OJ?X$GJae8lL_X;UXBn3HzuG}V^(lgv5%F} zzhGbKr;<t?+PG5?6f!tWbzt7=5%OjX0%m{r)x~i< z`6CyX7S6hoBiKIG&=n-(iMX)eqq3YualJ|h@}uFNe^MYGlyHUxl&aLLoXYlfnI|>W zeV#ESwwfcf8;Kl}SpisfSRBEi;{*5P4waU4Dyf)bcO%a^4j`o;InbWgNK?YEopd0CVjl>G~^qokhkd}#|ikW>l>>|51e zKPHMAePauSIRMHW-9-L(A7un(;qSZjig^KIvd6M(T1aEM)PQ%kqmzqa)58T=gu!F4 zgf>jJq|TJs{X4K4uBhPdm3a>|*#q60CS)m%@=JF3?y`a}6WY{Jsf5-|hH8|Cx`%Q` zrXZDJGj zH_;w&WuBiDwqQ7?Ex4~~dv~F@+I5Z)r23ZX?L%l{n$0;=I*KdF<+G#*49e~@Lo$2W z&rL_Da=Gpjxawo@>pajEwh74_CT zZGXbuqtcVSy@LWlG+YzrjeI`Wj2}1SjAt}`OZf%-uc?3g^BbNE91svDH4u>4f0F9D z*gKhu+S}Qg8vmaCBjfe-QTu`Zc|~@Uuxm;z1m#TFZ%227jJMhp=~=1&0?(Bj5& zFGV0{R$@=#zo=nrd%m=$ovLjesUxet1|p-RobPH~EsNP)Ez5b?v(}wlvF&?*G?f`_ zLJ`ob|E>2k{&_3QeWrbz`+NpE`dI*I2hFhnccQ)#DY{MhtDV!n*+#*R2DpAw zZ?9UI-b+Kk`Ucg|G{^?SwfpFVo#$%=uKw_H(KG?)lTofuhibnb%u^>b_Tb$!Ft|Hy zTPe`9OmrZ4+e|df2BSXz&JkMZ0a_^RjVtwM2-{1Z&vRTCU)2sWr&ZlTwB)Tk^=nE1 zV51Fa+pQ2Gc49(-+E3(bPEu;=!6EP6ogVl$ZQt|Z{({P3IX-I_RJ1NSrXJtR3iWC|kcIVDFUam3|1yFbFp=(Vue#-h z=}t2?n6iAoYQe=_c&}HTydHj&2d|d1Ky9)n@{UbwiyW!m7=dCr}1@WVj(2T)q;D_C;Z5kbUT*!*8OD! z_}Yc=)%Rx-#!H2g*x2D-g&F4A`HmK;N3htRRcJ+#8G`QY^eA(p{Z(v}R1izp`4*#? zFl=uh5Xq8rb3Ou!))O86Ip41)rk>G3g}({m!mGgfMG{<*N~W+b@28#$^LOYPsM<0- zjOQr0;)31ji1;Pc73m>bWrKe3XVh=4qvjP=@gLknaCpG$eYOO>Di2^<;F!h~QBhLZ zK!XQn#45Vj>5`3n-W^zm3@oWJJic&!{8D+@rUf5cX!uH>k$~qf-t5|5tSS`eTwPcv zzP`IKpiC0xp)C@^j1*(#mnh7(fb&gKM|@M+%GF~~!!-%DQ2Lmm`b(@Bd@bJc5$0%R zwe@}m@-2duqZr86nsF``C9&xu#t7f$93cWLJVa_;U4AgX`K^2F$HM7+l2nNnIL=6b zxeR1lPzfj1U{v2wojlIGxAM6o_d5x8 zO@hFT1+3#NB4bPSMxpJrhE(bJ{ZcLl8p#4Q8RCS=xa>uhuL#Lf4>u zNsiFp=j6jYZs0|=;D&f(L8s@^>$g{bN9e^&%h2|tg?RX`pR;;gbWKJ3))hZa|Fz>n zU%Pw{@ujfqU3xh8HX@+6?1lETWbgVVG~ips3+89_o}kAMC)Rbn6d`dCRcEpSY?U=D zEMn1-+pvgVd+fl9F_C`j|>ItwC)%wF!9)>g^>k z=r}E91LXTWq@KUx@VMhczhKeD*G4!R{jTSf`=-2Vs`ZhBD;y%BE0n~MQ|H7!*l|PL zr7-eoBE+GABUIGohAJzlP!rXdwd9n=+tVr{V6exAjcI60!t))+QGrnwhFsJqkyz20 zWzidl@zmAEZ*)T@z$;kP^<;c1hCKWa?XQrk!%)?jQ~fc16ok-o2v;=o>u`(gB+=oy zt9^gur@`IR@@!zkFq2o8%_jy#X@L?VDd_ ztthW#+{H`&&1ey#`Fr2X)e?NQUbU`0t8Yk>@c|}@Jy#XD^bc^!?d9Uuo{^+mjEwjZ zOfgYrWX$BX2mk2XoRnQ->Sa`zm38abGpP>#P!r!=1OIjfkoyOnw&1J@C#7#qFgoRK&+S520gjRdQ5JX9>M6#YJy z>FoYscxrEr(5@kd;FpqoZpt8AY*_;{+jXf)T6r)^f%AwBv#E=yhWhr(#%gDDPM!@& z7p8u^DUB3Z*(v#NHmu2D4$3ix`RPX3>kQX!Z&)b&S7S3-(L)Mml~>OvAG_a=;WoF8 zEH1__K>3@x=b;{9Cudfi5`KJfU~9H?Z+M8MEDVZ_Z`J{u#tBTdTsoguL}}n`P<64^ z1*fF2-K0(k#WK&(5%CA=P9EfAh$o+RmX$suV7&e;!&6Rl2GKOhXeWOOI2$k6CFwux-fsFmY;DUp0J zqb3SmMEix&?0A6OSAz`8?n9N?qKtlL+yn~_%<{2{ARQSA97#?c z;14Pw^rf#Vll>A|re_{>qnR(91V+^)wJTAL^g@qa6OJ`qn-m+X9#`$VdA z=T0nW?;bj~$$bl?Hg7+zjqdyni!x4Y-U!)NgG}5oEa5q^|JaaNz+j)+Fxh_SO|B@? z#w3!?gasyeR5ep9(TK->HXEx;gGRQPL^9&2JN{#o=m$d^VnN7NZd<9SQ_okYt%r^nlboc3NAjY#;-s%3uWcJ8Z!TtD zNbBL2J>ov$HT@FH2KA~x1yMaE9RdO}lHa}cc*^pW`w^fpfB|sK70gQl6X}Opk^3*l z+tID?g)OAzT&E&q!i%;>ODmYi*YPuuN`%%t#cbT+5=EemT9$q4NqVxYKge#_(}HK1 z443PH_3U{T`Z^d6n~s@_8DgZ=ZjIK{hg}{6n*<`UqoU7SbW#`jauw^{k9CF^MZR)o z^9G23V)HUfuYjmzBZq}L z$_TAm8>XkAl=^mEmp&+roH0xe*F{@)fT}h}m#_JU&#jt>AERi9z%2%=CFhxuv$`rn zu#wXhnXo}pdw-vQqw38)bR3mn9rHI!i8f0a7KD{g4L)L7+MnzEkPdZp(ry(b&$RnR zWch(4yK9I6C@ge)i`_@1-uyf$vik-uV42^wvNokg&VdSlsd4MJ$eJB_-H(FFQ3C9h zC4U1A;+23KT#EO74^O4t_9xnTLMg7sRx(|+Sn$Y}o@sCA+SaMA=xXx?1Sy?(X-Ug| zspWpUa%#c(WE_+a$GytKQz5;0scAPFzvS4x$sDY1#8PRyD*=Lz4#xKtQlU|Jeq< ztf{BEi>1xq%NcD*54>UY&s>*znaX5SG9B<$G*F2tf&G|H^7R)X2BLK$Io|B0yYz+4 z*DI5W2L%gY6{zwcFcB(Z4q{4NWG1pCrNlTH5fEr%;v!KTX(D^KXASSlWV>gFo;1d% zXHPfw!*9R3Jg=v^+I`QmAqha30&C)s2PRm)_Roml!a~=M-|QZ;WQFRd?D}+3MCePu zG)Jj)G3;}Ch`PK~pM3B__om)JVjJH`Vxzw#1_@4-4P_R1$PVb6L;>{0Z#kjAr7F{E zTZvBynk??iL)%ZJg?$_Uowv~6Y@>jG=_NJB-l7-B6s9Nu35OY8po}0s14p2Qf&Q=UX0t}jTVjCe(%kFy}o z_Ymg7QJ^P5<6yHRdRa5tq{%Pf$5a3E{ODF5?%H1iwXD_x5a}NcF(9G)2_pV}qc zU3t$39kcGk;^*K!ulh$6qs!F5?m2tQSC!nWU6E zQ7*J1NmFV5Db$K1AFRsT=4;E8MI>i!vDnwznAesebtP4@xK$*Ga>XKKON!;Fi?O#B zG%hdKP(qAkVY7|0f${KMZ}Dr~!0EoiAPZ6{%VF|FC4qY6{#=u?JYTaQ6QCaS69qb@ z<_ly8Hnk8H%<#_wY7x(k`OL%E=21IDN>w-_+a+$K3cPT2^K&0bzQSh?+$E5i3ruF8 zV~2d$hYBgCwS7b}Rjb&Hv|LO#Pss@Q@5t;UDD(*y%=v1p{%ZOG#||C$UKgwou3Bb9 zV3nI*lmkZ)_7do$hCw{wm2eAEVWm9$p-m?(24ovkg(=UWiykz1kuc{l>%N8+Ulily z`C58!rD40#!!_;CW0J-n?{PU=js*Gj-I>tI&}?jyEuh!y)kizhs7VyxN206oJQD_S zpq%f_bg^a1O4Q57$gD$eBt&D7N?i5PqaftHC+Av8(Phipy+T&|Py=c?Owv2(@l>2O zJ57_1IlTfnDJE!?j8aySw7Vv=t7*)pV`mS(V&Ywk)Ki6mBgC-YE5qea?xA9@*06~` zBv==42b6D4Z6w~{jodXkei?Kzc3>CTZe6A@D%YgK&|2Hf-s$HaDa()h=DEJsQzON7 zV0%^VJ9j0;z{qsc8oZymO2NJHrtp64aRY4Z@e6y<56cBHZSHL*3M%$OvNXWw*=w6Z z;4J?71a$ZIyWTM&0U}uUFVy`VGzR1iyG8AWq1BqkAv#|q1e^fY`~KCdyM8({di11( z^dNdknaj!Ir4$vNBToM3cRafcIuDGh@|-%XV8)KZY=n{)*nZd8Vx*^fK$@7CY2@`SRZc&rO*g$o2g6*n+j;jR4>aiK; zZk-so{lVB>TVBd>LvsQT({GSpU_V0??5z4I&%L=R=2Tggc0gI+ZWz_j(b6{qGq{bL zTzfWMzrUpIh1~EYq?L4-p5MsdNc6Ln&C(`1h`ybqkE#?scAd!?>a12a5rZi)P?F1@ zlHxu@7RIbZfR(Tug`T|1rq04cei?&3X*_j$obORkP5^*JsWGro*}OSuI0XBq{PXcE zJ5N#^{e-bAo~~sBX;dHvfY9#bcsq-1%pcXs@^Ut{Baev3k}Qfe2T z>|N7pS5fs7Ijb)xbTHCGu(k6gx@xvH`AUek8dP#RXgrxDFm^|rM~jt8NeaqT5{}9? z>Qutg;VLjWd9GQIY+-oij@p92D38=l(JiQD!M_Mglmx?73tGDa5sSA5zl$8twxeX1 z&c5z>`io$mmAJ(NAJebVx-eHKo}dIX6iU}dJHzCRu)^34AERvq%+Ry}b2bp% z6a@ervjYGQu>q1dWdWf#bpepK?BIw7O8s^h$_^ZFJo5$H3>&38+P7}x0(mY-0 zeYE;FL3R{2iVbRk+CcTy9C&-;LhEsNN0`VE*AWOi8hbZ)KyQ`AiO2=C!s-pbSSIhL z3D%9EP_QMg&u`<#HbzGjvJ$@Rf2zJ*Bit*>J|8K6(M}v~(x`}Ud%e#5L*C=gZjt|1J9e%UpK`1)q%EjKsN1slJs1-GdICi>orY4#wBg)7B9ekhs|;68!38Fw6btV?LtGlIe7_U4e9 z_hsgLUi8%e$Yi9o8BR%$5R_-ya^myx=iVNM%pB~N0$a|H-PnjG>2l zB?B1f34he2Bcdw2t%n_)<4RZj8{K?%QSf}@P#q-MS_3JSjFMHZ9*&ocN?3n8Ws zYai;`9I4Fl5=GI!lR2KTLMXCBytx$`l-gd1OlanEw_qw?k&pC*h)BKLROZ&<23JDv zp5qD4@ogvZQ&CLPR?w~u0_=V85{Go4Z(r+OzB*>t)^Ndla*hwFa=*s7>sQ@hE{Z|T zK{h(%B~Ngu8DN(faHDGZlXEE)Hiqex7|Qn_?QnxuZCvC>o$ISD6J@&mGMT4U%hIk+-Q?SF;=|2TwS( zhbR|Ml?m++%QlD@716TE-<~G?x)ZWMYzBY-V8CGx0 zSZB>!zWJsA47bjel3Vai*%@d5;(Mt!P^4TRi4W4UHN~)2Sidc8EY+U*E76EMN2^@< zJ$#;aAG-EV0M3S7cOmqb7{uK{ZEtRYfLN=6aYs~vlJY7%&PK;m*^0Dof&T6X^tU33 zeN_nfS)0ZX*BPkcHl#>|;d`jcNFQpn)3fX$; z$jg<3mN|oGcs7t5JOgJw1g(~pgRhzW^wS$bER~STjBWBO?J_G)kv~#o(_~uc?T<73 zMlS?KrVVu|aa(lr#-^mII7$pO9s2D0v*a=~_7>u-A(-nM+Z}*Hxsa%XZAhJvmvtw{ zdxywW%!pNtQOd_CoVv21j$0w(zlsv8PX%{6mVt)v=J=v7>rQR?L`#b_#MODZVT2og zPNugE?h%;jZ{SL5ZZO^&MGPNS`L^vQM-20CAh<3-g|s@S?OqP}LY&xE8SN6wLNJ#% z#;H3G4VAn(n%obu#jHh+t$E`};?9KRS4ixyHesSd&hibJqUd0)=tZt-0UE9s8?W*D zw^);>d^vF6jqGsmCdnyg$X{TtmXGSiU}3=5*1vF@VVIq=Ut-N`UyQdIgLNZd5!-S; zarMcy10b9g5z-);PJmc{Oq(r3G5tIe+9Lq~kJyJwAAk3RLd0c?gu_ZVEV=Aw2IF1I z1DEBfH=&1vt?H&oNuGD%o!mmEGBqIRFNB!%0hO?FJGf5+mtyfJ^TFSq{Z7F8VOb8W zHtl0p#JqrfMGc$fia7rAW4{DjYu79q?D-wa0h`wYL=ne2-W#dAfKG~V(xWv)s zkne+8-y5yC{=4zqz;p^Q9S%w{WZredQ_qb5F{Bxg5tFJ(Vg4!!Uby`o9qh}8C7&^r zy*Vc`nl)s7TOu^wYGCnzsrKtf7;w{S=_Beyla)Mna|URHC4b~`l#pYVzb<>2DTwE* zUDXc25meI>(svldDmry$Unhm@;j&&9`;!;q9nR0nv9jEg5u+5jE>L@>Cw<2(*b9dd zizm%Y2%VF;cVBXlddbn_p}NWyN1=!#8dVp`@Vm|pzcGc@-nw2GVZ>yVGbm%Xf(Zg+ zz1w%I`4}xcvjt6d9Nl*5Bj^_V&{d{BEB8jv4K4g+%PL36n>1D-OC0M>V7@&#Chlm| z-;)ElJ|3w#GK)U@d%dX6^~4%4sSRV8rsjhrK3Bs$#0XtJ-$bWbm`OT8YaBe@LD zxLBj97_DV_h@+zm4aX1?JC@TIEWdJZ@)uNQUny|A^PqAM!Xh*0cawT;q+f+??8+U` z)r#4!9K#h86@-T05#==9m;-QJ2U%d&zy9_l-IkgvQMYVxd`1-0MqjF$qg6$~9G z&)cptRdNc!?44$QX>e}h5pImXR(z&II3q!xGWvGGz(cc{J@raCsJ0DVtt7 zm+!t6{_2+?Jvp5eJ9wjZ_=sgaP9da;EGsyY*W3AoW4$=wLbCYSd_Y}Ed$rnwIP%PR zD+SxFH0X?*@#<$}>J5JqBvjKhTvrVe^SMAcK6S`1rd=A-x zj(AKg0GB*y3@XNI36v5&BXMN*+@xp}_S9%&UC~?5wdF#|TSm(%%09sf_3`3s_3=aQ z=(qW&9&BkS(tv{neJ=&zsgv&Zw~wprx0mDI&nI`Fj~7|UwHp+`#ARE~5W+^>`4_GS zvd#NDBZo9;mr-#TPXuC4^8_WOoizD@fhn>h1820vlK@i2nk1F8K6(#4KNRnHdr6|} z{I1D~2@#Y?&4l+_C!e7GLTS7qK!TEE;Ql^duGAisz}PKy`Sk)`uAv2C=A<4$2A(u4 zU$$d+fZIX_W)Cka-AT^udx6~~y_KCZn%-0^OpKj%`w0hmtJ6ScD;)D^9VIJALe6Bd zJzR8@qJrK;ktAc0B=!m{Ty+oB0E(#W*@}9|P?$ALNKMpX*jQZ}=9VeH{lfILu22WX zgNkRi93xHvnr6+uvro$YJ>K6sGr4-kZ`osNoqYoLGx4^KxC}f*5Qoar=0thJqG5Fv z;S%L`)8Hso0WFU9+2QY}(`_Mk`ak$(wN*K&ktekUL?zMui5|d=uQ`aHY{c4ySY!t7 zQ{RQqe5YFO4%=eV2cWAAEOqHjT_e`jPj-H* z&(fOIT>go0kjumzX6prc>)e(?&CpCKk+XBS{)L=N<=Q<~fR6>WF^S#s{wgJCZ*hHB z^Mc`E8?&BX#i=!s=im&?3Wj`%*asm!C($3X_&;1P$k`tb?Z}v~Pqz{}4CdxnRg4s% zTt^<&>dP=?Ta?TL^9~R#)&>Sd^!uUTOzOr*Ue~H;N!DTWO(V8M#&WJ>H6zu2R8WI# zHhffsv(c?sWgFP~nHR;Blh2TF`{QC`$V)b9q4if;SD5euON+&pgHEHK8AFPbm-)g- z+-3wAb4}kY`o`F(KtTUw=m9v?Kx3m|3?9_AZ|!EFDwVR*c;L^-1oq506omJT-hzVL z?}XwBs@xdA2Yq#%O3ht-Qw0|soI!sF={Ykotvn%Z%&CMP{nuLASBCcwjAZF=k4DlZ;g{RajrFUu2@(&0PeR?CcFrnn_y%w6WwkD$Boqn zq^4uahE{;RHxHDC1Kq zatcl|@gOjqFfk^jmGD04r_vioG4oqbiSkYksTcRtS2 z>_E}v5dGBx_z`!~Btsc57kJ2G9}v8^TO;W^%GjzM61hHH_zBhFnP6H){$t1mL$dmI zuH5nGT6w)=O@?6$a>-BULNH)PLc!CZ1tmTXxeyg09ka86;0N-M568Gd-8H*>q2zqi z6N2pFrzKB`LTGXxW)4rk2t$2-J85FXS+8<5by557z4az_?uQLgeT8@LxmHNKF7X#x zTME3Yu9>r!(OD5(INx%5{PJxsf-(%hsHSr4kmX;Q>b>IPY=deX^3zWymtfUWxBT68 zC|{jUrAX~n`20DFfG&ho6bc^bsb|1kj!%Rbf1ZR(RHn{>yxzaCtl+M+hu|-4(pO4t z=t-Ol9(34xp}!+&TYh|L+Kv&+DLD620m5NjiyN_v+BY=GGeY4{BRLcT;dOK0brCDi zBQ(Du6Zl?wBg_{K47W#@L+pj4aBH)gwBTayVy`a>NiE-__u~h~L?y!`YH*Pye^11;Yv5t8mp|rMU8)sWa|*aRT(Vg<@gN7zx+1e|A_xXG7n&oA z{T@P^TAvl#1`&AeY$!$f3IT`kl9N|M8-(f4p!jIxy3GXOR&Ms&<{V0IjBI`hL zoZRKH>A*N$9tN=!&+il?&;uyw4q+XZNag4cg_7{mr3_N{Mx&BwKG+60)RX??2&OyT z62B9aQ$Zitf8(fAW>8YoIyG^{_o zZt3Aqh{VNIKyc88^7<|M0b^ITyq8xB>DjhZy(&sDFrPrb0&73vZm%f(W>e*VQoPi$ zK5y|K`#FT(KLSb_fjAr+!+Dl`b!`g*9wDsSj`llZaY1Cux8&6xq9|&!W55XDqBfJ? zeuk+u?#CpPr;pHZdC*vv%3aq zTW0C~IQV1xTCh!@O_HJ!n18N)s480{n)mHYs(Dr@G59#DV>uw!8RU7IOb6uBb34~P z(zJ8hY5He+s!8Pl$Mf{D=aW}i19+8-0ufKHvZr&vnWV=xtV!gI?uS1D-^K`vPBk?F zW}WVVUcb1+K|7iEYp?b(XqS60>SE;1X76Y-wr6vn?w-As-iWrN{wuVSEHDTf=>IJ3 z`yDyI2NKYw@E`C0ztHcmu)hlZ{xJSy6#4(|$@~-cPqM$idicZopY>aRT>p(Y>G#Kf zsJZ?f;tw^~-^L$HZTmaoU$kBS4*rL>>reRUe+U1&((B)O`9taTrx#+-zb*S;7V&p4 zf75;aJ70h3zW(&3|9|!McNN%QUH(-C_7A5;{zd<_2|M z|M{Ev`|AIh>c8p9{VU^NPssmdG{ybjGycnY`CqC2x|{GPRT0hqit2ydTlg#EU#rVM z86oKZSB!sG^Ziq;`Cq91a;x~0s)+qRp!##aL`fF>_uU5w2>$m&@3$y(mGh6Q{{!IS B2=o8| literal 0 HcmV?d00001 diff --git a/app/proguard.cfg b/app/proguard.cfg index 61a8a6914..f1c1d4fc6 100644 --- a/app/proguard.cfg +++ b/app/proguard.cfg @@ -82,6 +82,8 @@ -keep class org.jivesoftware.smackx.vcardtemp.** { *; } -keep class org.jivesoftware.smackx.xdata.** { *; } -keep class org.jivesoftware.smackx.forward.** { *; } +-keep class org.jivesoftware.smackx.pubsub.** { *; } +-keep class org.jivesoftware.smackx.omemo.** { *; } # keep other Smack utilities -keep class org.jivesoftware.smack.**.java7.** { *; } @@ -129,6 +131,9 @@ public *; } +# WhisperSystems (libsignal and curve*) +-keep class org.whispersystems.** { *; } + # OkHttp -dontwarn okio.** -dontwarn javax.annotation.Nullable diff --git a/app/src/main/java/org/kontalk/Kontalk.java b/app/src/main/java/org/kontalk/Kontalk.java index 577161a42..c0249835c 100644 --- a/app/src/main/java/org/kontalk/Kontalk.java +++ b/app/src/main/java/org/kontalk/Kontalk.java @@ -33,7 +33,6 @@ import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.OnAccountsUpdateListener; -import android.app.Application; import android.content.ComponentName; import android.content.Context; import android.content.SharedPreferences; @@ -41,6 +40,7 @@ import android.os.Handler; import android.os.StrictMode; import android.preference.PreferenceManager; +import android.support.multidex.MultiDexApplication; import org.kontalk.authenticator.Authenticator; import org.kontalk.crypto.PGP; @@ -67,7 +67,7 @@ * The Application. * @author Daniele Ricci */ -public class Kontalk extends Application { +public class Kontalk extends MultiDexApplication { public static final String TAG = Kontalk.class.getSimpleName(); /** diff --git a/app/src/main/java/org/kontalk/client/SmackInitializer.java b/app/src/main/java/org/kontalk/client/SmackInitializer.java index 41be712b0..ca7749529 100644 --- a/app/src/main/java/org/kontalk/client/SmackInitializer.java +++ b/app/src/main/java/org/kontalk/client/SmackInitializer.java @@ -18,6 +18,7 @@ package org.kontalk.client; +import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -27,6 +28,8 @@ import org.jivesoftware.smack.roster.Roster; import org.jivesoftware.smackx.iqregister.provider.RegistrationProvider; import org.jivesoftware.smackx.iqversion.VersionManager; +import org.jivesoftware.smackx.omemo.OmemoConfiguration; +import org.jivesoftware.smackx.omemo.signal.SignalOmemoService; import org.jivesoftware.smackx.xdata.provider.DataFormProvider; import android.content.Context; @@ -64,6 +67,18 @@ public static void initialize(Context context) { // we want to manually handle roster stuff Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual); + // initialize omemo engine + SignalOmemoService.acknowledgeLicense(); + try { + SignalOmemoService.setup(); + OmemoConfiguration.setFileBasedOmemoStoreDefaultPath + (new File(context.getFilesDir(), "omemo")); + } + catch (Exception e) { + // this shouldn't happen, so we just crash for now + throw new RuntimeException("OMEMO engine failure", e); + } + sInitialized = true; } } diff --git a/app/src/main/java/org/kontalk/crypto/Coder.java b/app/src/main/java/org/kontalk/crypto/Coder.java index 513542b3e..4fd3150c3 100644 --- a/app/src/main/java/org/kontalk/crypto/Coder.java +++ b/app/src/main/java/org/kontalk/crypto/Coder.java @@ -78,6 +78,9 @@ public abstract class Coder { /** Basic encryption (e.g. PGP). */ public static final int SECURITY_BASIC = SECURITY_BASIC_ENCRYPTED | SECURITY_BASIC_SIGNED; + /** Advanced encryption (e.g. OTR). */ + public static final int SECURITY_ADVANCED = SECURITY_ADVANCED_ENCRYPTED | SECURITY_ADVANCED_SIGNED; + /** How much time to consider a message timestamp drifted (and thus compromised). */ public static final long TIMEDIFF_THRESHOLD = TimeUnit.DAYS.toMillis(1); diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java new file mode 100644 index 000000000..7667b0c2b --- /dev/null +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -0,0 +1,66 @@ +/* + * Kontalk Android client + * Copyright (C) 2018 Kontalk Devteam + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kontalk.crypto; + +import java.io.InputStream; +import java.io.OutputStream; +import java.security.GeneralSecurityException; +import java.util.List; + + +/** + * OMEMO coder implementation. + * @author Daniele Ricci + */ +public class OmemoCoder extends Coder { + + public OmemoCoder() { + // TODO + } + + @Override + public byte[] encryptText(CharSequence text) throws GeneralSecurityException { + return new byte[0]; + } + + @Override + public byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException { + return new byte[0]; + } + + @Override + public DecryptOutput decryptText(byte[] encrypted, boolean verify) throws GeneralSecurityException { + return null; + } + + @Override + public void encryptFile(InputStream input, OutputStream output) throws GeneralSecurityException { + + } + + @Override + public void decryptFile(InputStream input, boolean verify, OutputStream output, List errors) throws GeneralSecurityException { + + } + + @Override + public VerifyOutput verifyText(byte[] signed, boolean verify) throws GeneralSecurityException { + return null; + } +} diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java index 17aa43020..ac2e23e07 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java @@ -67,6 +67,7 @@ import org.jivesoftware.smackx.iqlast.packet.LastActivity; import org.jivesoftware.smackx.iqversion.VersionManager; import org.jivesoftware.smackx.iqversion.packet.Version; +import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.ping.PingFailedListener; import org.jivesoftware.smackx.ping.PingManager; import org.jivesoftware.smackx.receipts.DeliveryReceipt; @@ -455,6 +456,7 @@ protected void log(String logMessage, Throwable throwable) { private WakeLock mPingLock; LocalBroadcastManager mLocalBroadcastManager; private AlarmManager mAlarmManager; + private OmemoManager mOmemoManager; private LastActivityListener mLastActivityListener; private PingFailedListener mPingFailedListener; @@ -1019,10 +1021,15 @@ private synchronized void quit(boolean restarting) { disconnectThread.start(); disconnectThread.joinTimeout(500); + if (mOmemoManager != null) { + mOmemoManager.shutdown(); + } + // clear the connection only if we are quitting if (!restarting) { // clear the roster store since we are about to close it getRoster().setRosterStore(null); + mOmemoManager = null; mConnection = null; } } @@ -1931,6 +1938,30 @@ public void run() { } }); + boolean supported; + try { + supported = OmemoManager.serverSupportsOmemo(mConnection, + mConnection.getXMPPServiceDomain()); + } + catch (Exception e) { + supported = false; + Log.w(TAG, "unable to determine server support for OMEMO", e); + ReportingManager.logException(e); + } + + if (supported) { + // we logged in so we can now initialize OMEMO + mOmemoManager = OmemoManager.getInstanceFor(connection); + try { + mOmemoManager.initialize(); + } + catch (Exception e) { + Log.w(TAG, "unable to initialize OMEMO engine", e); + ReportingManager.logException(e); + mOmemoManager = null; + } + } + // re-acquire the wakelock for a limited time to allow for messages to come mWakeLock.acquire(WAIT_FOR_MESSAGES_DELAY); } diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index 0637c656d..6ebcc2b5d 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -29,6 +29,10 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.forward.packet.Forwarded; +import org.jivesoftware.smackx.omemo.OmemoManager; +import org.jivesoftware.smackx.omemo.element.OmemoElement; +import org.jivesoftware.smackx.omemo.internal.ClearTextMessage; +import org.jivesoftware.smackx.omemo.util.OmemoConstants; import org.jivesoftware.smackx.receipts.DeliveryReceipt; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; import org.jxmpp.jid.Jid; @@ -300,8 +304,9 @@ private void processChatMessage(Message m, Intent chatStateBroadcast) throws Sma boolean needAck = m.hasExtension(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE); ExtensionElement _encrypted = m.getExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE); + ExtensionElement _oMemoEncrypted = m.getExtension(OmemoElement.ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL); - if (_encrypted != null && _encrypted instanceof E2EEncryption) { + if (_encrypted instanceof E2EEncryption) { E2EEncryption mEnc = (E2EEncryption) _encrypted; byte[] encryptedData = mEnc.getData(); @@ -340,6 +345,37 @@ private void processChatMessage(Message m, Intent chatStateBroadcast) throws Sma } } + else if (_oMemoEncrypted instanceof OmemoElement) { + OmemoElement mEnc = (OmemoElement) _oMemoEncrypted; + if (mEnc.isMessageElement()) { + try { + ClearTextMessage decrypted = OmemoManager.getInstanceFor(getConnection()) + .decrypt(m.getFrom().asBareJid(), m); + if (decrypted == null) { + Log.d(TAG, "could not decrypt OMEMO message, silently discarding"); + return; + } + + // encrypted message + msg.setEncrypted(true); + msg.setSecurityFlags(Coder.SECURITY_ADVANCED); + + m.removeExtension(mEnc); + m.setBody(decrypted.getBody()); + } + catch (Exception e) { + Log.w(TAG, "error decrypting OMEMO message", e); + + // raw component for encrypted data + // reuse security flags + msg.clearComponents(); + // TODO what data to keep here? + msg.addComponent(new RawComponent(null, true, msg.getSecurityFlags())); + + } + } + } + else { // use message body @@ -386,7 +422,7 @@ private void processChatMessage(Message m, Intent chatStateBroadcast) throws Sma // bits-of-binary for preview ExtensionElement _preview = m.getExtension(BitsOfBinary.ELEMENT_NAME, BitsOfBinary.NAMESPACE); - if (_preview != null && _preview instanceof BitsOfBinary) { + if (_preview instanceof BitsOfBinary) { BitsOfBinary preview = (BitsOfBinary) _preview; String previewMime = preview.getType(); if (previewMime == null) @@ -465,14 +501,14 @@ else if (AudioComponent.supportsMimeType(mime)) { } ExtensionElement _location = m.getExtension(UserLocation.ELEMENT_NAME, UserLocation.NAMESPACE); - if (_location != null && _location instanceof UserLocation) { + if (_location instanceof UserLocation) { UserLocation location = (UserLocation) _location; msg.addComponent(new LocationComponent(location.getLatitude(), location.getLongitude(), location.getText(), location.getStreet())); } ExtensionElement _fwd = m.getExtension(Forwarded.ELEMENT, Forwarded.NAMESPACE); - if (_fwd != null && _fwd instanceof Forwarded) { + if (_fwd instanceof Forwarded) { // we actually use only the stanza id for looking up the referenced message in our database. // The forwarded stanza was included for compatibility with other XMPP clients. // Although technically it's a waste of space, and the replied message will diff --git a/app/src/main/res/raw/service.providers b/app/src/main/res/raw/service.providers index 7554c4ed9..ded1b3952 100644 --- a/app/src/main/res/raw/service.providers +++ b/app/src/main/res/raw/service.providers @@ -272,4 +272,163 @@ org.kontalk.client.Account$Provider + + + pubsub + http://jabber.org/protocol/pubsub + org.jivesoftware.smackx.pubsub.provider.PubSubProvider + + + + create + http://jabber.org/protocol/pubsub + org.jivesoftware.smackx.pubsub.provider.SimpleNodeProvider + + + + items + http://jabber.org/protocol/pubsub + org.jivesoftware.smackx.pubsub.provider.ItemsProvider + + + + item + http://jabber.org/protocol/pubsub + org.jivesoftware.smackx.pubsub.provider.ItemProvider + + + + subscriptions + http://jabber.org/protocol/pubsub + org.jivesoftware.smackx.pubsub.provider.SubscriptionsProvider + + + + subscription + http://jabber.org/protocol/pubsub + org.jivesoftware.smackx.pubsub.provider.SubscriptionProvider + + + + affiliations + http://jabber.org/protocol/pubsub + org.jivesoftware.smackx.pubsub.provider.AffiliationsProvider + + + + affiliation + http://jabber.org/protocol/pubsub + org.jivesoftware.smackx.pubsub.provider.AffiliationProvider + + + + options + http://jabber.org/protocol/pubsub + org.jivesoftware.smackx.pubsub.provider.FormNodeProvider + + + + + + affiliation + http://jabber.org/protocol/pubsub#owner + org.jivesoftware.smackx.pubsub.provider.AffiliationProvider + + + + pubsub + http://jabber.org/protocol/pubsub#owner + org.jivesoftware.smackx.pubsub.provider.PubSubProvider + + + + configure + http://jabber.org/protocol/pubsub#owner + org.jivesoftware.smackx.pubsub.provider.FormNodeProvider + + + + default + http://jabber.org/protocol/pubsub#owner + org.jivesoftware.smackx.pubsub.provider.FormNodeProvider + + + + subscriptions + http://jabber.org/protocol/pubsub#owner + org.jivesoftware.smackx.pubsub.provider.SubscriptionsProvider + + + + subscription + http://jabber.org/protocol/pubsub#owner + org.jivesoftware.smackx.pubsub.provider.SubscriptionProvider + + + + + event + http://jabber.org/protocol/pubsub#event + org.jivesoftware.smackx.pubsub.provider.EventProvider + + + + configuration + http://jabber.org/protocol/pubsub#event + org.jivesoftware.smackx.pubsub.provider.ConfigEventProvider + + + + delete + http://jabber.org/protocol/pubsub#event + org.jivesoftware.smackx.pubsub.provider.SimpleNodeProvider + + + + options + http://jabber.org/protocol/pubsub#event + org.jivesoftware.smackx.pubsub.provider.FormNodeProvider + + + + items + http://jabber.org/protocol/pubsub#event + org.jivesoftware.smackx.pubsub.provider.ItemsProvider + + + + item + http://jabber.org/protocol/pubsub#event + org.jivesoftware.smackx.pubsub.provider.ItemProvider + + + + retract + http://jabber.org/protocol/pubsub#event + org.jivesoftware.smackx.pubsub.provider.RetractEventProvider + + + + purge + http://jabber.org/protocol/pubsub#event + org.jivesoftware.smackx.pubsub.provider.SimpleNodeProvider + + + + + encrypted + eu.siacs.conversations.axolotl + org.jivesoftware.smackx.omemo.provider.OmemoVAxolotlProvider + + + list + eu.siacs.conversations.axolotl + org.jivesoftware.smackx.omemo.provider.OmemoDeviceListVAxolotlProvider + + + bundle + eu.siacs.conversations.axolotl + org.jivesoftware.smackx.omemo.provider.OmemoBundleVAxolotlProvider + + From 0353ef12b79ac93573b39655ad226c00ee4ea773 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Fri, 19 Apr 2019 21:06:05 +0200 Subject: [PATCH 02/30] Merge branch master Signed-off-by: Daniele Ricci --- app/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 7523f75ed..073add853 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,7 +46,6 @@ android { versionName project.versionName targetSdkVersion project.targetSdkVersion minSdkVersion project.minSdkVersion - multiDexEnabled true resConfigs "en", "de", "fr", "it", "es", "ca", "cs", "el", "fa", "gl", "ja", "nl", "pt", "pt-rBR", "ru", "sr", "zh-rCN", "ar", "hi", "tr", "nb-rNO" resValue "string", "application_id", applicationId resValue "string", "account_type", applicationId + '.account' From 77e371f83a3a2e6d6da33e4ef3631f67eb83070d Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Fri, 19 Apr 2019 21:09:57 +0200 Subject: [PATCH 03/30] Merge branch master Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/Kontalk.java | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/Kontalk.java b/app/src/main/java/org/kontalk/Kontalk.java index 94113117d..1286d50de 100644 --- a/app/src/main/java/org/kontalk/Kontalk.java +++ b/app/src/main/java/org/kontalk/Kontalk.java @@ -41,7 +41,6 @@ import android.os.Build; import android.os.Handler; import android.preference.PreferenceManager; -import android.support.multidex.MultiDexApplication; import android.support.annotation.RequiresApi; import android.support.multidex.MultiDexApplication; From 707a6514f355a2b40c70355a2de6397d5370dc4c Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 21 Apr 2019 14:03:13 +0200 Subject: [PATCH 04/30] Merge branch master Signed-off-by: Daniele Ricci --- ...3.0.jar => smack-omemo-4.3.4-SNAPSHOT.jar} | Bin 102008 -> 102047 bytes ... => smack-omemo-signal-4.3.4-SNAPSHOT.jar} | Bin 15436 -> 15473 bytes .../service/msgcenter/MessageListener.java | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename app/libs/{smack-omemo-4.3.0.jar => smack-omemo-4.3.4-SNAPSHOT.jar} (91%) rename app/libs/{smack-omemo-signal-4.3.0.jar => smack-omemo-signal-4.3.4-SNAPSHOT.jar} (85%) diff --git a/app/libs/smack-omemo-4.3.0.jar b/app/libs/smack-omemo-4.3.4-SNAPSHOT.jar similarity index 91% rename from app/libs/smack-omemo-4.3.0.jar rename to app/libs/smack-omemo-4.3.4-SNAPSHOT.jar index 9c85001864201abf927e88f84c606607ce2ba969..f50ca1c4c3ccb6b696aa324f0b145ac8b6dbb39f 100644 GIT binary patch delta 4142 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 YAF?x0MY1T$7ybNbLQ&UK&;vsK2OJYp5dZ)H delta 4141 zcmZWr30PCd7M>wtix3fH2NPrygkT9vMA?x=5KxO+5v5|GugX$ERwXR5i=y}GD2Sq> z;=)p?g(5}VBCUehQmcZ3)k?K2)(w54zPa}%;Dzv!+;7f*{_~$X=gi!LUe_ZU>Eegn7+mc%wEN-e4F`K`B&xh}$UXOPjYG%a-qbgiFUxm)UZ(w^-h#z5 zoblQwHT%?s&MQF!Il+>*S=(>w&agi|rOD*LNbm`;4* z=BabP-Z5WAcw<(S_*9=sdgh&O;~)#Kzh>SrE-{y1;<3EY+;&^&`bTpO-M(LJwcq{k zg45!*rR=MvOYNr}61-4Ld0w$f%YT2Edhad&w8NascMmSEHNCLUxb8snk$$6~8E=Rj zZ{)F5RK}a4D*igy=m-9D+gq3*-|-g<#oHhJ+)m09twvL=oJ$hpcRToTi&NN|O9sBa zwp8_+ZeZZva>GY+;koj|4;5zyam?M9fo(CKl*zQCdQv~*-?17Z^a3{XrA*u*U-InhARkpm{ zX80=C`H!Ej`}G^?7Go?8bIrQ(Ph^_PGyDh?c z4yWFB7yK$Y6Y^cV>2nk99t*BP+w<#!>$}>fR$m?cF45?Aiy-#tpDkVo+Q02j=)KQL z*`CmQ;$}^#l~z_fCrqUM*QMQ-OD;ye%lkoX@5k<>&M5umyPF1*g{=u`2ww5~mJ`PDqheo3<)0US0cNitsa_%At%FXe_#I*j8B~aPaAQM#b;9wkZnDUe87_j*{`_-Q3?Z4t0{8t_T7cy!?i3pBLc}nKNN){d5)YDX&Mx)<= z4!u^?bYy&Q1VnhVCcU20EZgM<$!@7vSf?eMOV<2UO}fityifWZ;(|m_zp6!^)?feC zr9c*iC@*{J^b!J4#wHPHI2&r#|MuX6v^2rj1k!R9b&aS{I~P#8Shx}AkcxBg$2nM{ z{aMhY{$BfLX&!?%kR{EdYOR3Y86xC~D=pXOeFR;X`n^vGWqK|1^+oBf)hNNgrJ-$J z1kx;L(k!KE-E;Gjqcq%dZ8mS<0dRjE0CNx&LbLi!xg9*3?e~Y9<=NzggYIvHPV|#c z0^FtY>7~?Q5D$5f3+1M>#&0Urp8$o-y9$|ag7y?I3f~0v?5gku7P~RZCfI-6b>eVM zqh}0%tARoH>n!4sNJDL+1G0w1* zreaGIWkKbs(1sR1oPt+X6-Y9SH$$zdP=!y5zVfE@UAYi|S$ps{j)?}KGP6jLM5DMQ zsDe%GZYWy@JaHZc?@WSRA26*aNIs;DsIcMDU%c7r(=MH ziwr=ls4+%r>9Sy-A^=?l8O}7)!5>$U3TtAnzRK{%M^{3`X;w3Oc8E8k;XfM$~!y-I#9@8DkN`P@$Otz0bn5` z;O8+iqzQVH2dC59bTb#n+Z>NgLd4ILRs2FCUcIpwuliU`Z1^(8gqX_;0CY~B*gd+3 zs0g@wR8d+Q)Sx4%$J1VY1(1(c7sMn#F6kQ07s2x~VSy6po831kVi5(@WrF`m&pdE#D{4gq6h%-6F1;C9FSgC28 zU0fg~W{p18QpP$nFgdf9b*&T}>!gdI>(J|%jH^BaXXL>k=3N;l#-22D|$0K$;~wFkcFF$_yy9Te!1`L(m(K_HiB&gh)MLD623}{X5hE|g#%LQ-qAWp=YffPTN(Gx=wEp#taHZ+}CSeDKRWvy%H zBdcs#uY8w*E#B|O{6br_kjj{!K_<550hE~w`9xhi7e!`59ZJObZ8}lJ9TpA8Nur@_ zIFn9)kYt*rFlNF66^P<%Q)f?{9_!}A_LaCTt{VX`myz8{l#vUKC>h(bp)MtZaYr-I zA(}GP)~Gd0dJp`Nn|kUj-ry~`_n18}M~8-!MdQzb(@=h%)JH2;ZG9*1XnlMQGpEl{ zpYlQYY*F8n0{N68QUs1beehu<{gC(Le@scZDf)4%tSs+x@TQ!mmMsbK`{|RK;-W)T zqZf{S*$@DG85jPo%+&adB(o$spQs{(92wzGlJKOH%%n)PzE(y5U^68xZdjOIf?F&L z>vLhS#krF%0d%RXNRm~a_`>50?3s)I%^3{+6m+)b_*)N&FChAt^ d+6)E9LuIXCe|dad0^pAS&gcSAmw@vJ{|By@BHaK0 diff --git a/app/libs/smack-omemo-signal-4.3.0.jar b/app/libs/smack-omemo-signal-4.3.4-SNAPSHOT.jar similarity index 85% rename from app/libs/smack-omemo-signal-4.3.0.jar rename to app/libs/smack-omemo-signal-4.3.4-SNAPSHOT.jar index 2e062ae5c7ca12b4f77358d7c2854b04d8bd50ce..fd37a4999d9ed4b3bc226e2a59defb27eb209503 100644 GIT binary patch delta 1372 zcmX?8@v(w0z?+$ci-CcIgCQt?s^3IDRbCLCyd(2z91{aW!$b$^`njQ}^KLna+>QQZ z9}yR_z)1NvqmP4*l+aRN?oOvGzT#IC;_ocnc6_cR$Di8uxn~xewv<)NKeu}}$2xi1 zCrb_AeN7=TOKMLq?aREQbJ>8+=&p*f;OB%xLNB77+@`o|URP*5I!oOl^ib3M5S0lW zi!K~by8L5GilNGctYv}){CDb?g>Ug)Hksf5a(};mC9hm|(P>b`2*6jlyCT#h+ipH|>~P+#yZzAG zKU0)lUp0TT@baGLHnZgo%bnN!>x{nSOlfnuIODt*o0#7b%csi)Skf6Y)LO+FPfQee z?z6zD?{MXsn$-M9PaI96l#G2JOV;V`WHCyrmz@1^Mz%;(fZ%e5W=k#Rl==+wd#2mA zJ!DSveLt%=%Tw>`Y~S6JF78cPF(;;`F12dM-#M%IxX*n*ubZn%Q0ZOt&c1;8R~D{t zz5j7<-*bz-`>)iT|7yJEN8H8mBX1|2D6QSkoLK&C-hzuJ@2(wRWbMDYi2HZCo*wI5 zzkDA z5z8lk>V@sZV;xhg%d`5|o7wKU)w=(WVAr#gAAia}e7-QOu8(fq&oRKlL>^O&uj%@W`LqwgI?28I?k1_tZNin=C~b!@mc|24hGD2bft<)2(u1X*|%sLTbZOmFjBEoCM? zkO_+4j+}YI$H2htFj-F5Z1ZO6-+W*zHqYg|!~<4g6_lqr0hobgRe&liHh*DW$^}s& zqC1I$kC91)8Iijtzce>S&0do=Elh(z7J)pAzYBHxKj{>?|{ z?T7T0Ho!wET(YLa;zgpUUO2s+r)sdCCH$J?#_T<>W%~CaY=64(8y8JYq zr`#8g{buwkp4lfeKj8MR*rgL0-d9dOv-H73@o5K6Ow>}d@!QGB>LW3a;UXvZe}S0} zO18b0YB{fyCvkcxn4VF1IPXV&TZwj-UQhXP9wW9Z@>O4&-G7(N|$4GrW}F8(&USQ{MMLAo2d*Jo^_H6+UP#O%)H4d@ud&cv4N)$!j|53vZ>km34Ve ztKp5(JHs7(+w$1!SB;&&g{$9wxs`W(+nmc=D^0&}o;X?cWThg@s<$!RcOQQeD6fB% zay0B~{o=~5`)&V&8M}9`JHC!be7U@f+@>>=b(~8|0-QYeKfb=FbY4Nov6qW%`^~zR z7cNt0E`2E*~mY_ZW5BJpaUUXv4&Y6fY^sQ%#(eQe389(zAAUO^tn` z+2ygvDfQMh;Xkp3toNp*=A2_S&tO@Z?8k7dp1Xu?#l<7Jd$V(sC%e1Y-rpYmbyAH{ z#`e3n-1xf|7IU5FeRbjS-Q|bwI6Kvs*53Z5w^YKiwRhK(wt%KEnG22=xHV+uDt|Q2 zR*(41EWdyI+TVY+9-d_y(!1yX6Vs6|$aWUb^WTI6bPgeEXLlQqv#4IxwBLdj0yJBKdn8SJ<4{ zS~cfd_)#aub0Jz+99B-;mV5J>J!<-ze4I&v1(d)hzh=^fFl3lbAdC=ZD?yMH0}ud{ z9vG-jPGnY`e1KUPEMpavr#S(bSY=fh7;K?3?2{F&cs3tVS;hr6W%F9`OFR%(9)~qQ zgvG-a#{?DApTxm~oZvTKXVzrmV`LIxMr4A?d=|#2xnQ!Ng=rAF+gPA(WMB|rcsFli!I8>!ABd@!OFzPxu%ZxE+A;fTAf?8mP%=@;VM)3{CP+E-QjU;w&4wO-r81&C8l6U=~(fIuma6tVBiJ9SYS-t HGzakjfB~t? diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index 7668baa67..fb3407679 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -353,7 +353,7 @@ else if (_oMemoEncrypted instanceof OmemoElement) { .decrypt(m.getFrom().asBareJid(), m); if (decrypted == null) { Log.d(TAG, "could not decrypt OMEMO message, silently discarding"); - return; + return null; } // encrypted message From c7b71111c7248c3df4578052dc988aa9610e9f17 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 21 Apr 2019 15:20:19 +0200 Subject: [PATCH 05/30] Merge branch master Signed-off-by: Daniele Ricci --- app/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle b/app/build.gradle index 073add853..401f2e630 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -162,6 +162,7 @@ dependencies { implementation "org.igniterealtime.smack:smack-android:$smackVersion" implementation fileTree(dir: 'libs', include: "smack-omemo-${smackVersion}.jar") implementation fileTree(dir: 'libs', include: "smack-omemo-signal-${smackVersion}.jar") + implementation 'org.whispersystems:signal-protocol-java:2.4.0' implementation 'info.guardianproject.netcipher:netcipher:1.2.1' implementation 'com.squareup.okhttp3:okhttp:3.10.0' implementation 'com.segment.backo:backo:1.0.0' From cdb15a37fd6652d121474e7ac9b6acc593d754ef Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 21 Apr 2019 15:20:29 +0200 Subject: [PATCH 06/30] Begin refactoring of Coder to accomodate for OMEMO needs Actually it's not just because of OMEMO, the aim here is to create a more clear abstraction for encrypting/decrypting. Signed-off-by: Daniele Ricci --- .../main/java/org/kontalk/crypto/Coder.java | 12 +- .../java/org/kontalk/crypto/OmemoCoder.java | 36 ++- .../java/org/kontalk/crypto/PGPCoder.java | 63 +++++- .../service/msgcenter/MessageListener.java | 209 ++++++------------ 4 files changed, 169 insertions(+), 151 deletions(-) diff --git a/app/src/main/java/org/kontalk/crypto/Coder.java b/app/src/main/java/org/kontalk/crypto/Coder.java index 4fd3150c3..42c386567 100644 --- a/app/src/main/java/org/kontalk/crypto/Coder.java +++ b/app/src/main/java/org/kontalk/crypto/Coder.java @@ -25,6 +25,8 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import org.jivesoftware.smack.packet.Message; + /** * Generic coder interface. @@ -90,8 +92,8 @@ public abstract class Coder { /** Encrypts a stanza. */ public abstract byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException; - /** Decrypts a byte array which should contain text. */ - public abstract DecryptOutput decryptText(byte[] encrypted, boolean verify) + /** Decrypts a stanza. The type of stanza is left to the implementation. */ + public abstract DecryptOutput decryptMessage(Message message, boolean verify) throws GeneralSecurityException; /** Encrypts a file. */ @@ -118,14 +120,16 @@ public static boolean isError(int securityFlags) { public static class DecryptOutput { public final String mime; - public final String cleartext; + public final Message cleartext; public final Date timestamp; + public final int securityFlags; public final List errors; - DecryptOutput(String cleartext, String mime, Date timestamp, List errors) { + DecryptOutput(Message cleartext, String mime, Date timestamp, int securityFlags, List errors) { this.cleartext = cleartext; this.mime = mime; this.timestamp = timestamp; + this.securityFlags = securityFlags; this.errors = errors; } } diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java index 7667b0c2b..27fd07529 100644 --- a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -21,8 +21,17 @@ import java.io.InputStream; import java.io.OutputStream; import java.security.GeneralSecurityException; +import java.util.Collections; +import java.util.Date; import java.util.List; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.omemo.OmemoManager; +import org.jivesoftware.smackx.omemo.element.OmemoElement; +import org.jivesoftware.smackx.omemo.internal.ClearTextMessage; +import org.jivesoftware.smackx.omemo.util.OmemoConstants; + /** * OMEMO coder implementation. @@ -30,8 +39,10 @@ */ public class OmemoCoder extends Coder { - public OmemoCoder() { - // TODO + private final OmemoManager mManager; + + public OmemoCoder(XMPPConnection connection) { + mManager = OmemoManager.getInstanceFor(connection); } @Override @@ -45,8 +56,25 @@ public byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException { } @Override - public DecryptOutput decryptText(byte[] encrypted, boolean verify) throws GeneralSecurityException { - return null; + public DecryptOutput decryptMessage(Message message, boolean verify) throws GeneralSecurityException { + ClearTextMessage cleartext; + try { + cleartext = mManager.decrypt(null, message); + } + catch (Exception e) { + throw new GeneralSecurityException("OMEMO decryption failed", e); + } + + // simple text message + Message output = new Message(); + output.setType(message.getType()); + output.setFrom(message.getFrom()); + output.setTo(message.getTo()); + output.setBody(cleartext.getBody()); + // copy extensions and remove our own + output.addExtensions(message.getExtensions()); + output.removeExtension(OmemoElement.ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL); + return new DecryptOutput(output, "text/plain", new Date(), SECURITY_ADVANCED, Collections.emptyList()); } @Override diff --git a/app/src/main/java/org/kontalk/crypto/PGPCoder.java b/app/src/main/java/org/kontalk/crypto/PGPCoder.java index 5361bafc9..e97fa6cf8 100644 --- a/app/src/main/java/org/kontalk/crypto/PGPCoder.java +++ b/app/src/main/java/org/kontalk/crypto/PGPCoder.java @@ -32,6 +32,8 @@ import java.util.Iterator; import java.util.List; +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.Message; import org.spongycastle.bcpg.HashAlgorithmTags; import org.spongycastle.openpgp.PGPCompressedData; import org.spongycastle.openpgp.PGPCompressedDataGenerator; @@ -58,6 +60,7 @@ import org.spongycastle.openpgp.operator.bc.BcPublicKeyDataDecryptorFactory; import org.spongycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; +import org.kontalk.client.E2EEncryption; import org.kontalk.client.EndpointServer; import org.kontalk.message.TextComponent; import org.kontalk.util.CPIMMessage; @@ -71,7 +74,6 @@ import static org.kontalk.crypto.DecryptException.DECRYPT_EXCEPTION_INVALID_TIMESTAMP; import static org.kontalk.crypto.DecryptException.DECRYPT_EXCEPTION_PRIVATE_KEY_NOT_FOUND; import static org.kontalk.crypto.DecryptException.DECRYPT_EXCEPTION_VERIFICATION_FAILED; - import static org.kontalk.crypto.VerifyException.VERIFY_EXCEPTION_INVALID_DATA; import static org.kontalk.crypto.VerifyException.VERIFY_EXCEPTION_VERIFICATION_FAILED; @@ -220,9 +222,21 @@ private byte[] encryptData(String mime, CharSequence data) return out.toByteArray(); } - @SuppressWarnings("unchecked") @Override - public DecryptOutput decryptText(byte[] encrypted, boolean verify) + public DecryptOutput decryptMessage(Message message, boolean verify) throws GeneralSecurityException { + ExtensionElement _encrypted = message.getExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE); + if (!(_encrypted instanceof E2EEncryption)) { + throw new DecryptException(DECRYPT_EXCEPTION_INVALID_DATA, "Not an encrypted message"); + } + + E2EEncryption encrypted = (E2EEncryption) _encrypted; + byte[] encryptedData = encrypted.getData(); + + return decryptText(encryptedData, message, verify); + } + + @SuppressWarnings("unchecked") + private DecryptOutput decryptText(byte[] encrypted, Message origin, boolean verify) throws GeneralSecurityException { List errors = new ArrayList<>(); @@ -498,7 +512,35 @@ public DecryptOutput decryptText(byte[] encrypted, boolean verify) SystemUtils.closeStream(cDataIn); } - return new DecryptOutput(out, mime, timestamp, errors); + Message message; + if (XMPPUtils.XML_XMPP_TYPE.equalsIgnoreCase(mime)) { + try { + message = XMPPUtils.parseMessageStanza(out); + } + catch (Exception e) { + throw new DecryptException(DECRYPT_EXCEPTION_INVALID_DATA, e); + } + + if (timestamp != null && !checkDriftedDelay(message, timestamp)) { + errors.add(new DecryptException(DECRYPT_EXCEPTION_INVALID_TIMESTAMP, + "Drifted timestamp")); + } + + // extensions won't be copied because the new message will take over + } + else { + // simple text message + message = new Message(); + message.setType(origin.getType()); + message.setFrom(origin.getFrom()); + message.setTo(origin.getTo()); + message.setBody(out); + // copy extensions and remove our own + message.addExtensions(message.getExtensions()); + message.removeExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE); + } + + return new DecryptOutput(message, mime, timestamp, SECURITY_BASIC, errors); } @Override @@ -848,4 +890,17 @@ public VerifyOutput verifyText(byte[] signed, boolean verify) throws GeneralSecu return new VerifyOutput(out, timestamp, errors); } + private static boolean checkDriftedDelay(Message m, Date expected) { + Date stamp = XMPPUtils.getStanzaDelay(m); + if (stamp != null) { + long time = stamp.getTime(); + long now = expected.getTime(); + long diff = Math.abs(now - time); + return (diff < Coder.TIMEDIFF_THRESHOLD); + } + + // no timestamp found + return true; + } + } diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index fb3407679..430106108 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -29,9 +29,7 @@ import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.forward.packet.Forwarded; -import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.element.OmemoElement; -import org.jivesoftware.smackx.omemo.internal.ClearTextMessage; import org.jivesoftware.smackx.omemo.util.OmemoConstants; import org.jivesoftware.smackx.receipts.DeliveryReceipt; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; @@ -58,6 +56,7 @@ import org.kontalk.client.UserLocation; import org.kontalk.crypto.Coder; import org.kontalk.crypto.DecryptException; +import org.kontalk.crypto.OmemoCoder; import org.kontalk.crypto.PersonalKey; import org.kontalk.crypto.VerifyException; import org.kontalk.data.Contact; @@ -88,8 +87,6 @@ import org.kontalk.util.Preferences; import org.kontalk.util.XMPPUtils; -import static org.kontalk.crypto.DecryptException.DECRYPT_EXCEPTION_INVALID_TIMESTAMP; - /** * Packet listener for message stanzas. @@ -303,110 +300,82 @@ private ChatStateEvent processChatMessage(Message m, @Nullable ChatStateEvent ch // ack request might not be encrypted boolean needAck = m.hasExtension(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE); - ExtensionElement _encrypted = m.getExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE); - ExtensionElement _oMemoEncrypted = m.getExtension(OmemoElement.ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL); - - if (_encrypted instanceof E2EEncryption) { - E2EEncryption mEnc = (E2EEncryption) _encrypted; - byte[] encryptedData = mEnc.getData(); - - // encrypted message - msg.setEncrypted(true); - msg.setSecurityFlags(Coder.SECURITY_BASIC); - - if (encryptedData != null) { - - // decrypt message - try { - Message innerStanza = decryptMessage(msg, encryptedData); - if (innerStanza != null) { - // copy some attributes over - innerStanza.setTo(m.getTo()); - innerStanza.setFrom(m.getFrom()); - innerStanza.setType(m.getType()); - m = innerStanza; - - if (!needAck) { - // try the decrypted message - needAck = m.hasExtension(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE); - } - } - } - - catch (Exception exc) { - Log.e(MessageCenterService.TAG, "decryption failed", exc); + try { + Coder coder = null; + if (m.hasExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE)) { + Context context = getContext(); + EndpointServer server = getServer(); + if (server == null) + server = Preferences.getEndpointServer(context); - // raw component for encrypted data - // reuse security flags - msg.clearComponents(); - msg.addComponent(new RawComponent(encryptedData, true, msg.getSecurityFlags())); - } + PersonalKey key = Kontalk.get().getPersonalKey(); + coder = Keyring.getDecryptCoder(context, server, key, msg.getSender(true)); } - } - else if (_oMemoEncrypted instanceof OmemoElement) { - OmemoElement mEnc = (OmemoElement) _oMemoEncrypted; - if (mEnc.isMessageElement()) { - try { - ClearTextMessage decrypted = OmemoManager.getInstanceFor(getConnection()) - .decrypt(m.getFrom().asBareJid(), m); - if (decrypted == null) { - Log.d(TAG, "could not decrypt OMEMO message, silently discarding"); - return null; - } - - // encrypted message - msg.setEncrypted(true); - msg.setSecurityFlags(Coder.SECURITY_ADVANCED); - - m.removeExtension(mEnc); - m.setBody(decrypted.getBody()); - } - catch (Exception e) { - Log.w(TAG, "error decrypting OMEMO message", e); - - // raw component for encrypted data - // reuse security flags - msg.clearComponents(); - // TODO what data to keep here? - msg.addComponent(new RawComponent(null, true, msg.getSecurityFlags())); - - } + else if (m.hasExtension(OmemoElement.ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL)) { + coder = new OmemoCoder(getConnection()); } - } - else { + else if (m.hasExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage.NAMESPACE)) { + // FIXME not using a Coder here - // use message body - if (body != null) - msg.addComponent(new TextComponent(body)); + // use message body + if (body != null) + msg.addComponent(new TextComponent(body)); - // old PGP signature - ExtensionElement _pgpSigned = m.getExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage.NAMESPACE); - if (_pgpSigned instanceof OpenPGPSignedMessage) { - OpenPGPSignedMessage pgpSigned = (OpenPGPSignedMessage) _pgpSigned; - byte[] signedData = pgpSigned.getData(); + // old PGP signature + ExtensionElement _pgpSigned = m.getExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage.NAMESPACE); + if (_pgpSigned instanceof OpenPGPSignedMessage) { + OpenPGPSignedMessage pgpSigned = (OpenPGPSignedMessage) _pgpSigned; + byte[] signedData = pgpSigned.getData(); - // signed message - msg.setSecurityFlags(Coder.SECURITY_BASIC_SIGNED); + // signed message + msg.setSecurityFlags(Coder.SECURITY_BASIC_SIGNED); - if (signedData != null) { - // check signature - try { - checkSignedMessage(msg, pgpSigned.getData()); - // at this point our message should be filled with the verified body - } + if (signedData != null) { + // check signature + try { + checkSignedMessage(msg, pgpSigned.getData()); + // at this point our message should be filled with the verified body + } - catch (Exception exc) { - Log.e(MessageCenterService.TAG, "signature check failed", exc); - // TODO what to do here? - msg.setSecurityFlags(msg.getSecurityFlags() | - Coder.SECURITY_ERROR_INVALID_SIGNATURE); + catch (Exception exc) { + Log.e(MessageCenterService.TAG, "signature check failed", exc); + // TODO what to do here? + msg.setSecurityFlags(msg.getSecurityFlags() | + Coder.SECURITY_ERROR_INVALID_SIGNATURE); + } } } } + else { + // use message body + if (body != null) + msg.addComponent(new TextComponent(body)); + } + + if (coder != null) { + Message innerStanza = decryptMessage(msg, coder, m); + // copy some attributes over + innerStanza.setTo(m.getTo()); + innerStanza.setFrom(m.getFrom()); + innerStanza.setType(m.getType()); + m = innerStanza; + + if (!needAck) { + // try the decrypted message + needAck = m.hasExtension(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE); + } + } + } + catch (Exception exc) { + Log.e(MessageCenterService.TAG, "decryption failed", exc); + // raw component for encrypted data + // reuse security flags + msg.clearComponents(); + msg.addComponent(new RawComponent(m.toXML(null).toString().getBytes(), true, msg.getSecurityFlags())); } // out of band data @@ -611,46 +580,21 @@ private void sendReceipt(Uri msgUri, String msgId, Jid from) { sendMessage(ack, storageId); } - private Message decryptMessage(CompositeMessage msg, byte[] encryptedData) throws Exception { - // message stanza - Message m = null; - + private Message decryptMessage(CompositeMessage msg, Coder coder, Message packet) throws Exception { try { - Context context = getContext(); - PersonalKey key = Kontalk.get().getPersonalKey(); - - EndpointServer server = getServer(); - if (server == null) - server = Preferences.getEndpointServer(context); - - Coder coder = Keyring.getDecryptCoder(context, server, key, msg.getSender(true)); - // decrypt - Coder.DecryptOutput result = coder.decryptText(encryptedData, true); - - String contentText; - - if (XMPPUtils.XML_XMPP_TYPE.equalsIgnoreCase(result.mime)) { - m = XMPPUtils.parseMessageStanza(result.cleartext); - - if (result.timestamp != null && !checkDriftedDelay(m, result.timestamp)) - result.errors.add(new DecryptException(DECRYPT_EXCEPTION_INVALID_TIMESTAMP, - "Drifted timestamp")); - - contentText = m.getBody(); - } - else { - contentText = result.cleartext; - } + Coder.DecryptOutput result = coder.decryptMessage(packet, true); // clear components (we are adding new ones) msg.clearComponents(); // decrypted text - if (contentText != null) - msg.addComponent(new TextComponent(contentText)); + if (result.cleartext != null) + msg.addComponent(new TextComponent(result.cleartext.getBody())); - if (result.errors.size() > 0) { + // import security flags from coder + msg.setSecurityFlags(result.securityFlags); + if (result.errors.size() > 0) { int securityFlags = msg.getSecurityFlags(); for (DecryptException err : result.errors) { @@ -691,7 +635,7 @@ private Message decryptMessage(CompositeMessage msg, byte[] encryptedData) throw msg.setEncrypted(false); - return m; + return result.cleartext; } catch (Exception exc) { // pass over the message even if encrypted @@ -794,17 +738,4 @@ private void checkSignedMessage(CompositeMessage msg, byte[] signedData) throws } } - private static boolean checkDriftedDelay(Message m, Date expected) { - Date stamp = XMPPUtils.getStanzaDelay(m); - if (stamp != null) { - long time = stamp.getTime(); - long now = expected.getTime(); - long diff = Math.abs(now - time); - return (diff < Coder.TIMEDIFF_THRESHOLD); - } - - // no timestamp found - return true; - } - } From 3ffc7786699cdab4b2ceee628d30cc9b0398e01b Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 21 Apr 2019 15:35:47 +0200 Subject: [PATCH 07/30] Comments [skip ci] Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/crypto/Coder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/crypto/Coder.java b/app/src/main/java/org/kontalk/crypto/Coder.java index 42c386567..b8bad67f4 100644 --- a/app/src/main/java/org/kontalk/crypto/Coder.java +++ b/app/src/main/java/org/kontalk/crypto/Coder.java @@ -92,7 +92,7 @@ public abstract class Coder { /** Encrypts a stanza. */ public abstract byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException; - /** Decrypts a stanza. The type of stanza is left to the implementation. */ + /** Decrypts a stanza. */ public abstract DecryptOutput decryptMessage(Message message, boolean verify) throws GeneralSecurityException; From 06f6d5b86ecd6140a3ceca9c528bab01d4665a73 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Mon, 22 Apr 2019 22:16:42 +0200 Subject: [PATCH 08/30] Some tests with OMEMO send/receive Signed-off-by: Daniele Ricci --- .../main/java/org/kontalk/crypto/Coder.java | 9 +-- .../java/org/kontalk/crypto/OmemoCoder.java | 77 +++++++++++++++++-- .../java/org/kontalk/crypto/PGPCoder.java | 18 ++++- .../java/org/kontalk/provider/Keyring.java | 43 ++++++++--- .../provider/MessagesProviderClient.java | 2 +- .../msgcenter/MessageCenterService.java | 39 +++------- .../service/msgcenter/MessageListener.java | 2 +- .../java/org/kontalk/util/MessageUtils.java | 6 +- 8 files changed, 135 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/org/kontalk/crypto/Coder.java b/app/src/main/java/org/kontalk/crypto/Coder.java index b8bad67f4..ef6ee04b9 100644 --- a/app/src/main/java/org/kontalk/crypto/Coder.java +++ b/app/src/main/java/org/kontalk/crypto/Coder.java @@ -86,13 +86,10 @@ public abstract class Coder { /** How much time to consider a message timestamp drifted (and thus compromised). */ public static final long TIMEDIFF_THRESHOLD = TimeUnit.DAYS.toMillis(1); - /** Encrypts a string. */ - public abstract byte[] encryptText(CharSequence text) throws GeneralSecurityException; + /** Encrypts a message stanza. */ + public abstract Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException; - /** Encrypts a stanza. */ - public abstract byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException; - - /** Decrypts a stanza. */ + /** Decrypts a message stanza. */ public abstract DecryptOutput decryptMessage(Message message, boolean verify) throws GeneralSecurityException; diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java index 27fd07529..6e9257974 100644 --- a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -21,16 +21,28 @@ import java.io.InputStream; import java.io.OutputStream; import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map; +import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.omemo.OmemoFingerprint; import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.element.OmemoElement; +import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; +import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; +import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; import org.jivesoftware.smackx.omemo.internal.ClearTextMessage; +import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.util.OmemoConstants; +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.impl.JidCreate; /** @@ -40,19 +52,66 @@ public class OmemoCoder extends Coder { private final OmemoManager mManager; + private final Jid[] mRecipients; - public OmemoCoder(XMPPConnection connection) { + public OmemoCoder(XMPPConnection connection, Jid[] recipients) throws XMPPException.XMPPErrorException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { mManager = OmemoManager.getInstanceFor(connection); - } + if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) { + throw new UnsupportedOperationException("Server does not support OMEMO"); + } - @Override - public byte[] encryptText(CharSequence text) throws GeneralSecurityException { - return new byte[0]; + mRecipients = recipients; + if (recipients != null) { + for (Jid jid : recipients) { + Map fingerprints = mManager + .getActiveFingerprints(JidCreate.bareFromOrThrowUnchecked(jid)); + if (fingerprints.size() == 0) { + throw new UnsupportedOperationException("Recipient " + jid + " does not support OMEMO"); + } + } + } } + @SuppressWarnings("unchecked") @Override - public byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException { - return new byte[0]; + public Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException { + // FIXME test code + Message output = null; + ArrayList recipients = new ArrayList(Arrays.asList(mRecipients)); + try { + output = mManager.encrypt(recipients, message.getBody()); + } + catch (UndecidedOmemoIdentityException e) { + // TODO rethrow? + e.printStackTrace(); + } + catch (CannotEstablishOmemoSessionException e) { + // TODO rethrow? + try { + output = mManager.encryptForExistingSessions(e, message.getBody()); + } + catch (UndecidedOmemoIdentityException e1) { + // TODO rethrow? + e1.printStackTrace(); + } + catch (CryptoFailedException e1) { + throw new GeneralSecurityException(e1); + } + } + catch (Exception e) { + throw new GeneralSecurityException(e); + } + + if (output != null) { + output.setBody(placeholder); + output.setStanzaId(message.getStanzaId()); + output.setFrom(message.getFrom()); + output.setTo(message.getTo()); + output.setType(message.getType()); + } + + return output; } @Override @@ -79,16 +138,18 @@ public DecryptOutput decryptMessage(Message message, boolean verify) throws Gene @Override public void encryptFile(InputStream input, OutputStream output) throws GeneralSecurityException { - + // TODO } @Override public void decryptFile(InputStream input, boolean verify, OutputStream output, List errors) throws GeneralSecurityException { + // TODO } @Override public VerifyOutput verifyText(byte[] signed, boolean verify) throws GeneralSecurityException { + // TODO return null; } } diff --git a/app/src/main/java/org/kontalk/crypto/PGPCoder.java b/app/src/main/java/org/kontalk/crypto/PGPCoder.java index e97fa6cf8..b303ce272 100644 --- a/app/src/main/java/org/kontalk/crypto/PGPCoder.java +++ b/app/src/main/java/org/kontalk/crypto/PGPCoder.java @@ -112,7 +112,7 @@ public PGPCoder(EndpointServer server, PersonalKey key, PGPPublicKeyRing sender) mSender = sender; } - @Override + @Deprecated public byte[] encryptText(CharSequence text) throws GeneralSecurityException { try { // consider plain text @@ -128,8 +128,7 @@ public byte[] encryptText(CharSequence text) throws GeneralSecurityException { } } - @Override - public byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException { + private byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException { try { // prepare XML wrapper final String xmlWrapper = @@ -149,6 +148,19 @@ public byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException { } } + @Override + public Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException { + byte[] toMessage = encryptStanza(message.toXML(null)); + + org.jivesoftware.smack.packet.Message encMsg = + new org.jivesoftware.smack.packet.Message(message.getTo(), message.getType()); + + encMsg.setBody(placeholder); + encMsg.setStanzaId(message.getStanzaId()); + encMsg.addExtension(new E2EEncryption(toMessage)); + return encMsg; + } + private byte[] encryptData(String mime, CharSequence data) throws PGPException, IOException, SignatureException { diff --git a/app/src/main/java/org/kontalk/provider/Keyring.java b/app/src/main/java/org/kontalk/provider/Keyring.java index 5b69d4756..a589ea583 100644 --- a/app/src/main/java/org/kontalk/provider/Keyring.java +++ b/app/src/main/java/org/kontalk/provider/Keyring.java @@ -24,6 +24,8 @@ import java.util.Iterator; import java.util.Map; +import org.jivesoftware.smack.XMPPConnection; +import org.jxmpp.jid.Jid; import org.spongycastle.openpgp.PGPException; import org.spongycastle.openpgp.PGPPublicKey; import org.spongycastle.openpgp.PGPPublicKeyRing; @@ -34,8 +36,10 @@ import android.support.annotation.VisibleForTesting; import android.text.TextUtils; +import org.kontalk.Log; import org.kontalk.client.EndpointServer; import org.kontalk.crypto.Coder; +import org.kontalk.crypto.OmemoCoder; import org.kontalk.crypto.PGP; import org.kontalk.crypto.PGPCoder; import org.kontalk.crypto.PersonalKey; @@ -47,6 +51,8 @@ */ public class Keyring { + private static final String TAG = Keyring.class.getSimpleName(); + /** * Special value used in the fingerprint column so the first key that comes * in is automatically trusted. @@ -58,18 +64,35 @@ private Keyring() { } /** Returns a {@link Coder} instance for encrypting data. */ - public static Coder getEncryptCoder(Context context, EndpointServer server, PersonalKey key, String[] recipients) { - // get recipients public keys from users database - PGPPublicKeyRing keys[] = new PGPPublicKeyRing[recipients.length]; - for (int i = 0; i < recipients.length; i++) { - PGPPublicKeyRing ring = getPublicKey(context, recipients[i], MyUsers.Keys.TRUST_UNKNOWN); - if (ring == null) - throw new IllegalArgumentException("public key not found for user " + recipients[i]); - - keys[i] = ring; + public static Coder getEncryptCoder(Context context, int securityFlags, XMPPConnection connection, EndpointServer server, PersonalKey key, Jid[] recipients) { + if (securityFlags == Coder.SECURITY_ADVANCED) { + try { + return new OmemoCoder(connection, recipients); + } + catch (Exception e) { + Log.w(TAG, "unable to setup advanced coder, falling back to basic", e); + securityFlags = Coder.SECURITY_BASIC; + } } - return new PGPCoder(server, key, keys); + // used for fallback + if (securityFlags == Coder.SECURITY_BASIC) { + // get recipients public keys from users database + PGPPublicKeyRing[] keys = new PGPPublicKeyRing[recipients.length]; + for (int i = 0; i < recipients.length; i++) { + PGPPublicKeyRing ring = getPublicKey(context, recipients[i].toString(), MyUsers.Keys.TRUST_UNKNOWN); + if (ring == null) + throw new IllegalArgumentException("public key not found for user " + recipients[i]); + + keys[i] = ring; + } + + return new PGPCoder(server, key, keys); + } + + else { + throw new IllegalArgumentException("Invalid security flags. No Coder found."); + } } /** Returns a {@link Coder} instance for decrypting data. */ diff --git a/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java b/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java index 17c8fd418..77aa44b1c 100644 --- a/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java +++ b/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java @@ -102,7 +102,7 @@ public static Uri newOutgoingMessage(Context context, String msgId, String userI // of course outgoing messages are not encrypted in database values.put(Messages.ENCRYPTED, false); values.put(Threads.ENCRYPTION, encrypted); - values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); + values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT); if (inReplyTo > 0) values.put(Messages.IN_REPLY_TO, inReplyTo); return context.getContentResolver().insert( diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java index af42634ef..84af329d2 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java @@ -120,7 +120,6 @@ import org.kontalk.authenticator.Authenticator; import org.kontalk.client.BitsOfBinary; import org.kontalk.client.BlockingCommand; -import org.kontalk.client.E2EEncryption; import org.kontalk.client.EndpointServer; import org.kontalk.client.KontalkConnection; import org.kontalk.client.OutOfBandData; @@ -1329,8 +1328,7 @@ public void handleUploadAttachment(UploadAttachmentRequest request) { encryptTo = new String[] { message.getRecipient() }; } - File encrypted = MessageUtils.encryptFile(this, in, - SystemUtils.toString(encryptTo)); + File encrypted = MessageUtils.encryptFile(this, in, XMPPUtils.parseJids(encryptTo)); fileLength = encrypted.length(); preMediaUri = Uri.fromFile(encrypted); } @@ -2414,38 +2412,19 @@ else if (groupCmdComponent.isPartCommand()) { } if (message.getSecurityFlags() != Coder.SECURITY_CLEARTEXT) { - byte[] toMessage = null; boolean encryptError = false; try { - Coder coder = Keyring.getEncryptCoder(this, mServer, key, SystemUtils.toString(toGroup)); - if (coder != null) { - - // no extensions, create a simple text version to save space - if (msg.getExtensions().size() == 0) { - if (!(request instanceof SendDeliveryReceiptRequest)) { - // a special case for delivery receipts whom doesn't have a body - // but we want to encrypt it for groups (extensions.size() > 0) - toMessage = coder.encryptText(msg.getBody()); - } - } + Coder coder = Keyring.getEncryptCoder(this, message.getSecurityFlags(), + mConnection, mServer, key, toGroup); - // some extension, encrypt whole stanza just to be sure - else { - toMessage = coder.encryptStanza(msg.toXML(null)); - } + org.jivesoftware.smack.packet.Message encMsg = null; - if (toMessage != null) { - org.jivesoftware.smack.packet.Message encMsg = - new org.jivesoftware.smack.packet.Message(msg.getTo(), msg.getType()); - - encMsg.setBody(getString(R.string.text_encrypted)); - encMsg.setStanzaId(m.getStanzaId()); - encMsg.addExtension(new E2EEncryption(toMessage)); + if (!(request instanceof SendDeliveryReceiptRequest && groupController == null)) { + encMsg = coder.encryptMessage(msg, getString(R.string.text_encrypted)); + } - // save the unencrypted stanza for later - originalStanza = msg; - m = encMsg; - } + if (encMsg != null) { + m = encMsg; } } diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index 430106108..1bd735014 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -314,7 +314,7 @@ private ChatStateEvent processChatMessage(Message m, @Nullable ChatStateEvent ch } else if (m.hasExtension(OmemoElement.ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL)) { - coder = new OmemoCoder(getConnection()); + coder = new OmemoCoder(getConnection(), null); } else if (m.hasExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage.NAMESPACE)) { diff --git a/app/src/main/java/org/kontalk/util/MessageUtils.java b/app/src/main/java/org/kontalk/util/MessageUtils.java index 2ff14b5b0..b62a8b4f0 100644 --- a/app/src/main/java/org/kontalk/util/MessageUtils.java +++ b/app/src/main/java/org/kontalk/util/MessageUtils.java @@ -34,6 +34,7 @@ import com.google.i18n.phonenumbers.Phonenumber; import org.jivesoftware.smack.util.StringUtils; +import org.jxmpp.jid.Jid; import org.spongycastle.openpgp.PGPException; import android.content.ContentValues; @@ -574,11 +575,12 @@ public static String messageId() { return StringUtils.randomString(30); } - public static File encryptFile(Context context, InputStream in, String[] users) + public static File encryptFile(Context context, InputStream in, Jid[] users) throws GeneralSecurityException, IOException, PGPException { PersonalKey key = Kontalk.get().getPersonalKey(); EndpointServer server = Preferences.getEndpointServer(context); - Coder coder = Keyring.getEncryptCoder(context, server, key, users); + // TODO advanced coder not supported yet + Coder coder = Keyring.getEncryptCoder(context, Coder.SECURITY_BASIC, null, server, key, users); // create a temporary file to store encrypted data File temp = File.createTempFile("media", null, context.getCacheDir()); FileOutputStream out = new FileOutputStream(temp); From e31673cc7635d144141f9f5f1915e808c213b88d Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Tue, 23 Apr 2019 21:22:47 +0200 Subject: [PATCH 09/30] Better handle decryption errors Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/crypto/OmemoCoder.java | 4 ++++ .../java/org/kontalk/service/msgcenter/MessageListener.java | 3 +++ 2 files changed, 7 insertions(+) diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java index 6e9257974..f0e32c10c 100644 --- a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -124,6 +124,10 @@ public DecryptOutput decryptMessage(Message message, boolean verify) throws Gene throw new GeneralSecurityException("OMEMO decryption failed", e); } + if (cleartext.getBody() == null) { + throw new DecryptException(DecryptException.DECRYPT_EXCEPTION_PRIVATE_KEY_NOT_FOUND); + } + // simple text message Message output = new Message(); output.setType(message.getType()); diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index 1bd735014..325df2077 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -376,6 +376,9 @@ else if (m.hasExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage. // reuse security flags msg.clearComponents(); msg.addComponent(new RawComponent(m.toXML(null).toString().getBytes(), true, msg.getSecurityFlags())); + // and body placeholder + if (body != null) + msg.addComponent(new TextComponent(body)); } // out of band data From 9a920535df2fca4bf58e1a6852b70af9295047a1 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Wed, 24 Apr 2019 19:22:37 +0200 Subject: [PATCH 10/30] Handle encryption fallback in a proper manner Also update security flags of a message when falling back to basic encryption Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/crypto/Coder.java | 3 +++ app/src/main/java/org/kontalk/crypto/OmemoCoder.java | 5 +++++ app/src/main/java/org/kontalk/crypto/PGPCoder.java | 5 +++++ app/src/main/java/org/kontalk/provider/Keyring.java | 9 ++++++--- .../org/kontalk/provider/MessagesProviderClient.java | 5 +++++ .../kontalk/service/msgcenter/MessageCenterService.java | 9 +++++++++ app/src/main/java/org/kontalk/util/MessageUtils.java | 7 ++++++- app/src/main/res/values/strings.xml | 1 + 8 files changed, 40 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/kontalk/crypto/Coder.java b/app/src/main/java/org/kontalk/crypto/Coder.java index ef6ee04b9..485894ca2 100644 --- a/app/src/main/java/org/kontalk/crypto/Coder.java +++ b/app/src/main/java/org/kontalk/crypto/Coder.java @@ -86,6 +86,9 @@ public abstract class Coder { /** How much time to consider a message timestamp drifted (and thus compromised). */ public static final long TIMEDIFF_THRESHOLD = TimeUnit.DAYS.toMillis(1); + /** Returns the supported security flags for the coder. */ + public abstract int getSupportedFlags(); + /** Encrypts a message stanza. */ public abstract Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException; diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java index f0e32c10c..fb1afdb67 100644 --- a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -73,6 +73,11 @@ public OmemoCoder(XMPPConnection connection, Jid[] recipients) throws XMPPExcept } } + @Override + public int getSupportedFlags() { + return Coder.SECURITY_ADVANCED; + } + @SuppressWarnings("unchecked") @Override public Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException { diff --git a/app/src/main/java/org/kontalk/crypto/PGPCoder.java b/app/src/main/java/org/kontalk/crypto/PGPCoder.java index b303ce272..d490052c7 100644 --- a/app/src/main/java/org/kontalk/crypto/PGPCoder.java +++ b/app/src/main/java/org/kontalk/crypto/PGPCoder.java @@ -112,6 +112,11 @@ public PGPCoder(EndpointServer server, PersonalKey key, PGPPublicKeyRing sender) mSender = sender; } + @Override + public int getSupportedFlags() { + return Coder.SECURITY_BASIC; + } + @Deprecated public byte[] encryptText(CharSequence text) throws GeneralSecurityException { try { diff --git a/app/src/main/java/org/kontalk/provider/Keyring.java b/app/src/main/java/org/kontalk/provider/Keyring.java index a589ea583..b4fd31abd 100644 --- a/app/src/main/java/org/kontalk/provider/Keyring.java +++ b/app/src/main/java/org/kontalk/provider/Keyring.java @@ -65,8 +65,11 @@ private Keyring() { /** Returns a {@link Coder} instance for encrypting data. */ public static Coder getEncryptCoder(Context context, int securityFlags, XMPPConnection connection, EndpointServer server, PersonalKey key, Jid[] recipients) { - if (securityFlags == Coder.SECURITY_ADVANCED) { + if ((securityFlags & Coder.SECURITY_ADVANCED) != 0) { try { + if (recipients.length == 1 || recipients[0].equals(connection.getUser().asBareJid())) { + throw new IllegalArgumentException("OMEMO with yourself is not supported"); + } return new OmemoCoder(connection, recipients); } catch (Exception e) { @@ -75,8 +78,8 @@ public static Coder getEncryptCoder(Context context, int securityFlags, XMPPConn } } - // used for fallback - if (securityFlags == Coder.SECURITY_BASIC) { + // used also as fallback + if ((securityFlags & Coder.SECURITY_BASIC) != 0) { // get recipients public keys from users database PGPPublicKeyRing[] keys = new PGPPublicKeyRing[recipients.length]; for (int i = 0; i < recipients.length; i++) { diff --git a/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java b/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java index 77aa44b1c..5c82ef621 100644 --- a/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java +++ b/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java @@ -363,6 +363,11 @@ public MessageUpdater setServerTimestamp(long timestamp) { return this; } + public MessageUpdater setSecurityFlags(int securityFlags) { + mValues.put(Messages.SECURITY_FLAGS, securityFlags); + return this; + } + public MessageUpdater appendWhere(String where) { mWhere = DatabaseUtils.concatenateWhere(mWhere, where); return this; diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java index 84af329d2..d105ecdec 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java @@ -2417,6 +2417,15 @@ else if (groupCmdComponent.isPartCommand()) { Coder coder = Keyring.getEncryptCoder(this, message.getSecurityFlags(), mConnection, mServer, key, toGroup); + // security flags changed (most probably because of coder fallback) + // update message accordingly + if ((message.getSecurityFlags() & coder.getSupportedFlags()) == 0) { + message.setSecurityFlags(message.getSecurityFlags() | coder.getSupportedFlags()); + MessagesProviderClient.MessageUpdater.forMessage(this, message.getDatabaseId()) + .setSecurityFlags(message.getSecurityFlags()) + .commit(); + } + org.jivesoftware.smack.packet.Message encMsg = null; if (!(request instanceof SendDeliveryReceiptRequest && groupController == null)) { diff --git a/app/src/main/java/org/kontalk/util/MessageUtils.java b/app/src/main/java/org/kontalk/util/MessageUtils.java index b62a8b4f0..325c0d077 100644 --- a/app/src/main/java/org/kontalk/util/MessageUtils.java +++ b/app/src/main/java/org/kontalk/util/MessageUtils.java @@ -437,7 +437,12 @@ else if ((securityFlags & Coder.SECURITY_ERROR_PUBLIC_KEY_UNAVAILABLE) != 0) { } else { - details.append(res.getString(R.string.security_status_good)); + if ((securityFlags & Coder.SECURITY_BASIC) != 0) { + details.append(res.getString(R.string.security_status_good)); + } + else if ((securityFlags & Coder.SECURITY_ADVANCED) != 0) { + details.append(res.getString(R.string.security_status_strong)); + } } details.setSpan(STYLE_BOLD, startPos, details.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index faeede27f..ab02b3f52 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -415,6 +415,7 @@ Unable to write personal key to external storage. Unable to export personal key. + strong good bad From df0f3f0bc048ae31d84cd139315c88c648034d8f Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Thu, 25 Apr 2019 23:28:31 +0200 Subject: [PATCH 11/30] Extract trust information for OMEMO Also move TRUST_* constants to Keyring Signed-off-by: Daniele Ricci --- .../kontalk/provider/UsersProviderTest.java | 4 +- .../java/org/kontalk/MessagesController.java | 3 +- .../java/org/kontalk/crypto/OmemoCoder.java | 44 ++++++++++++++++--- .../main/java/org/kontalk/data/Contact.java | 5 +-- .../java/org/kontalk/provider/Keyring.java | 23 +++++++--- .../java/org/kontalk/provider/MyUsers.java | 4 -- .../msgcenter/MessageCenterService.java | 3 +- .../service/msgcenter/PresenceListener.java | 9 ++-- .../service/msgcenter/PublicKeyListener.java | 5 +-- .../service/msgcenter/RosterListener.java | 5 +-- .../main/java/org/kontalk/sync/Syncer.java | 3 +- .../kontalk/ui/ComposeMessageFragment.java | 9 ++-- .../org/kontalk/ui/ContactInfoFragment.java | 14 +++--- .../org/kontalk/ui/GroupInfoFragment.java | 23 +++++----- .../org/kontalk/ui/GroupMessageFragment.java | 3 +- .../org/kontalk/ui/view/ContactsListItem.java | 8 ++-- 16 files changed, 99 insertions(+), 66 deletions(-) diff --git a/app/src/androidTest/java/org/kontalk/provider/UsersProviderTest.java b/app/src/androidTest/java/org/kontalk/provider/UsersProviderTest.java index 7f0c8ec5e..bd636f25a 100644 --- a/app/src/androidTest/java/org/kontalk/provider/UsersProviderTest.java +++ b/app/src/androidTest/java/org/kontalk/provider/UsersProviderTest.java @@ -478,7 +478,7 @@ public void testSetup() { @Test public void testAutotrustedLevel() throws IOException, PGPException { - Keyring.setAutoTrustLevel(getMockContext(), TEST_USERID, MyUsers.Keys.TRUST_VERIFIED); + Keyring.setAutoTrustLevel(getMockContext(), TEST_USERID, Keyring.TRUST_VERIFIED); assertQueryValues(MyUsers.Keys.getUri(TEST_USERID, Keyring.VALUE_AUTOTRUST), MyUsers.Keys.JID, TEST_USERID, MyUsers.Keys.FINGERPRINT, Keyring.VALUE_AUTOTRUST); @@ -486,7 +486,7 @@ public void testAutotrustedLevel() throws IOException, PGPException { byte[] keydata = Base64.decode(TEST_KEYDATA, Base64.DEFAULT); PGPPublicKeyRing originalKey = PGP.readPublicKeyring(keydata); Keyring.setKey(getMockContext(), TEST_USERID, keydata); - PGPPublicKeyRing publicKey = Keyring.getPublicKey(getMockContext(), TEST_USERID, MyUsers.Keys.TRUST_VERIFIED); + PGPPublicKeyRing publicKey = Keyring.getPublicKey(getMockContext(), TEST_USERID, Keyring.TRUST_VERIFIED); assertNotNull(publicKey); assertTrue(Arrays.equals(publicKey.getEncoded(), originalKey.getEncoded())); diff --git a/app/src/main/java/org/kontalk/MessagesController.java b/app/src/main/java/org/kontalk/MessagesController.java index 6efd8b09c..7a702447c 100644 --- a/app/src/main/java/org/kontalk/MessagesController.java +++ b/app/src/main/java/org/kontalk/MessagesController.java @@ -63,7 +63,6 @@ import org.kontalk.provider.KontalkGroupCommands; import org.kontalk.provider.MessagesProviderClient; import org.kontalk.provider.MyMessages; -import org.kontalk.provider.MyUsers; import org.kontalk.provider.UsersProvider; import org.kontalk.service.DownloadService; import org.kontalk.service.MediaService; @@ -284,7 +283,7 @@ public boolean setTrustLevelAndRetryMessages(String jid, String fingerprint, int throw new NullPointerException("fingerprint"); Keyring.setTrustLevel(mContext, jid, fingerprint, trustLevel); - if (trustLevel >= MyUsers.Keys.TRUST_IGNORED) { + if (trustLevel >= Keyring.TRUST_IGNORED) { retryMessagesTo(jid); return true; } diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java index fb1afdb67..151bd6b57 100644 --- a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -44,6 +44,8 @@ import org.jxmpp.jid.Jid; import org.jxmpp.jid.impl.JidCreate; +import org.kontalk.provider.Keyring; + /** * OMEMO coder implementation. @@ -52,9 +54,9 @@ public class OmemoCoder extends Coder { private final OmemoManager mManager; - private final Jid[] mRecipients; + private final TrustedRecipient[] mRecipients; - public OmemoCoder(XMPPConnection connection, Jid[] recipients) throws XMPPException.XMPPErrorException, + public OmemoCoder(XMPPConnection connection, TrustedRecipient[] recipients) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { mManager = OmemoManager.getInstanceFor(connection); if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) { @@ -63,11 +65,25 @@ public OmemoCoder(XMPPConnection connection, Jid[] recipients) throws XMPPExcept mRecipients = recipients; if (recipients != null) { - for (Jid jid : recipients) { + for (TrustedRecipient rcpt : recipients) { Map fingerprints = mManager - .getActiveFingerprints(JidCreate.bareFromOrThrowUnchecked(jid)); + .getActiveFingerprints(JidCreate.bareFromOrThrowUnchecked(rcpt.jid)); if (fingerprints.size() == 0) { - throw new UnsupportedOperationException("Recipient " + jid + " does not support OMEMO"); + throw new UnsupportedOperationException("Recipient " + rcpt.jid + " does not support OMEMO"); + } + + // Trust the OMEMO fingerprints by looking at user trust information. + // Unknown trust level means a new key came in recently and was not ignored nor verified. + // When that meets manual trust, it means user exited from Blind Trust Before Verification. + // In that case, identities will not be trusted and encryption will fail. + boolean willTrust = !(rcpt.trustLevel == Keyring.TRUST_UNKNOWN && rcpt.manualTrust); + for (Map.Entry device : fingerprints.entrySet()) { + if (willTrust) { + mManager.trustOmemoIdentity(device.getKey(), device.getValue()); + } + else { + mManager.distrustOmemoIdentity(device.getKey(), device.getValue()); + } } } } @@ -161,4 +177,22 @@ public VerifyOutput verifyText(byte[] signed, boolean verify) throws GeneralSecu // TODO return null; } + + /** + * Recipient information for encryption. + * The trust level is considered blocking if TRUST_UKNOWN and manualTrust is true, + * meaning we manually verified a previous key and thus overridden Blind Trust + * Before Verification. + */ + public static class TrustedRecipient { + public final Jid jid; + public final int trustLevel; + public final boolean manualTrust; + + public TrustedRecipient(Jid jid, int trustLevel, boolean manualTrust) { + this.jid = jid; + this.trustLevel = trustLevel; + this.manualTrust = manualTrust; + } + } } diff --git a/app/src/main/java/org/kontalk/data/Contact.java b/app/src/main/java/org/kontalk/data/Contact.java index 55334d6b8..5954de3f0 100644 --- a/app/src/main/java/org/kontalk/data/Contact.java +++ b/app/src/main/java/org/kontalk/data/Contact.java @@ -59,7 +59,6 @@ import org.kontalk.authenticator.Authenticator; import org.kontalk.crypto.PGPLazyPublicKeyRingLoader; import org.kontalk.provider.Keyring; -import org.kontalk.provider.MyUsers.Keys; import org.kontalk.provider.MyUsers.Users; import org.kontalk.util.MessageUtils; import org.kontalk.util.Permissions; @@ -730,9 +729,9 @@ public static Contact findByUserId(Context context, @NonNull String userId, Stri private static void retrieveKeyInfo(Context context, Contact c) { // trusted key - Keyring.TrustedPublicKeyData trustedKeyring = Keyring.getPublicKeyData(context, c.getJID(), Keys.TRUST_IGNORED); + Keyring.TrustedPublicKeyData trustedKeyring = Keyring.getPublicKeyData(context, c.getJID(), Keyring.TRUST_IGNORED); // latest (possibly unknown) fingerprint - c.mFingerprint = Keyring.getFingerprint(context, c.getJID(), Keys.TRUST_UNKNOWN); + c.mFingerprint = Keyring.getFingerprint(context, c.getJID(), Keyring.TRUST_UNKNOWN); if (trustedKeyring != null) { c.mTrustedKeyRing = new PGPLazyPublicKeyRingLoader(trustedKeyring.keyData); c.mTrustedLevel = trustedKeyring.trustLevel; diff --git a/app/src/main/java/org/kontalk/provider/Keyring.java b/app/src/main/java/org/kontalk/provider/Keyring.java index b4fd31abd..a6f255459 100644 --- a/app/src/main/java/org/kontalk/provider/Keyring.java +++ b/app/src/main/java/org/kontalk/provider/Keyring.java @@ -53,6 +53,10 @@ public class Keyring { private static final String TAG = Keyring.class.getSimpleName(); + public static final int TRUST_UNKNOWN = 0; + public static final int TRUST_IGNORED = 1; + public static final int TRUST_VERIFIED = 2; + /** * Special value used in the fingerprint column so the first key that comes * in is automatically trusted. @@ -70,7 +74,7 @@ public static Coder getEncryptCoder(Context context, int securityFlags, XMPPConn if (recipients.length == 1 || recipients[0].equals(connection.getUser().asBareJid())) { throw new IllegalArgumentException("OMEMO with yourself is not supported"); } - return new OmemoCoder(connection, recipients); + return new OmemoCoder(connection, getTrustedRecipients(context, recipients)); } catch (Exception e) { Log.w(TAG, "unable to setup advanced coder, falling back to basic", e); @@ -83,7 +87,7 @@ public static Coder getEncryptCoder(Context context, int securityFlags, XMPPConn // get recipients public keys from users database PGPPublicKeyRing[] keys = new PGPPublicKeyRing[recipients.length]; for (int i = 0; i < recipients.length; i++) { - PGPPublicKeyRing ring = getPublicKey(context, recipients[i].toString(), MyUsers.Keys.TRUST_UNKNOWN); + PGPPublicKeyRing ring = getPublicKey(context, recipients[i].toString(), Keyring.TRUST_UNKNOWN); if (ring == null) throw new IllegalArgumentException("public key not found for user " + recipients[i]); @@ -98,15 +102,24 @@ public static Coder getEncryptCoder(Context context, int securityFlags, XMPPConn } } + private static OmemoCoder.TrustedRecipient[] getTrustedRecipients(Context context, Jid[] recipients) { + OmemoCoder.TrustedRecipient[] trustedRecipients = new OmemoCoder.TrustedRecipient[recipients.length]; + for (int i = 0; i < recipients.length; i++) { + TrustedPublicKeyData keyInfo = getPublicKeyData(context, recipients[i].asBareJid().toString(), TRUST_UNKNOWN); + trustedRecipients[i] = new OmemoCoder.TrustedRecipient(recipients[i], keyInfo.trustLevel, keyInfo.manualTrust); + } + return trustedRecipients; + } + /** Returns a {@link Coder} instance for decrypting data. */ public static Coder getDecryptCoder(Context context, EndpointServer server, PersonalKey key, String sender) { - PGPPublicKeyRing senderKey = getPublicKey(context, sender, MyUsers.Keys.TRUST_IGNORED); + PGPPublicKeyRing senderKey = getPublicKey(context, sender, Keyring.TRUST_IGNORED); return new PGPCoder(server, key, senderKey); } /** Returns a {@link Coder} instance for verifying data. */ public static Coder getVerifyCoder(Context context, EndpointServer server, String sender) { - PGPPublicKeyRing senderKey = getPublicKey(context, sender, MyUsers.Keys.TRUST_UNKNOWN); + PGPPublicKeyRing senderKey = getPublicKey(context, sender, Keyring.TRUST_UNKNOWN); return new PGPCoder(server, null, senderKey); } @@ -353,7 +366,7 @@ public static TrustedFingerprint fromString(String value) { if (!TextUtils.isEmpty(value)) { String[] parsed = value.split("\\|"); String fingerprint = parsed[0]; - int trustLevel = MyUsers.Keys.TRUST_UNKNOWN; + int trustLevel = Keyring.TRUST_UNKNOWN; if (parsed.length > 1) { String _trustLevel = parsed[1]; try { diff --git a/app/src/main/java/org/kontalk/provider/MyUsers.java b/app/src/main/java/org/kontalk/provider/MyUsers.java index 2e7625033..12aeb0632 100644 --- a/app/src/main/java/org/kontalk/provider/MyUsers.java +++ b/app/src/main/java/org/kontalk/provider/MyUsers.java @@ -97,10 +97,6 @@ public static Uri getUri(String jid, String fingerprint) { .appendPath(fingerprint).build(); } - public static final int TRUST_UNKNOWN = 0; - public static final int TRUST_IGNORED = 1; - public static final int TRUST_VERIFIED = 2; - public static final String INSERT_ONLY = "insertOnly"; } } diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java index d105ecdec..6798aa2b6 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java @@ -149,7 +149,6 @@ import org.kontalk.provider.MyMessages.Messages; import org.kontalk.provider.MyMessages.Threads; import org.kontalk.provider.MyMessages.Threads.Requests; -import org.kontalk.provider.MyUsers; import org.kontalk.provider.UsersProvider; import org.kontalk.reporting.ReportingManager; import org.kontalk.service.KeyPairGeneratorService; @@ -1810,7 +1809,7 @@ public void run() { final XMPPConnection conn = mConnection; if (conn != null && conn.isConnected()) { Jid jid = conn.getXMPPServiceDomain(); - if (Keyring.getPublicKey(MessageCenterService.this, jid.toString(), MyUsers.Keys.TRUST_UNKNOWN) == null) { + if (Keyring.getPublicKey(MessageCenterService.this, jid.toString(), Keyring.TRUST_UNKNOWN) == null) { BUS.post(new PublicKeyRequest(jid)); } } diff --git a/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java b/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java index 779e21e52..92858d67f 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java @@ -42,7 +42,6 @@ import org.kontalk.data.Contact; import org.kontalk.provider.Keyring; import org.kontalk.provider.MessagesProviderClient; -import org.kontalk.provider.MyUsers; import org.kontalk.provider.MyUsers.Users; import org.kontalk.provider.UsersProvider; import org.kontalk.service.msgcenter.event.PresenceEvent; @@ -102,7 +101,7 @@ public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) { String jid = from.asBareJid().toString(); // store key to users table - Keyring.setKey(getContext(), jid, keydata, MyUsers.Keys.TRUST_VERIFIED); + Keyring.setKey(getContext(), jid, keydata, Keyring.TRUST_VERIFIED); } } catch (Exception e) { @@ -169,7 +168,7 @@ public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) { // insert key if any if (publicKey != null) { try { - Keyring.setKey(ctx, fromStr, publicKey, MyUsers.Keys.TRUST_UNKNOWN); + Keyring.setKey(ctx, fromStr, publicKey, Keyring.TRUST_UNKNOWN); } catch (Exception e) { Log.w(TAG, "invalid public key from " + fromStr, e); @@ -207,7 +206,7 @@ public void run() { boolean requestKey = false; String jid = p.getFrom().asBareJid().toString(); PGPPublicKeyRing pubRing = Keyring.getPublicKey(getContext(), - jid, MyUsers.Keys.TRUST_UNKNOWN); + jid, Keyring.TRUST_UNKNOWN); if (pubRing != null) { String oldFingerprint = PGP.getFingerprint(PGP.getMasterKey(pubRing)); if (!newFingerprint.equalsIgnoreCase(oldFingerprint)) { @@ -252,7 +251,7 @@ public static PresenceEvent createEvent(Context ctx, Presence p, RosterEntry ent String fingerprint = PublicKeyPresence.getFingerprint(p); if (fingerprint == null) { // try untrusted fingerprint from database - fingerprint = Keyring.getFingerprint(ctx, jid, MyUsers.Keys.TRUST_UNKNOWN); + fingerprint = Keyring.getFingerprint(ctx, jid, Keyring.TRUST_UNKNOWN); } // subscription information diff --git a/app/src/main/java/org/kontalk/service/msgcenter/PublicKeyListener.java b/app/src/main/java/org/kontalk/service/msgcenter/PublicKeyListener.java index 2aee10b3c..e06f95e61 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/PublicKeyListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/PublicKeyListener.java @@ -33,7 +33,6 @@ import org.kontalk.crypto.X509Bridge; import org.kontalk.data.Contact; import org.kontalk.provider.Keyring; -import org.kontalk.provider.MyUsers; import org.kontalk.provider.UsersProvider; import org.kontalk.service.msgcenter.event.PublicKeyEvent; import org.kontalk.sync.SyncAdapter; @@ -102,7 +101,7 @@ public void processStanza(Stanza packet) { Log.v("pubkey", "Updating server key for " + from); try { Keyring.setKey(getContext(), from.toString(), _publicKey, - MyUsers.Keys.TRUST_VERIFIED); + Keyring.TRUST_VERIFIED); } catch (Exception e) { // TODO warn user @@ -114,7 +113,7 @@ public void processStanza(Stanza packet) { try { Log.v("pubkey", "Updating key for " + from); Keyring.setKey(getContext(), from.toString(), _publicKey, - selfJid ? MyUsers.Keys.TRUST_VERIFIED : -1); + selfJid ? Keyring.TRUST_VERIFIED : -1); // update display name with uid (if empty) PGPUserID keyUid = PGP.parseUserId(_publicKey, getConnection().getXMPPServiceDomain().toString()); diff --git a/app/src/main/java/org/kontalk/service/msgcenter/RosterListener.java b/app/src/main/java/org/kontalk/service/msgcenter/RosterListener.java index 852f0c9f9..571335147 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/RosterListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/RosterListener.java @@ -32,7 +32,6 @@ import org.kontalk.Log; import org.kontalk.data.Contact; import org.kontalk.provider.Keyring; -import org.kontalk.provider.MyUsers; import org.kontalk.service.msgcenter.event.RosterLoadedEvent; import org.kontalk.service.msgcenter.event.UserSubscribedEvent; @@ -78,10 +77,10 @@ public void onRosterLoadingFailed(Exception exception) { public void entriesAdded(Collection addresses) { final MessageCenterService service = mService.get(); for (Jid jid : addresses) { - if (Keyring.getPublicKey(service, jid.toString(), MyUsers.Keys.TRUST_UNKNOWN) == null) { + if (Keyring.getPublicKey(service, jid.toString(), Keyring.TRUST_UNKNOWN) == null) { // autotrust the first key we have // but set the trust level to ignored because we didn't really verify it - Keyring.setAutoTrustLevel(service, jid.toString(), MyUsers.Keys.TRUST_IGNORED); + Keyring.setAutoTrustLevel(service, jid.toString(), Keyring.TRUST_IGNORED); } } } diff --git a/app/src/main/java/org/kontalk/sync/Syncer.java b/app/src/main/java/org/kontalk/sync/Syncer.java index 584853af9..ef3552b54 100644 --- a/app/src/main/java/org/kontalk/sync/Syncer.java +++ b/app/src/main/java/org/kontalk/sync/Syncer.java @@ -52,7 +52,6 @@ import org.kontalk.crypto.PGPUserID; import org.kontalk.data.Contact; import org.kontalk.provider.Keyring; -import org.kontalk.provider.MyUsers; import org.kontalk.provider.MyUsers.Users; import org.kontalk.service.msgcenter.MessageCenterService; import org.kontalk.util.XMPPUtils; @@ -283,7 +282,7 @@ void performSync(Context context, Account account, String authority, PGPPublicKey pubKey = PGP.getMasterKey(entry.publicKey); // trust our own key blindly int trustLevel = Authenticator.isSelfJID(mContext, entry.from) ? - MyUsers.Keys.TRUST_VERIFIED : -1; + Keyring.TRUST_VERIFIED : -1; // update keys table immediately Keyring.setKey(mContext, entry.from.toString(), entry.publicKey, trustLevel); diff --git a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java index 09ee46b2a..9c0a1f8a6 100644 --- a/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/ComposeMessageFragment.java @@ -76,7 +76,6 @@ import org.kontalk.provider.MessagesProviderClient; import org.kontalk.provider.MyMessages; import org.kontalk.provider.MyMessages.Threads; -import org.kontalk.provider.MyUsers; import org.kontalk.provider.UsersProvider; import org.kontalk.service.msgcenter.MessageCenterService; import org.kontalk.service.msgcenter.PrivacyCommand; @@ -513,7 +512,7 @@ else if ((trustedPublicKey == null && event.fingerprint == null) || event.type = else { // autotrust the key we are about to request // but set the trust level to ignored because we didn't really verify it - Keyring.setAutoTrustLevel(context, event.jid.toString(), MyUsers.Keys.TRUST_IGNORED); + Keyring.setAutoTrustLevel(context, event.jid.toString(), Keyring.TRUST_IGNORED); requestPublicKey(event.jid); } } @@ -773,7 +772,7 @@ void setPrivacy(@NonNull Context ctx, PrivacyCommand action) { if (fingerprint != null) { Kontalk.get().getMessagesController() .setTrustLevelAndRetryMessages(mUserJID, - fingerprint, MyUsers.Keys.TRUST_VERIFIED); + fingerprint, Keyring.TRUST_VERIFIED); } } @@ -879,7 +878,7 @@ void showIdentityDialog(boolean informationOnly, int titleId) { String fingerprint; String uid; - PGPPublicKeyRing publicKey = Keyring.getPublicKey(getActivity(), mUserJID, MyUsers.Keys.TRUST_UNKNOWN); + PGPPublicKeyRing publicKey = Keyring.getPublicKey(getActivity(), mUserJID, Keyring.TRUST_UNKNOWN); if (publicKey != null) { PGPPublicKey pk = PGP.getMasterKey(publicKey); fingerprint = PGP.formatFingerprint(PGP.getFingerprint(pk)); @@ -956,7 +955,7 @@ void trustKeyChange(@NonNull Context context, String fingerprint) { if (fingerprint == null) fingerprint = getContact().getFingerprint(); Kontalk.get().getMessagesController() - .setTrustLevelAndRetryMessages(mUserJID, fingerprint, MyUsers.Keys.TRUST_VERIFIED); + .setTrustLevelAndRetryMessages(mUserJID, fingerprint, Keyring.TRUST_VERIFIED); // reload contact invalidateContact(); } diff --git a/app/src/main/java/org/kontalk/ui/ContactInfoFragment.java b/app/src/main/java/org/kontalk/ui/ContactInfoFragment.java index f623de77e..1b0e0c377 100644 --- a/app/src/main/java/org/kontalk/ui/ContactInfoFragment.java +++ b/app/src/main/java/org/kontalk/ui/ContactInfoFragment.java @@ -47,7 +47,7 @@ import org.kontalk.R; import org.kontalk.crypto.PGP; import org.kontalk.data.Contact; -import org.kontalk.provider.MyUsers; +import org.kontalk.provider.Keyring; import org.kontalk.service.msgcenter.MessageCenterService; import org.kontalk.service.msgcenter.event.ConnectedEvent; import org.kontalk.service.msgcenter.event.LastActivityEvent; @@ -139,17 +139,17 @@ else if (mContact.isKeyChanged()) { mTrustStatus.setEnabled(true); switch (mContact.getTrustedLevel()) { - case MyUsers.Keys.TRUST_UNKNOWN: + case Keyring.TRUST_UNKNOWN: resId = R.drawable.ic_trust_unknown; textId = R.string.trust_unknown; trustButtonsVisibility = View.VISIBLE; break; - case MyUsers.Keys.TRUST_IGNORED: + case Keyring.TRUST_IGNORED: resId = R.drawable.ic_trust_ignored; textId = R.string.trust_ignored; trustButtonsVisibility = View.VISIBLE; break; - case MyUsers.Keys.TRUST_VERIFIED: + case Keyring.TRUST_VERIFIED: resId = R.drawable.ic_trust_verified; textId = R.string.trust_verified; trustButtonsVisibility = View.GONE; @@ -385,19 +385,19 @@ public void onClick(View view) { view.findViewById(R.id.btn_ignore).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - trustKey(mContact.getFingerprint(), MyUsers.Keys.TRUST_IGNORED); + trustKey(mContact.getFingerprint(), Keyring.TRUST_IGNORED); } }); view.findViewById(R.id.btn_refuse).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - trustKey(mContact.getFingerprint(), MyUsers.Keys.TRUST_UNKNOWN); + trustKey(mContact.getFingerprint(), Keyring.TRUST_UNKNOWN); } }); view.findViewById(R.id.btn_accept).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - trustKey(mContact.getFingerprint(), MyUsers.Keys.TRUST_VERIFIED); + trustKey(mContact.getFingerprint(), Keyring.TRUST_VERIFIED); } }); diff --git a/app/src/main/java/org/kontalk/ui/GroupInfoFragment.java b/app/src/main/java/org/kontalk/ui/GroupInfoFragment.java index 658f9d020..a8e0aebf3 100644 --- a/app/src/main/java/org/kontalk/ui/GroupInfoFragment.java +++ b/app/src/main/java/org/kontalk/ui/GroupInfoFragment.java @@ -73,7 +73,6 @@ import org.kontalk.provider.MessagesProviderClient; import org.kontalk.provider.MyMessages; import org.kontalk.provider.MyMessages.Groups; -import org.kontalk.provider.MyUsers; import org.kontalk.service.msgcenter.MessageCenterService; import org.kontalk.service.msgcenter.event.RosterStatusEvent; import org.kontalk.service.msgcenter.event.RosterStatusRequest; @@ -145,7 +144,7 @@ private void loadConversation(long threadId) { mMembersAdapter.clear(); for (String jid : members) { Contact c = Contact.findByUserId(getContext(), jid); - if (c.isKeyChanged() || c.getTrustedLevel() == MyUsers.Keys.TRUST_UNKNOWN) + if (c.isKeyChanged() || c.getTrustedLevel() == Keyring.TRUST_UNKNOWN) showIgnoreAll = true; boolean owner = KontalkGroup.checkOwnership(mConversation.getGroupJid(), jid); boolean isSelfJid = jid.equalsIgnoreCase(selfJid); @@ -440,7 +439,7 @@ private void showIdentityDialog(Contact c, Boolean subscribed) { int titleResId = R.string.title_identity; String uid; - PGPPublicKeyRing publicKey = Keyring.getPublicKey(getContext(), jid, MyUsers.Keys.TRUST_UNKNOWN); + PGPPublicKeyRing publicKey = Keyring.getPublicKey(getContext(), jid, Keyring.TRUST_UNKNOWN); if (publicKey != null) { PGPPublicKey pk = PGP.getMasterKey(publicKey); String rawFingerprint = PGP.getFingerprint(pk); @@ -491,14 +490,14 @@ else if (subscribed) { int trustedLevel; if (c.isKeyChanged()) { // the key has changed and was not trusted yet - trustedLevel = MyUsers.Keys.TRUST_UNKNOWN; + trustedLevel = Keyring.TRUST_UNKNOWN; } else { trustedLevel = c.getTrustedLevel(); } switch (trustedLevel) { - case MyUsers.Keys.TRUST_IGNORED: + case Keyring.TRUST_IGNORED: trustStringId = R.string.trust_ignored; trustSpans = new CharacterStyle[] { SystemUtils.getTypefaceSpan(Typeface.BOLD), @@ -506,7 +505,7 @@ else if (subscribed) { }; break; - case MyUsers.Keys.TRUST_VERIFIED: + case Keyring.TRUST_VERIFIED: trustStringId = R.string.trust_verified; trustSpans = new CharacterStyle[] { SystemUtils.getTypefaceSpan(Typeface.BOLD), @@ -514,7 +513,7 @@ else if (subscribed) { }; break; - case MyUsers.Keys.TRUST_UNKNOWN: + case Keyring.TRUST_UNKNOWN: default: trustStringId = R.string.trust_unknown; trustSpans = new CharacterStyle[] { @@ -550,15 +549,15 @@ public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) switch (which) { case POSITIVE: // trust the key - trustKey(jid, dialogFingerprint, MyUsers.Keys.TRUST_VERIFIED); + trustKey(jid, dialogFingerprint, Keyring.TRUST_VERIFIED); break; case NEUTRAL: // ignore the key - trustKey(jid, dialogFingerprint, MyUsers.Keys.TRUST_IGNORED); + trustKey(jid, dialogFingerprint, Keyring.TRUST_IGNORED); break; case NEGATIVE: // untrust the key - trustKey(jid, dialogFingerprint, MyUsers.Keys.TRUST_UNKNOWN); + trustKey(jid, dialogFingerprint, Keyring.TRUST_UNKNOWN); break; } } @@ -743,9 +742,9 @@ public void ignoreAll() { synchronized (mMembers) { for (GroupMember m : mMembers) { Contact c = m.contact; - if (c.isKeyChanged() || c.getTrustedLevel() == MyUsers.Keys.TRUST_UNKNOWN) { + if (c.isKeyChanged() || c.getTrustedLevel() == Keyring.TRUST_UNKNOWN) { String fingerprint = c.getFingerprint(); - Keyring.setTrustLevel(mContext, c.getJID(), fingerprint, MyUsers.Keys.TRUST_IGNORED); + Keyring.setTrustLevel(mContext, c.getJID(), fingerprint, Keyring.TRUST_IGNORED); Contact.invalidate(c.getJID()); } } diff --git a/app/src/main/java/org/kontalk/ui/GroupMessageFragment.java b/app/src/main/java/org/kontalk/ui/GroupMessageFragment.java index 602dfeb67..756b6aac0 100644 --- a/app/src/main/java/org/kontalk/ui/GroupMessageFragment.java +++ b/app/src/main/java/org/kontalk/ui/GroupMessageFragment.java @@ -63,7 +63,6 @@ import org.kontalk.provider.Keyring; import org.kontalk.provider.MyMessages; import org.kontalk.provider.MyMessages.Groups; -import org.kontalk.provider.MyUsers; import org.kontalk.service.msgcenter.event.NoPresenceEvent; import org.kontalk.service.msgcenter.event.PresenceEvent; import org.kontalk.service.msgcenter.event.PresenceRequest; @@ -432,7 +431,7 @@ else if ((trustedPublicKey == null && event.fingerprint == null) || event.type = else { // autotrust the key we are about to request // but set the trust level to ignored because we didn't really verify it - Keyring.setAutoTrustLevel(context, event.jid.toString(), MyUsers.Keys.TRUST_IGNORED); + Keyring.setAutoTrustLevel(context, event.jid.toString(), Keyring.TRUST_IGNORED); requestPublicKey(event.jid); } } diff --git a/app/src/main/java/org/kontalk/ui/view/ContactsListItem.java b/app/src/main/java/org/kontalk/ui/view/ContactsListItem.java index 0b5151207..205f97f6b 100644 --- a/app/src/main/java/org/kontalk/ui/view/ContactsListItem.java +++ b/app/src/main/java/org/kontalk/ui/view/ContactsListItem.java @@ -20,7 +20,7 @@ import org.kontalk.R; import org.kontalk.data.Contact; -import org.kontalk.provider.MyUsers; +import org.kontalk.provider.Keyring; import org.kontalk.util.SystemUtils; import android.content.Context; @@ -124,13 +124,13 @@ else if (contact.isKeyChanged()) { } else { switch (contact.getTrustedLevel()) { - case MyUsers.Keys.TRUST_UNKNOWN: + case Keyring.TRUST_UNKNOWN: resId = R.drawable.ic_trust_unknown; break; - case MyUsers.Keys.TRUST_IGNORED: + case Keyring.TRUST_IGNORED: resId = R.drawable.ic_trust_ignored; break; - case MyUsers.Keys.TRUST_VERIFIED: + case Keyring.TRUST_VERIFIED: resId = R.drawable.ic_trust_verified; break; default: From fbe15a861c40886dd730c2e93bd675fd899459cf Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Mon, 29 Apr 2019 19:41:19 +0200 Subject: [PATCH 12/30] Fix using OMEMO with yourself Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/provider/Keyring.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/provider/Keyring.java b/app/src/main/java/org/kontalk/provider/Keyring.java index a6f255459..27b18bd4b 100644 --- a/app/src/main/java/org/kontalk/provider/Keyring.java +++ b/app/src/main/java/org/kontalk/provider/Keyring.java @@ -71,7 +71,7 @@ private Keyring() { public static Coder getEncryptCoder(Context context, int securityFlags, XMPPConnection connection, EndpointServer server, PersonalKey key, Jid[] recipients) { if ((securityFlags & Coder.SECURITY_ADVANCED) != 0) { try { - if (recipients.length == 1 || recipients[0].equals(connection.getUser().asBareJid())) { + if (recipients.length == 1 && recipients[0].equals(connection.getUser().asBareJid())) { throw new IllegalArgumentException("OMEMO with yourself is not supported"); } return new OmemoCoder(connection, getTrustedRecipients(context, recipients)); From e81cd19c9b66b31e8fb8d02a9f3d4ae2c944d347 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Tue, 30 Apr 2019 19:44:51 +0200 Subject: [PATCH 13/30] Time to deprecated stuff from years ago [skip ci] Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/crypto/Coder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/kontalk/crypto/Coder.java b/app/src/main/java/org/kontalk/crypto/Coder.java index 485894ca2..c11cd37d0 100644 --- a/app/src/main/java/org/kontalk/crypto/Coder.java +++ b/app/src/main/java/org/kontalk/crypto/Coder.java @@ -42,6 +42,7 @@ public abstract class Coder { /** Cleartext messages. Not encrypted nor signed. */ public static final int SECURITY_CLEARTEXT = 0; /** Legacy (2.x) encryption method. For compatibility with old messages. */ + @Deprecated public static final int SECURITY_LEGACY_ENCRYPTED = 1; /** Basic encryption (e.g. PGP encrypted). Safe enough. */ public static final int SECURITY_BASIC_ENCRYPTED = 1 << 1; From ba70eed213875ae8723c9ad3e81f32b878111664 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Thu, 2 May 2019 19:23:30 +0200 Subject: [PATCH 14/30] Fix processing encrypted chat state group message Signed-off-by: Daniele Ricci --- .../java/org/kontalk/service/msgcenter/MessageListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index 325df2077..7a178dd31 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -591,7 +591,7 @@ private Message decryptMessage(CompositeMessage msg, Coder coder, Message packet // clear components (we are adding new ones) msg.clearComponents(); // decrypted text - if (result.cleartext != null) + if (result.cleartext != null && result.cleartext.getBody() != null) msg.addComponent(new TextComponent(result.cleartext.getBody())); // import security flags from coder From 216c9ac18aa1a9a8bfef46c17d6ee29bab75612e Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sat, 25 May 2019 11:05:42 +0200 Subject: [PATCH 15/30] Update Gradle tools Signed-off-by: Daniele Ricci --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 98fe8a395..a6748d875 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { maven { url 'https://maven.fabric.io/public' } } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:3.4.1' classpath 'com.google.gms:google-services:4.2.0' classpath 'com.github.triplet.gradle:play-publisher:2.0.0-rc1' classpath 'io.fabric.tools:gradle:1.+' From c423e8a12ae50df446cef941cacdcf37619abe0c Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sat, 25 May 2019 21:02:14 +0200 Subject: [PATCH 16/30] Working incoming OMEMO processing via some dirty workarounds Because of the way smack-omemo is designed, processing and decryption can't be separated, so a few dirty tricks were put in place to make this work. Signed-off-by: Daniele Ricci --- app/proguard.cfg | 1 + .../java/org/kontalk/crypto/OmemoCoder.java | 91 ++++++---- .../java/org/kontalk/provider/Keyring.java | 23 ++- .../org/kontalk/service/DownloadService.java | 29 ++- .../msgcenter/DiscoverInfoListener.java | 92 ---------- .../msgcenter/DiscoverItemsListener.java | 60 ------- .../MessageCenterPacketListener.java | 37 ---- .../msgcenter/MessageCenterService.java | 138 +++++++++++--- .../service/msgcenter/MessageListener.java | 168 +++++++++++++----- .../msgcenter/PushDiscoverItemsListener.java | 75 -------- 10 files changed, 327 insertions(+), 387 deletions(-) delete mode 100644 app/src/main/java/org/kontalk/service/msgcenter/DiscoverInfoListener.java delete mode 100644 app/src/main/java/org/kontalk/service/msgcenter/DiscoverItemsListener.java delete mode 100644 app/src/main/java/org/kontalk/service/msgcenter/PushDiscoverItemsListener.java diff --git a/app/proguard.cfg b/app/proguard.cfg index 4b07f7619..e8b474ae9 100644 --- a/app/proguard.cfg +++ b/app/proguard.cfg @@ -83,6 +83,7 @@ -keep class org.jivesoftware.smackx.xdata.** { *; } -keep class org.jivesoftware.smackx.forward.** { *; } -keep class org.jivesoftware.smackx.pubsub.** { *; } +-keep class org.jivesoftware.smackx.pep.** { *; } -keep class org.jivesoftware.smackx.omemo.** { *; } # keep other Smack utilities diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java index 151bd6b57..fec406179 100644 --- a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -32,6 +32,7 @@ import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smackx.delay.packet.DelayInformation; import org.jivesoftware.smackx.omemo.OmemoFingerprint; import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.element.OmemoElement; @@ -41,6 +42,7 @@ import org.jivesoftware.smackx.omemo.internal.ClearTextMessage; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.util.OmemoConstants; +import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.impl.JidCreate; @@ -53,17 +55,19 @@ */ public class OmemoCoder extends Coder { - private final OmemoManager mManager; private final TrustedRecipient[] mRecipients; + private final BareJid mSender; + private OmemoManager mManager; + + /** For encryption. */ public OmemoCoder(XMPPConnection connection, TrustedRecipient[] recipients) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - mManager = OmemoManager.getInstanceFor(connection); - if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) { - throw new UnsupportedOperationException("Server does not support OMEMO"); - } + init(connection); + mSender = null; mRecipients = recipients; + if (recipients != null) { for (TrustedRecipient rcpt : recipients) { Map fingerprints = mManager @@ -89,6 +93,23 @@ public OmemoCoder(XMPPConnection connection, TrustedRecipient[] recipients) thro } } + /** For decryption. */ + public OmemoCoder(XMPPConnection connection, Jid sender) throws XMPPException.XMPPErrorException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + init(connection); + + mSender = sender.asBareJid(); + mRecipients = null; + } + + private void init(XMPPConnection connection) throws XMPPException.XMPPErrorException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + mManager = OmemoManager.getInstanceFor(connection); + if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) { + throw new UnsupportedOperationException("Server does not support OMEMO"); + } + } + @Override public int getSupportedFlags() { return Coder.SECURITY_ADVANCED; @@ -130,52 +151,62 @@ public Message encryptMessage(Message message, String placeholder) throws Genera output.setFrom(message.getFrom()); output.setTo(message.getTo()); output.setType(message.getType()); + output.addExtensions(message.getExtensions()); } return output; } + /** + * For now just here to fool {@link org.jivesoftware.smack.MessageListener}. + */ @Override public DecryptOutput decryptMessage(Message message, boolean verify) throws GeneralSecurityException { - ClearTextMessage cleartext; - try { - cleartext = mManager.decrypt(null, message); - } - catch (Exception e) { - throw new GeneralSecurityException("OMEMO decryption failed", e); - } + if (message.hasExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE)) { + // offline message - decrypt manually + ClearTextMessage cleartext; + try { + cleartext = mManager.decrypt(mSender, message); + } + catch (Exception e) { + throw new GeneralSecurityException("OMEMO decryption failed", e); + } - if (cleartext.getBody() == null) { - throw new DecryptException(DecryptException.DECRYPT_EXCEPTION_PRIVATE_KEY_NOT_FOUND); - } + if (cleartext.getBody() == null) { + throw new DecryptException(DecryptException.DECRYPT_EXCEPTION_PRIVATE_KEY_NOT_FOUND); + } - // simple text message - Message output = new Message(); - output.setType(message.getType()); - output.setFrom(message.getFrom()); - output.setTo(message.getTo()); - output.setBody(cleartext.getBody()); - // copy extensions and remove our own - output.addExtensions(message.getExtensions()); - output.removeExtension(OmemoElement.ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL); - return new DecryptOutput(output, "text/plain", new Date(), SECURITY_ADVANCED, Collections.emptyList()); + // simple text message + Message output = new Message(); + output.setStanzaId(message.getStanzaId()); + output.setType(message.getType()); + output.setFrom(message.getFrom()); + output.setTo(message.getTo()); + output.setBody(cleartext.getBody()); + // copy extensions and remove our own + output.addExtensions(message.getExtensions()); + output.removeExtension(OmemoElement.ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL); + return new DecryptOutput(output, "text/plain", new Date(), SECURITY_ADVANCED, Collections.emptyList()); + } + else { + // online message - already decrypted by smack-omemo + return new DecryptOutput(message, "text/plain", new Date(), SECURITY_ADVANCED, Collections.emptyList()); + } } @Override public void encryptFile(InputStream input, OutputStream output) throws GeneralSecurityException { - // TODO + throw new UnsupportedOperationException("OMEMO does not support file encryption"); } @Override public void decryptFile(InputStream input, boolean verify, OutputStream output, List errors) throws GeneralSecurityException { - // TODO - + throw new UnsupportedOperationException("OMEMO does not support file encryption"); } @Override public VerifyOutput verifyText(byte[] signed, boolean verify) throws GeneralSecurityException { - // TODO - return null; + throw new UnsupportedOperationException("OMEMO does not support verification and signing"); } /** diff --git a/app/src/main/java/org/kontalk/provider/Keyring.java b/app/src/main/java/org/kontalk/provider/Keyring.java index 27b18bd4b..58c099fad 100644 --- a/app/src/main/java/org/kontalk/provider/Keyring.java +++ b/app/src/main/java/org/kontalk/provider/Keyring.java @@ -112,9 +112,26 @@ private static OmemoCoder.TrustedRecipient[] getTrustedRecipients(Context contex } /** Returns a {@link Coder} instance for decrypting data. */ - public static Coder getDecryptCoder(Context context, EndpointServer server, PersonalKey key, String sender) { - PGPPublicKeyRing senderKey = getPublicKey(context, sender, Keyring.TRUST_IGNORED); - return new PGPCoder(server, key, senderKey); + public static Coder getDecryptCoder(Context context, int securityFlags, XMPPConnection connection, EndpointServer server, PersonalKey key, Jid sender) { + if ((securityFlags & Coder.SECURITY_ADVANCED) != 0) { + try { + return new OmemoCoder(connection, sender); + } + catch (Exception e) { + Log.w(TAG, "unable to setup advanced coder, falling back to basic", e); + securityFlags = Coder.SECURITY_BASIC; + } + } + + // used also as fallback + if ((securityFlags & Coder.SECURITY_BASIC) != 0) { + PGPPublicKeyRing senderKey = getPublicKey(context, sender.toString(), Keyring.TRUST_IGNORED); + return new PGPCoder(server, key, senderKey); + } + + else { + throw new IllegalArgumentException("Invalid security flags. No Coder found."); + } } /** Returns a {@link Coder} instance for verifying data. */ diff --git a/app/src/main/java/org/kontalk/service/DownloadService.java b/app/src/main/java/org/kontalk/service/DownloadService.java index 382a5e828..226d35dcf 100644 --- a/app/src/main/java/org/kontalk/service/DownloadService.java +++ b/app/src/main/java/org/kontalk/service/DownloadService.java @@ -31,6 +31,7 @@ import java.util.Map; import org.greenrobot.eventbus.EventBus; +import org.jxmpp.jid.impl.JidCreate; import android.app.IntentService; import android.app.Notification; @@ -325,25 +326,23 @@ public void completed(String url, String mime, File destination) { try { EndpointServer server = Preferences.getEndpointServer(this); PersonalKey key = Kontalk.get().getPersonalKey(); - Coder coder = Keyring.getDecryptCoder(this, server, key, mPeer); - if (coder != null) { - in = new FileInputStream(destination); + Coder coder = Keyring.getDecryptCoder(this, Coder.SECURITY_BASIC, null, server, key, JidCreate.fromOrThrowUnchecked(mPeer)); + in = new FileInputStream(destination); - File outFile = new File(destination + ".new"); - out = new FileOutputStream(outFile); - List errors = new LinkedList<>(); - coder.decryptFile(in, true, out, errors); + File outFile = new File(destination + ".new"); + out = new FileOutputStream(outFile); + List errors = new LinkedList<>(); + coder.decryptFile(in, true, out, errors); - // TODO process errors + // TODO process errors - // delete old file and rename the decrypted one - destination.delete(); - outFile.renameTo(destination); + // delete old file and rename the decrypted one + destination.delete(); + outFile.renameTo(destination); - // save this for later - destinationEncrypted = false; - destinationLength = destination.length(); - } + // save this for later + destinationEncrypted = false; + destinationLength = destination.length(); } catch (Exception e) { Log.e(TAG, "decryption failed!", e); diff --git a/app/src/main/java/org/kontalk/service/msgcenter/DiscoverInfoListener.java b/app/src/main/java/org/kontalk/service/msgcenter/DiscoverInfoListener.java deleted file mode 100644 index 5918ac68e..000000000 --- a/app/src/main/java/org/kontalk/service/msgcenter/DiscoverInfoListener.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Kontalk Android client - * Copyright (C) 2018 Kontalk Devteam - - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.kontalk.service.msgcenter; - -import java.util.List; - -import org.jivesoftware.smack.XMPPConnection; -import org.jivesoftware.smack.filter.StanzaFilter; -import org.jivesoftware.smack.filter.StanzaIdFilter; -import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smackx.disco.packet.DiscoverInfo; -import org.jivesoftware.smackx.disco.packet.DiscoverItems; - -import org.kontalk.Log; -import org.kontalk.client.EndpointServer; -import org.kontalk.client.HTTPFileUpload; -import org.kontalk.client.PushRegistration; -import org.kontalk.upload.HTTPFileUploadService; - - -/** - * Packet listener for service discovery (info). - * @author Daniele Ricci - */ -class DiscoverInfoListener extends MessageCenterPacketListener { - - public DiscoverInfoListener(MessageCenterService instance) { - super(instance); - } - - @Override - public void processStanza(Stanza packet) { - XMPPConnection conn = getConnection(); - EndpointServer server = getServer(); - - // we don't need this listener anymore - conn.removeAsyncStanzaListener(this); - - DiscoverInfo query = (DiscoverInfo) packet; - List features = query.getFeatures(); - for (DiscoverInfo.Feature feat : features) { - - /* - * TODO do not request info about push if disabled by user. - * Of course if user enables push notification we should - * reissue this discovery again. - */ - if (PushRegistration.NAMESPACE.equals(feat.getVar())) { - // push notifications are enabled on this server - // request items to check if gcm is supported and obtain the server id - DiscoverItems items = new DiscoverItems(); - items.setNode(PushRegistration.NAMESPACE); - items.setTo(server.getNetwork()); - - StanzaFilter filter = new StanzaIdFilter(items.getStanzaId()); - conn.addAsyncStanzaListener(new PushDiscoverItemsListener(getInstance()), filter); - - sendPacket(items); - } - - /* - * TODO upload info should be requested only when needed and - * cached. This discovery should of course be issued before any - * media message gets requeued. - * Actually, delay any message from being requeued if at least - * 1 media message is present; do the discovery first. - */ - else if (HTTPFileUpload.NAMESPACE.equals(feat.getVar())) { - Log.d(MessageCenterService.TAG, "got upload service: " + packet.getFrom()); - addUploadService(new HTTPFileUploadService(conn, packet.getFrom().asBareJid()), 0); - // MessagesController will send pending messages - } - } - } -} - diff --git a/app/src/main/java/org/kontalk/service/msgcenter/DiscoverItemsListener.java b/app/src/main/java/org/kontalk/service/msgcenter/DiscoverItemsListener.java deleted file mode 100644 index 1e734ad1e..000000000 --- a/app/src/main/java/org/kontalk/service/msgcenter/DiscoverItemsListener.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Kontalk Android client - * Copyright (C) 2018 Kontalk Devteam - - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.kontalk.service.msgcenter; - -import java.util.List; - -import org.jivesoftware.smack.XMPPConnection; -import org.jivesoftware.smack.filter.StanzaFilter; -import org.jivesoftware.smack.filter.StanzaIdFilter; -import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smackx.disco.packet.DiscoverInfo; -import org.jivesoftware.smackx.disco.packet.DiscoverItems; - - -/** - * Packet listener for service discovery (items). - * @author Daniele Ricci - */ -class DiscoverItemsListener extends MessageCenterPacketListener { - - public DiscoverItemsListener(MessageCenterService instance) { - super(instance); - } - - @Override - public void processStanza(Stanza packet) { - XMPPConnection conn = getConnection(); - - // we don't need this listener anymore - conn.removeAsyncStanzaListener(this); - - DiscoverItems query = (DiscoverItems) packet; - List items = query.getItems(); - for (DiscoverItems.Item item : items) { - DiscoverInfo info = new DiscoverInfo(); - info.setTo(item.getEntityID()); - - StanzaFilter filter = new StanzaIdFilter(info.getStanzaId()); - conn.addAsyncStanzaListener(new DiscoverInfoListener(getInstance()), filter); - sendPacket(info); - } - } -} - diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterPacketListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterPacketListener.java index 0fdb78265..408ec9388 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterPacketListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterPacketListener.java @@ -130,43 +130,6 @@ protected boolean sendMessage(Message message, long databaseId) { return instance != null && instance.sendMessage(message, databaseId); } - protected void addUploadService(IUploadService service) { - MessageCenterService instance = mInstance.get(); - if (instance != null) - instance.addUploadService(service); - } - - protected void addUploadService(IUploadService service, int priority) { - MessageCenterService instance = mInstance.get(); - if (instance != null) - instance.addUploadService(service, priority); - } - - protected boolean isPushNotificationsEnabled() { - MessageCenterService instance = mInstance.get(); - return instance != null && instance.mPushNotifications; - } - - protected void setPushSenderId(String senderId) { - MessageCenterService.sPushSenderId = senderId; - } - - protected IPushListener getPushListener() { - return MessageCenterService.sPushListener; - } - - protected void startPushRegistrationCycle() { - MessageCenterService instance = mInstance.get(); - if (instance != null) - instance.mPushRegistrationCycle = true; - } - - protected void pushRegister() { - MessageCenterService instance = mInstance.get(); - if (instance != null) - instance.pushRegister(); - } - protected WakefulHashSet getWaitingReceiptList() { MessageCenterService instance = mInstance.get(); return (instance != null) ? instance.mWaitingReceipt : null; diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java index 6798aa2b6..ca771fa4b 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java @@ -62,11 +62,11 @@ import org.jivesoftware.smack.util.Async; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.SuccessCallback; -import org.jivesoftware.smackx.caps.packet.CapsExtension; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.csi.ClientStateIndicationManager; import org.jivesoftware.smackx.delay.packet.DelayInformation; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverItems; import org.jivesoftware.smackx.forward.packet.Forwarded; @@ -121,6 +121,7 @@ import org.kontalk.client.BitsOfBinary; import org.kontalk.client.BlockingCommand; import org.kontalk.client.EndpointServer; +import org.kontalk.client.HTTPFileUpload; import org.kontalk.client.KontalkConnection; import org.kontalk.client.OutOfBandData; import org.kontalk.client.PublicKeyPublish; @@ -191,6 +192,7 @@ import org.kontalk.service.msgcenter.group.PartCommand; import org.kontalk.service.msgcenter.group.SetSubjectCommand; import org.kontalk.ui.MessagingNotification; +import org.kontalk.upload.HTTPFileUploadService; import org.kontalk.util.EventBusIndex; import org.kontalk.util.MediaStorage; import org.kontalk.util.MessageUtils; @@ -1722,7 +1724,17 @@ public synchronized void created(final XMPPConnection connection) { filter = new StanzaTypeFilter(Presence.class); connection.addAsyncStanzaListener(presenceListener, filter); - filter = new StanzaTypeFilter(org.jivesoftware.smack.packet.Message.class); + filter = new StanzaFilter() { + @Override + public boolean accept(Stanza stanza) { + // ignore OMEMO messages, they will be processed by smack-omemo + // except delayed messages which must be processed manually via decrypt() + // .......ARGH!!! + return stanza instanceof org.jivesoftware.smack.packet.Message && + (!OmemoManager.stanzaContainsOmemoElement(stanza) || + stanza.hasExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE)); + } + }; connection.addSyncStanzaListener(new MessageListener(this), filter); // this is used as a reply callback @@ -1863,30 +1875,108 @@ void broadcast(String action, String extraName, String extraValue) { * Discovers info and items. */ private void discovery() { - StanzaFilter filter; + // FIXME some messed up, absolutely unmodular code here - Jid to; - try { - to = JidCreate.domainBareFrom(mServer.getNetwork()); - } - catch (XmppStringprepException e) { - Log.w(TAG, "error parsing JID: " + e.getCausingString(), e); - // report it because it's a big deal - ReportingManager.logException(e); - return; + final XMPPConnection connection = mConnection; + if (connection != null) { + Async.go(new Runnable() { + @Override + public void run() { + try { + ServiceDiscoveryManager manager = ServiceDiscoveryManager + .getInstanceFor(connection); + + DiscoverInfo info = manager.discoverInfo(connection.getXMPPServiceDomain()); + + if (info.containsFeature(PushRegistration.NAMESPACE)) { + discoverPushRegistration(connection); + } + } + catch (SmackException.NoResponseException e) { + Log.w(TAG, "No response for discovery of info was received", e); + } + catch (XMPPException.XMPPErrorException e) { + Log.w(TAG, "Error requesting discovery of info", e); + } + catch (NotConnectedException ignored) { + } + catch (InterruptedException ignored) { + } + } + }, "ServerDiscoveryInfo"); + + Async.go(new Runnable() { + @Override + public void run() { + try { + ServiceDiscoveryManager manager = ServiceDiscoveryManager + .getInstanceFor(connection); + + List items = manager. + discoverItems(connection.getXMPPServiceDomain()).getItems(); + + for (DiscoverItems.Item item : items) { + DiscoverInfo info = manager.discoverInfo(item.getEntityID()); + + if (info.containsFeature(HTTPFileUpload.NAMESPACE)) { + Log.d(MessageCenterService.TAG, "got upload service: " + item.getEntityID()); + addUploadService(new HTTPFileUploadService(connection, + item.getEntityID().asBareJid()), 0); + // MessagesController will send pending messages + } + } + } + catch (SmackException.NoResponseException e) { + Log.w(TAG, "No response for discovery of items was received", e); + } + catch (XMPPException.XMPPErrorException e) { + Log.w(TAG, "Error requesting discovery of items", e); + } + catch (NotConnectedException ignored) { + } + catch (InterruptedException ignored) { + } + } + }, "ServerDiscoveryItems"); } + } + + /** For call by {@link #discovery()} listeners. */ + void discoverPushRegistration(XMPPConnection connection) throws XMPPException.XMPPErrorException, + NotConnectedException, InterruptedException, SmackException.NoResponseException { + ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(connection); + List items = manager.discoverItems(connection.getXMPPServiceDomain(), + PushRegistration.NAMESPACE).getItems(); + + for (DiscoverItems.Item item : items) { + String jid = item.getEntityID().toString(); + // google push notifications + if (("gcm.push." + connection.getXMPPServiceDomain().toString()).equals(jid)) { + String senderId = item.getNode(); + MessageCenterService.sPushSenderId = senderId; + + if (mPushNotifications) { + String oldSender = Preferences.getPushSenderId(); - DiscoverInfo info = new DiscoverInfo(); - info.setTo(to); - filter = new StanzaIdFilter(info.getStanzaId()); - mConnection.addAsyncStanzaListener(new DiscoverInfoListener(this), filter); - sendPacket(info); + // store the new sender id + Preferences.setPushSenderId(senderId); + + // begin a registration cycle if senderId is different + if (oldSender != null && !oldSender.equals(senderId)) { + IPushService service = PushServiceManager.getInstance(this); + if (service != null) + service.unregister(sPushListener); + // unregister will see this as an attempt to register again + mPushRegistrationCycle = true; + } + else { + // begin registration immediately + pushRegister(); + } + } + } + } - DiscoverItems items = new DiscoverItems(); - items.setTo(to); - filter = new StanzaIdFilter(items.getStanzaId()); - mConnection.addAsyncStanzaListener(new DiscoverItemsListener(this), filter); - sendPacket(items); } synchronized void active(boolean available) { @@ -1979,10 +2069,6 @@ private Presence createPresence(Presence.Mode mode) { p.setStatus(status); if (mode != null) p.setMode(mode); - - // TODO find a place for this - p.addExtension(new CapsExtension("http://www.kontalk.org/", "none", "sha-1")); - return p; } diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index 7a178dd31..4df52ea5d 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -22,14 +22,20 @@ import java.io.IOException; import java.util.Date; +import org.jivesoftware.smack.ConnectionListener; import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.forward.packet.Forwarded; +import org.jivesoftware.smackx.omemo.OmemoManager; import org.jivesoftware.smackx.omemo.element.OmemoElement; +import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; +import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; +import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener; import org.jivesoftware.smackx.omemo.util.OmemoConstants; import org.jivesoftware.smackx.receipts.DeliveryReceipt; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; @@ -56,7 +62,6 @@ import org.kontalk.client.UserLocation; import org.kontalk.crypto.Coder; import org.kontalk.crypto.DecryptException; -import org.kontalk.crypto.OmemoCoder; import org.kontalk.crypto.PersonalKey; import org.kontalk.crypto.VerifyException; import org.kontalk.data.Contact; @@ -92,10 +97,24 @@ * Packet listener for message stanzas. * @author Daniele Ricci */ -class MessageListener extends WakefulMessageCenterPacketListener { +class MessageListener extends WakefulMessageCenterPacketListener implements ConnectionListener, OmemoMessageListener { + + private OmemoManager mOmemoManager; public MessageListener(MessageCenterService instance) { super(instance, "RECV"); + getConnection().addConnectionListener(this); + } + + @Override + public void authenticated(XMPPConnection connection, boolean resumed) { + // Because of the way the smack-omemo is designed, we can't separate + // incoming message handling from decryption. + // We'll have a local OmemoManager to inject decrypted messages in the + // incoming message processing workflow. + mOmemoManager = OmemoManager.getInstanceFor(connection); + mOmemoManager.removeOmemoMessageListener(this); + mOmemoManager.addOmemoMessageListener(this); } private static final class GroupMessageProcessingResult { @@ -243,6 +262,27 @@ private ChatStateEvent processChatState(Message m) { return null; } + /** + * Process an incoming OMEMO message. + */ + @Override + public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { + // duplicates the message to fool real processing + Message output = new Message(encryptedMessage); + output.setBody(decryptedBody); + + try { + processWakefulStanza(output); + } + catch (SmackException.NotConnectedException ignored) { + } + } + + @Override + public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { + // not used for now. + } + /** * Process an incoming message packet. * @param m the message @@ -302,70 +342,87 @@ private ChatStateEvent processChatMessage(Message m, @Nullable ChatStateEvent ch try { Coder coder = null; - if (m.hasExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE)) { - Context context = getContext(); - EndpointServer server = getServer(); - if (server == null) - server = Preferences.getEndpointServer(context); + int securityFlags = 0; + boolean verifyOnly = false; - PersonalKey key = Kontalk.get().getPersonalKey(); - - coder = Keyring.getDecryptCoder(context, server, key, msg.getSender(true)); + if (m.hasExtension(E2EEncryption.ELEMENT_NAME, E2EEncryption.NAMESPACE)) { + securityFlags = Coder.SECURITY_BASIC; } else if (m.hasExtension(OmemoElement.ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL)) { - coder = new OmemoCoder(getConnection(), null); + securityFlags = Coder.SECURITY_ADVANCED; } else if (m.hasExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage.NAMESPACE)) { - // FIXME not using a Coder here - + securityFlags = Coder.SECURITY_BASIC_SIGNED; + } + else { // use message body if (body != null) msg.addComponent(new TextComponent(body)); + } - // old PGP signature - ExtensionElement _pgpSigned = m.getExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage.NAMESPACE); - if (_pgpSigned instanceof OpenPGPSignedMessage) { - OpenPGPSignedMessage pgpSigned = (OpenPGPSignedMessage) _pgpSigned; - byte[] signedData = pgpSigned.getData(); - - // signed message - msg.setSecurityFlags(Coder.SECURITY_BASIC_SIGNED); + if (securityFlags > 0) { + Context context = getContext(); + EndpointServer server = getServer(); + if (server == null) + server = Preferences.getEndpointServer(context); - if (signedData != null) { - // check signature - try { - checkSignedMessage(msg, pgpSigned.getData()); - // at this point our message should be filled with the verified body - } + PersonalKey key = Kontalk.get().getPersonalKey(); - catch (Exception exc) { - Log.e(MessageCenterService.TAG, "signature check failed", exc); - // TODO what to do here? - msg.setSecurityFlags(msg.getSecurityFlags() | - Coder.SECURITY_ERROR_INVALID_SIGNATURE); - } - } + if ((securityFlags & Coder.SECURITY_ADVANCED_ENCRYPTED) != 0 || (securityFlags & Coder.SECURITY_BASIC_ENCRYPTED) != 0) { + coder = Keyring.getDecryptCoder(context, securityFlags, getConnection(), + server, key, JidCreate.bareFromOrThrowUnchecked(msg.getSender(true))); + } + else { + coder = Keyring.getVerifyCoder(context, server, msg.getSender(true)); + verifyOnly = true; } - } - else { - // use message body - if (body != null) - msg.addComponent(new TextComponent(body)); } if (coder != null) { - Message innerStanza = decryptMessage(msg, coder, m); - // copy some attributes over - innerStanza.setTo(m.getTo()); - innerStanza.setFrom(m.getFrom()); - innerStanza.setType(m.getType()); - m = innerStanza; - - if (!needAck) { - // try the decrypted message - needAck = m.hasExtension(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE); + if (verifyOnly) { + // use message body + if (body != null) + msg.addComponent(new TextComponent(body)); + + // old PGP signature + ExtensionElement _pgpSigned = m.getExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage.NAMESPACE); + if (_pgpSigned instanceof OpenPGPSignedMessage) { + OpenPGPSignedMessage pgpSigned = (OpenPGPSignedMessage) _pgpSigned; + byte[] signedData = pgpSigned.getData(); + + // signed message + msg.setSecurityFlags(Coder.SECURITY_BASIC_SIGNED); + + if (signedData != null) { + // check signature + try { + checkSignedMessage(msg, pgpSigned.getData()); + // at this point our message should be filled with the verified body + } + + catch (Exception exc) { + Log.e(MessageCenterService.TAG, "signature check failed", exc); + // TODO what to do here? + msg.setSecurityFlags(msg.getSecurityFlags() | + Coder.SECURITY_ERROR_INVALID_SIGNATURE); + } + } + } + } + else { + Message innerStanza = decryptMessage(msg, coder, m); + // copy some attributes over + innerStanza.setTo(m.getTo()); + innerStanza.setFrom(m.getFrom()); + innerStanza.setType(m.getType()); + m = innerStanza; + + if (!needAck) { + // try the decrypted message + needAck = m.hasExtension(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE); + } } } } @@ -741,4 +798,17 @@ private void checkSignedMessage(CompositeMessage msg, byte[] signedData) throws } } + // methods not used. + + @Override + public void connected(XMPPConnection connection) { + } + + @Override + public void connectionClosed() { + } + + @Override + public void connectionClosedOnError(Exception e) { + } } diff --git a/app/src/main/java/org/kontalk/service/msgcenter/PushDiscoverItemsListener.java b/app/src/main/java/org/kontalk/service/msgcenter/PushDiscoverItemsListener.java deleted file mode 100644 index cba2b6ab5..000000000 --- a/app/src/main/java/org/kontalk/service/msgcenter/PushDiscoverItemsListener.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Kontalk Android client - * Copyright (C) 2018 Kontalk Devteam - - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.kontalk.service.msgcenter; - -import java.util.List; - -import org.jivesoftware.smack.packet.Stanza; -import org.jivesoftware.smackx.disco.packet.DiscoverItems; -import org.kontalk.util.Preferences; - - -/** - * Packet listener for discovering push notifications support. - * @author Daniele Ricci - */ -class PushDiscoverItemsListener extends MessageCenterPacketListener { - - public PushDiscoverItemsListener(MessageCenterService instance) { - super(instance); - } - - @Override - public void processStanza(Stanza packet) { - // we don't need this listener anymore - getConnection().removeAsyncStanzaListener(this); - - DiscoverItems query = (DiscoverItems) packet; - List items = query.getItems(); - for (DiscoverItems.Item item : items) { - String jid = item.getEntityID().toString(); - // google push notifications - if (("gcm.push." + getServer().getNetwork()).equals(jid)) { - String senderId = item.getNode(); - setPushSenderId(senderId); - - if (isPushNotificationsEnabled()) { - String oldSender = Preferences.getPushSenderId(); - - // store the new sender id - Preferences.setPushSenderId(senderId); - - // begin a registration cycle if senderId is different - if (oldSender != null && !oldSender.equals(senderId)) { - IPushService service = PushServiceManager.getInstance(getContext()); - if (service != null) - service.unregister(getPushListener()); - // unregister will see this as an attempt to register again - startPushRegistrationCycle(); - } - else { - // begin registration immediately - pushRegister(); - } - } - } - } - } -} - From 8a9e45baef3b00490b72ca644fa3d5b66beef646 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sat, 8 Jun 2019 16:38:30 +0200 Subject: [PATCH 17/30] Upgrade Smack to 4.3.4 Signed-off-by: Daniele Ricci --- ...3.4-SNAPSHOT.jar => smack-omemo-4.3.4.jar} | Bin 102047 -> 102012 bytes ...PSHOT.jar => smack-omemo-signal-4.3.4.jar} | Bin 15473 -> 15439 bytes .../client/smack/XMPPTCPConnection.java | 71 +++++++++++++----- client-common-java | 2 +- 4 files changed, 54 insertions(+), 19 deletions(-) rename app/libs/{smack-omemo-4.3.4-SNAPSHOT.jar => smack-omemo-4.3.4.jar} (91%) rename app/libs/{smack-omemo-signal-4.3.4-SNAPSHOT.jar => smack-omemo-signal-4.3.4.jar} (87%) diff --git a/app/libs/smack-omemo-4.3.4-SNAPSHOT.jar b/app/libs/smack-omemo-4.3.4.jar similarity index 91% rename from app/libs/smack-omemo-4.3.4-SNAPSHOT.jar rename to app/libs/smack-omemo-4.3.4.jar index f50ca1c4c3ccb6b696aa324f0b145ac8b6dbb39f..756ee9b3facf8f969c68b5deb755c91a2b63a9c4 100644 GIT binary patch delta 3565 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 F#J{9{29N*% delta 3498 zcmZWs3pi9;8{W%YTxSfqi$+SJqNrvfrrVKAC>*+%q|=`wmoR7|*AeC4v`XRVR8pk! z7fI@*oH`=iPSI6LN$H|=8}+Zf_TbDpp2zbn-}}Aq_pWb$YpsWDFT1FK4a>uUQI3%z zM-ZClX}j4kBa%T7t8+xgk^=vEp=~1GS@U-zcxhILVpjW{&mEiAIcx3B&RJ$J52XF) zxN=QbTldjBI$ygT*4f@QkUZ12?5nM^bLj1Ck9%3kO8)aRcT&)WqD|uhPc4QyZ^CVZ zjYB60B1{h{-^||OeqpDR4?i*dk|V#^rQp%*n8z8Lx-<2|-k$gOHM^O(Q8L$QNAbkC z&mZi+$v*G9V(8y6+3&K@wtRc4(4PJCds(>M>*`&)VRxTzJl7cRB|Pt6w=Vv-;u`%4 zi*3Uu_Fn(R-mr~)chSB{U}Bc{F4^$LXw#2w{*m*BpH|I%QS!rVCE>Z;f2w}Yn~>o_ z&qT{i_=zD^(*iFo+420y>vsJIBlIMOt^G49j8`pdRhxO-TT*)efs2*4`@%gc9e&jz^k}+QLwmatWb{}!vE-KWRD?K)UdQRXH7jv%!LNzbw zyLPzT?2 zm6;CrWtDp#F1!$;b>>dDMNfparBQa@IEUEC=zhO5Pm*^zE9)6u%jCtkUGLj^>L-sR zz2H54E9`BXT$`DpyTz%x4hHSY^+v|-jVE4fRd;-eGw*rVG_Bij@-v=;wd-ELH|dhz zvB^C3=3Nigp4!K1j6Ykcoo4xo3@rAks5o03qv`AM!Smcko%gxSp4*E`=anZ06i&_y z&`EW)eBhUE;qc=?{Oz6nq3x%WKrt>3Zc zoR{^2u?3$Jocw%@ix$iDV#6yJN^CV(cR!Nka>^eEwO%IA$@0H?sCz&6=?sp#cQ7_^ z#do->=}POQuFZMZ!oo$d>h@kW{>t}$=`6Y2yYqPCyu9X;hYi+6R!>V$zYFho2v`^W zBPy@o+q_HjI z2k;^z)|+137hBUM)FfKU7&1*&;!_TJr0qc=j)WYHrjt8M4$+I8(o3|Tg=IwmoBvks zLF>4kaAjlG&gy0wT2r$h(1OObQ*p9iEv<8+&WVL{XViN!@PcgENz06Dv{Ayj%bM=e z3t@}g>6%-gHE<6K_Pm_-0T9Fi_M-Bqmf4=_<(CLAPP)W>Sd4{+lRrz zC}5>Diiy@nY;SjM))-m#^uAQ0vAPtVOT$mLiK0KQN3~hV0-$N z-$*x7B8V_1K};fse)cDdT;wD!G>HM)d{oPGz2mw6?l&NaGoA!7VJMl{pLp1H&_HAi z(8sm2W2n6a4g%=djHiH|u_%5uZ@L%(MX@+rECx1MqKpc@7t0H9V4qkn!;M1#Uor>;#L3&VfdcGP<$wmPiUZp8 zH*bvtTqWT_jGQl3|5pR2*I3TiLr3Eb`u$0vB}teBSHuH*+`Q=VAKhl7EGBBVt3sx% zWI&$eAL7Fw@$$`a6OgNtQaNZl1V||$w2T5MeGZvdW=Nyd#K*%27776s_lA(9m-tkZ>E2*A!X z7e4!+47sXBdl4u&^RK5$kVUsx#5TgR_3I!sY1`5E_{KAwq)w9t9cF}|&CJzz_bZDg%VKyJt@Z^-V8TF&xmZ9BFRY)@Hg&UK> zXwtb?2QEvI_i}v-vJ}yW5m=D|M&p(lz0&-RqfQV3rUc=rAR_D^5Me>nRKUmknvse` zepP`vX@F0z=whJ~TzFLlaA097szbH4oJ!fIMyC`ChQa<+`Bii(ReatD_I^Vfzi=h0pFhXz5oDbPCMjx?~49@Q^tXjHiZTo#HJ02PISrEXnO zIx^-vl)-`0bTERxeJCi&VUVl?D;xkD7Nn!er3P#2cUglQn;?q01R+weVC6hW;y`a_ zpbM8vsh?N*c3ZFv$Xb-6prM!R6b)L~K~Uxo6fK^w~PlBQ{r|0Xf02Jrh_`sVw+#8yJg4RI^Y)#}FFw$^vsh q3vAB5toMTe diff --git a/app/libs/smack-omemo-signal-4.3.4-SNAPSHOT.jar b/app/libs/smack-omemo-signal-4.3.4.jar similarity index 87% rename from app/libs/smack-omemo-signal-4.3.4-SNAPSHOT.jar rename to app/libs/smack-omemo-signal-4.3.4.jar index fd37a4999d9ed4b3bc226e2a59defb27eb209503..0c69535a03328fb8ab0ab56ea41a74df66acdfad 100644 GIT binary patch delta 1037 zcmexZalV2#z?+$ci-CcIgW-G4iHW>wygHwaGFwX9}lpTlnE^xgW>z)@tkX3wT3dFVJ8R_6!s0(WM~$_A{|276c_Eq)GtcpABpmv{({V21Px}cT zBgu>&!-rP#o1Glkb|$j1em_)i`>FBE8WVA~9jR=hyG{1+y$E$+|N8T6+6&bi!W=hB z4ex&25&SAi!@udg&+L^xKjt_$)qel|*HM2LpR!lx&DIdLA8mg(imZ>?AR0Y6B9}|q zx@;0_eB-p@&u6Zb^<7-GE8)%mBXQP$x7{z4eP8izm((whlNW2Qtn}sxxxI5sNqqhG zzC!i5#O0O+;?lVXI^cY8jMAq4b2YH(mO6yN?9?0lRX^`ssdu{i%Z#jP&LjB9% zhV5N4!R76?_~erH5|ff=cE4KOe%oK{9pl3NyA*}K#CjQi(Q)rkUC0vIlX2y3`O2jq z86H*V$t50*Z}@Ys^xd`fg4t$)rl0=>dW+U?IU<^Q!Qu{Ui`h?+(oMmo>@Q5yB2B!m zXmFPEXP6*0`7+aPX0@6VlP5Aef#}WGn2ngg3~sh~ z2qTxnh94}lM*K35AW)5;RZyPh1YpXPRbgPTot((5IQalG`(y$(@$cOi3Ui^~qocRN#mW#iB`c1AT^5Lj+}YI$H2ht0Q3ur8aoc4VMdeJaq#K{cr!AIFhlYp zNYl>mzhz2z7#PGg(G9K@2fN^%xUe?5CX`S^u_<`+25TR%zy4cGGui0_jWC%UU?a_T I#~h>%0L?t6SO5S3 delta 1014 zcmX?K@v&m^8z!lVQj)wu`BVLpcVs?|V`5-vnCKu~KR5Jr-Yo}_yV0NQBjQ397%AUo z^l{LU5?boZ-RX42SNv*1{GEl{j?b0k_*1(+_sn9`ma=O3=XTHLSSL^WWU1l1uPG#E zN$u&SeVKQ3E*r2J-BmFb{G4z|=tZ=X+Z1=r>k5rWXQ?}c9%`B&qB4PF(S_qlmw!x2 zF;tn5wM?*p|4#j~@GZW}CiDAW?(f&H7H`*ziu|bQQ=WYItlGT^DL@!IcdVJmKm)SvCj|G=|)h`TK_wv$}+Iv<9 z=0-&Bi~D;1rp?jshugJ;Kkw8#ADbn2UED=xSA=?G+s%iL9nQOTw;x*jXNt1xtLASO zUf%QEX12Uxx$~NToza(^DQzwnXPoz96Z1P_`EP3to>IPasN)&(_@|MSCIL1;|BI5 zZ&t47`yBR|rX8`=_&#mdslwNb1fKItPLtXbP`KORh~?V#%q!oh2AnrMV)^7xy|A5l ztYd0*c~<{=Guu74TKE4E?0R(Aww zEMX}y`G>hWY6_mLV-d`_b8>@)A>-Z2A1y>CAG6S6;$xodz$`VH$5M;Q4a8KK9AU`= z(l4OO00ROHZyj5Jl2+=IS6hOuo4m_Xn&~7+NO$rFW&!O0Z$>5&W-bN>4h{xTu91Io zS&;#lYtFJUFu1^s0n$qv|4p{H3YG_{QT%q~%o9Ea25tui23r(036nQk`G7n#$y%Ce zzBtqtuE`g~`E}53vI@%6oB*^~Rt4RZQWdC1w#kRAB|yeaz6&%jK_4!xYa_$<&m3em E06or~bN~PV diff --git a/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java b/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java index b0f46ec70..464374586 100644 --- a/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java +++ b/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java @@ -141,6 +141,7 @@ import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.stringprep.XmppStringprepException; import org.jxmpp.util.XmppStringUtils; +import org.minidns.dnsname.DnsName; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -518,18 +519,12 @@ protected void shutdown() { shutdown(false); } - /** - * Performs an unclean disconnect and shutdown of the connection. Does not send a closing stream stanza. - */ + @Override public synchronized void instantShutdown() { shutdown(true); } private void shutdown(boolean instant) { - if (disconnectedButResumeable) { - return; - } - // First shutdown the writer, this will result in a closing stream element getting send to // the server LOGGER.finer("PacketWriter shutdown()"); @@ -552,13 +547,25 @@ private void shutdown(boolean instant) { packetReader.shutdown(); LOGGER.finer("PacketReader has been shut down"); - try { + final Socket socket = this.socket; + if (socket != null && socket.isConnected()) { + try { socket.close(); - } catch (Exception e) { + } catch (Exception e) { LOGGER.log(Level.WARNING, "shutdown", e); + } } setWasAuthenticated(); + + // Wait for reader and writer threads to be terminated. + readerWriterSemaphore.acquireUninterruptibly(2); + readerWriterSemaphore.release(2); + + if (disconnectedButResumeable) { + return; + } + // If we are able to resume the stream, then don't set // connected/authenticated/usingTLS to false since we like behave like we are still // connected (e.g. sendStanza should not throw a NotConnectedException). @@ -661,6 +668,7 @@ private void connectUsingConfiguration() throws ConnectionException, IOException proxyInfo.getProxySocketConnection().connect(socket, host, port, timeout); } catch (IOException e) { hostAddress.setException(e); + failedAddresses.add(hostAddress); continue; } LOGGER.finer("Established TCP connection to " + hostAndPort); @@ -870,8 +878,31 @@ else if (keyStoreType != null) { final HostnameVerifier verifier = getConfiguration().getHostnameVerifier(); if (verifier == null) { throw new IllegalStateException("No HostnameVerifier set. Use connectionConfiguration.setHostnameVerifier() to configure."); - } else if (!verifier.verify(getXMPPServiceDomain().toString(), sslSocket.getSession())) { - throw new CertificateException("Hostname verification of certificate failed. Certificate does not authenticate " + getXMPPServiceDomain()); + } + + final String verifierHostname; + { + DnsName xmppServiceDomainDnsName = getConfiguration().getXmppServiceDomainAsDnsNameIfPossible(); + // Try to convert the XMPP service domain, which potentially includes Unicode characters, into ASCII + // Compatible Encoding (ACE) to match RFC3280 dNSname IA5String constraint. + // See also: https://bugzilla.mozilla.org/show_bug.cgi?id=280839#c1 + if (xmppServiceDomainDnsName != null) { + verifierHostname = xmppServiceDomainDnsName.ace; + } + else { + LOGGER.log(Level.WARNING, "XMPP service domain name '" + getXMPPServiceDomain() + + "' can not be represented as DNS name. TLS X.509 certificate validiation may fail."); + verifierHostname = getXMPPServiceDomain().toString(); + } + } + + final boolean verificationSuccessful; + // Verify the TLS session. + verificationSuccessful = verifier.verify(verifierHostname, sslSocket.getSession()); + if (!verificationSuccessful) { + throw new CertificateException( + "Hostname verification of certificate failed. Certificate does not authenticate " + + getXMPPServiceDomain()); } // Set that TLS was successful @@ -971,7 +1002,7 @@ protected void connectInternal() throws SmackException, IOException, XMPPExcepti * * @param e the exception that causes the connection close event. */ - private synchronized void notifyConnectionError(final Exception e) { + private void notifyConnectionError(final Exception e) { ASYNC_BUT_ORDERED.performAsyncButOrdered(this, new Runnable() { @Override public void run() { @@ -996,10 +1027,6 @@ public void run() { // Note that a connection listener of XMPPTCPConnection will drop the SM state in // case the Exception is a StreamErrorException. instantShutdown(); - - // Wait for reader and writer threads to be terminated. - readerWriterSemaphore.acquireUninterruptibly(2); - readerWriterSemaphore.release(2); } Async.go(new Runnable() { @@ -1080,6 +1107,8 @@ void openStream() throws SmackException, InterruptedException { protected class PacketReader { + private final String threadName = "Smack Reader (" + getConnectionCounter() + ')'; + XmlPullParser parser; private volatile boolean done; @@ -1094,13 +1123,15 @@ void init() { Async.go(new Runnable() { @Override public void run() { + LOGGER.finer(threadName + " start"); try { parsePackets(); } finally { + LOGGER.finer(threadName + " exit"); XMPPTCPConnection.this.readerWriterSemaphore.release(); } } - }, "Smack Reader (" + getConnectionCounter() + ")"); + }, threadName); } /** @@ -1363,6 +1394,8 @@ public void run() { protected class PacketWriter { public static final int QUEUE_SIZE = XMPPTCPConnection.QUEUE_SIZE; + private final String threadName = "Smack Writer (" + getConnectionCounter() + ')'; + private final ArrayBlockingQueueWithShutdown queue = new ArrayBlockingQueueWithShutdown<>( QUEUE_SIZE, true); @@ -1408,13 +1441,15 @@ void init() { Async.go(new Runnable() { @Override public void run() { + LOGGER.finer(threadName + " start"); try { writePackets(); } finally { + LOGGER.finer(threadName + " exit"); XMPPTCPConnection.this.readerWriterSemaphore.release(); } } - }, "Smack Writer (" + getConnectionCounter() + ")"); + }, threadName); } private boolean done() { diff --git a/client-common-java b/client-common-java index dea94365b..0266a5f66 160000 --- a/client-common-java +++ b/client-common-java @@ -1 +1 @@ -Subproject commit dea94365bf2d28b9e29a9ef3a136928eccffd793 +Subproject commit 0266a5f66356464474deed1e72dea2d90cb40209 From dfd4e7f531b578557caa61705d151b594f61e4ed Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 9 Jun 2019 22:37:17 +0200 Subject: [PATCH 18/30] Crash test scenario Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/crypto/OmemoCoder.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java index fec406179..041e7417a 100644 --- a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -118,24 +118,22 @@ public int getSupportedFlags() { @SuppressWarnings("unchecked") @Override public Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException { - // FIXME test code - Message output = null; + Message output; ArrayList recipients = new ArrayList(Arrays.asList(mRecipients)); try { output = mManager.encrypt(recipients, message.getBody()); } catch (UndecidedOmemoIdentityException e) { - // TODO rethrow? - e.printStackTrace(); + // TODO experimenting; crash for now + throw new RuntimeException("Impossible: we should have decided already!", e); } catch (CannotEstablishOmemoSessionException e) { - // TODO rethrow? try { output = mManager.encryptForExistingSessions(e, message.getBody()); } catch (UndecidedOmemoIdentityException e1) { - // TODO rethrow? - e1.printStackTrace(); + // TODO experimenting; crash for now + throw new RuntimeException("Impossible: we should have decided already!", e1); } catch (CryptoFailedException e1) { throw new GeneralSecurityException(e1); From 9b945238fbbb19373267ce61eeff2bb4e3e0c56d Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 9 Jun 2019 23:31:50 +0200 Subject: [PATCH 19/30] More or less working OMEMO between clients And a few optimizations. Signed-off-by: Daniele Ricci --- .../org/kontalk/client/KontalkConnection.java | 36 +++++++++++++++++++ .../java/org/kontalk/crypto/OmemoCoder.java | 26 ++++++++++---- .../provider/MessagesProviderClient.java | 10 +++--- .../msgcenter/MessageCenterService.java | 15 +++++--- 4 files changed, 70 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/kontalk/client/KontalkConnection.java b/app/src/main/java/org/kontalk/client/KontalkConnection.java index 25bc31264..f85cec063 100644 --- a/app/src/main/java/org/kontalk/client/KontalkConnection.java +++ b/app/src/main/java/org/kontalk/client/KontalkConnection.java @@ -42,12 +42,15 @@ import org.jivesoftware.smack.SASLAuthentication; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.StanzaListener; +import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.sm.StreamManagementException; import org.jivesoftware.smack.sm.predicates.ForMatchingPredicateOrAfterXStanzas; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.receipts.DeliveryReceipt; import org.jivesoftware.smackx.receipts.DeliveryReceiptRequest; import org.jxmpp.stringprep.XmppStringprepException; @@ -66,6 +69,8 @@ public class KontalkConnection extends XMPPTCPConnection { /** Packet reply timeout. */ public static final int DEFAULT_PACKET_TIMEOUT = 15000; + private DiscoverInfo mDiscoverInfoCache; + protected EndpointServer mServer; /** Actually a copy of the same Smack map, but since we need access to the listeners... */ @@ -246,10 +251,41 @@ protected void processStanza(Stanza packet) throws InterruptedException { } } + @Override + protected void shutdown() { + purgeCaches(); + super.shutdown(); + } + + @Override + public synchronized void instantShutdown() { + purgeCaches(); + super.instantShutdown(); + } + + private synchronized void purgeCaches() { + mDiscoverInfoCache = null; + } + public EndpointServer getServer() { return mServer; } + public synchronized DiscoverInfo getDiscoverInfo() throws XMPPException.XMPPErrorException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + if (mDiscoverInfoCache == null) { + mDiscoverInfoCache = ServiceDiscoveryManager.getInstanceFor(this) + .discoverInfo(getXMPPServiceDomain()); + } + return mDiscoverInfoCache; + } + + public boolean supportsFeature(String feature) throws XMPPException.XMPPErrorException, + SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + DiscoverInfo cache = getDiscoverInfo(); + return cache.containsFeature(feature); + } + @Override public StanzaListener addStanzaIdAcknowledgedListener(String id, StanzaListener listener) throws StreamManagementException.StreamManagementNotEnabledException { AckMultiListener multi = mStanzaIdAcknowledgedListeners.get(id); diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java index 041e7417a..d39c409f8 100644 --- a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -22,7 +22,6 @@ import java.io.OutputStream; import java.security.GeneralSecurityException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.List; @@ -42,10 +41,11 @@ import org.jivesoftware.smackx.omemo.internal.ClearTextMessage; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; import org.jivesoftware.smackx.omemo.util.OmemoConstants; +import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; -import org.jxmpp.jid.impl.JidCreate; +import org.kontalk.client.KontalkConnection; import org.kontalk.provider.Keyring; @@ -70,10 +70,17 @@ public OmemoCoder(XMPPConnection connection, TrustedRecipient[] recipients) thro if (recipients != null) { for (TrustedRecipient rcpt : recipients) { + BareJid user = rcpt.jid.asBareJid(); Map fingerprints = mManager - .getActiveFingerprints(JidCreate.bareFromOrThrowUnchecked(rcpt.jid)); - if (fingerprints.size() == 0) { - throw new UnsupportedOperationException("Recipient " + rcpt.jid + " does not support OMEMO"); + .getActiveFingerprints(user); + + if (fingerprints.isEmpty()) { + if (!mManager.contactSupportsOmemo(user)) { + throw new UnsupportedOperationException("Recipient " + user + " does not support OMEMO"); + } + + // fingerprints should be available after contactSupportsOmemo() + fingerprints = mManager.getActiveFingerprints(user); } // Trust the OMEMO fingerprints by looking at user trust information. @@ -105,7 +112,8 @@ public OmemoCoder(XMPPConnection connection, Jid sender) throws XMPPException.XM private void init(XMPPConnection connection) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { mManager = OmemoManager.getInstanceFor(connection); - if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) { + // FIXME should be: if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) { + if (!((KontalkConnection) connection).supportsFeature(PubSub.NAMESPACE)) { throw new UnsupportedOperationException("Server does not support OMEMO"); } } @@ -119,7 +127,11 @@ public int getSupportedFlags() { @Override public Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException { Message output; - ArrayList recipients = new ArrayList(Arrays.asList(mRecipients)); + ArrayList recipients = new ArrayList(mRecipients.length); + for (TrustedRecipient rcpt : mRecipients) { + recipients.add(rcpt.jid.asBareJid()); + } + try { output = mManager.encrypt(recipients, message.getBody()); } diff --git a/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java b/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java index 5c82ef621..6755af42b 100644 --- a/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java +++ b/app/src/main/java/org/kontalk/provider/MessagesProviderClient.java @@ -125,7 +125,7 @@ public static Uri newOutgoingMessage(Context context, String msgId, String userI values.put(Messages.UNREAD, false); // of course outgoing messages are not encrypted in database values.put(Messages.ENCRYPTED, false); - values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); + values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT); values.put(Messages.DIRECTION, Messages.DIRECTION_OUT); values.put(Messages.TIMESTAMP, System.currentTimeMillis()); values.put(Messages.STATUS, Messages.STATUS_QUEUED); @@ -137,7 +137,7 @@ public static Uri newOutgoingMessage(Context context, String msgId, String userI values.put(Messages.ATTACHMENT_LOCAL_URI, uri.toString()); values.put(Messages.ATTACHMENT_LENGTH, length); values.put(Messages.ATTACHMENT_COMPRESS, compress); - values.put(Messages.ATTACHMENT_SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); + values.put(Messages.ATTACHMENT_SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT); return context.getContentResolver().insert(Messages.CONTENT_URI, values); } @@ -167,7 +167,7 @@ public static Uri newOutgoingMessage(Context context, String msgId, String userI // of course outgoing messages are not encrypted in database values.put(Messages.ENCRYPTED, false); values.put(Threads.ENCRYPTION, encrypted); - values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); + values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT); return context.getContentResolver().insert( Messages.CONTENT_URI, values); } @@ -462,7 +462,7 @@ public static void setThreadSticky(Context context, long id, boolean sticky) { public static int retryMessage(Context context, Uri uri, boolean encrypted) { ContentValues values = new ContentValues(2); values.put(Messages.STATUS, Messages.STATUS_SENDING); - values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); + values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT); return context.getContentResolver().update(uri, values, null, null); } @@ -501,7 +501,7 @@ public static int retryAllMessages(Context context) { boolean encrypted = Preferences.getEncryptionEnabled(context); ContentValues values = new ContentValues(2); values.put(Messages.STATUS, Messages.STATUS_SENDING); - values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); + values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_ADVANCED : Coder.SECURITY_CLEARTEXT); return context.getContentResolver().update(Messages.CONTENT_URI, values, Messages.STATUS + "=" + Messages.STATUS_PENDING, null); diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java index ca771fa4b..2030fb40f 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java @@ -1877,16 +1877,13 @@ void broadcast(String action, String extraName, String extraValue) { private void discovery() { // FIXME some messed up, absolutely unmodular code here - final XMPPConnection connection = mConnection; + final KontalkConnection connection = mConnection; if (connection != null) { Async.go(new Runnable() { @Override public void run() { try { - ServiceDiscoveryManager manager = ServiceDiscoveryManager - .getInstanceFor(connection); - - DiscoverInfo info = manager.discoverInfo(connection.getXMPPServiceDomain()); + DiscoverInfo info = connection.getDiscoverInfo(); if (info.containsFeature(PushRegistration.NAMESPACE)) { discoverPushRegistration(connection); @@ -2460,6 +2457,9 @@ else if (groupCmdComponent.isPartCommand()) { attachment.getMime(), attachment.getLength(), attachment.getSecurityFlags() != Coder.SECURITY_CLEARTEXT)); } + + // fall back to basic until we'll have a XEP for this + message.setSecurityFlags(Coder.SECURITY_BASIC); } // add location data if present @@ -2470,6 +2470,9 @@ else if (groupCmdComponent.isPartCommand()) { UserLocation userLocation = new UserLocation(lat, lon, location.getText(), location.getStreet()); m.addExtension(userLocation); + + // fall back to basic until we'll have a XEP for this + message.setSecurityFlags(Coder.SECURITY_BASIC); } // add referenced message if any @@ -2534,6 +2537,8 @@ else if (groupCmdComponent.isPartCommand()) { encryptError = true; } catch (GeneralSecurityException e) { + Log.w(TAG, "encryption failed!", e); + // warn user: message will not be sent if (MessagingNotification.isPaused(convJid)) { Toast.makeText(this, R.string.warn_encryption_failed, From c8e92bbbae3be882a4cdadecd4e00af8ccf33e41 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 16 Jun 2019 18:28:44 +0200 Subject: [PATCH 20/30] Unused code Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/provider/Keyring.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/src/main/java/org/kontalk/provider/Keyring.java b/app/src/main/java/org/kontalk/provider/Keyring.java index 58c099fad..e9c835f05 100644 --- a/app/src/main/java/org/kontalk/provider/Keyring.java +++ b/app/src/main/java/org/kontalk/provider/Keyring.java @@ -353,18 +353,6 @@ public static Map fromTrustedFingerprintMap(Map toTrustedFingerprintMap(Map props) { - Map keys = new HashMap<>(props.size()); - for (Map.Entry e : props.entrySet()) { - TrustedFingerprint fingerprint = e.getValue(); - if (fingerprint != null) { - keys.put(e.getKey(), e.toString()); - } - } - return keys; - } - public static final class TrustedFingerprint { public final String fingerprint; public final int trustLevel; From d97b0abea13e7918a50e8ff8525887a533fa516f Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 16 Jun 2019 20:29:36 +0200 Subject: [PATCH 21/30] Remove name from key UID (close #1179) Signed-off-by: Daniele Ricci --- .../kontalk/authenticator/Authenticator.java | 5 +++- .../java/org/kontalk/crypto/PGPUserID.java | 26 +++++++++++++++---- .../java/org/kontalk/crypto/PersonalKey.java | 18 +++---------- .../kontalk/crypto/PersonalKeyExporter.java | 3 ++- .../msgcenter/RegenerateKeyPairListener.java | 3 +-- .../registration/RegistrationService.java | 8 +++--- 6 files changed, 36 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/org/kontalk/authenticator/Authenticator.java b/app/src/main/java/org/kontalk/authenticator/Authenticator.java index 3d20918ed..b660e9931 100644 --- a/app/src/main/java/org/kontalk/authenticator/Authenticator.java +++ b/app/src/main/java/org/kontalk/authenticator/Authenticator.java @@ -199,8 +199,11 @@ public static void exportDefaultPersonalKey(Context ctx, OutputStream dest, Stri // trusted keys Map trustedKeys = Keyring.getTrustedKeys(ctx); + String displayName = m.getUserData(acc, DATA_NAME); + PersonalKeyExporter exp = new PersonalKeyExporter(); - exp.save(privateKey, publicKey, dest, passphrase, exportPassphrase, bridgeCert, trustedKeys, acc.name); + exp.save(privateKey, publicKey, dest, passphrase, exportPassphrase, + bridgeCert, trustedKeys, acc.name, displayName); } public static byte[] getPrivateKeyExportData(Context ctx, String passphrase, String exportPassphrase) diff --git a/app/src/main/java/org/kontalk/crypto/PGPUserID.java b/app/src/main/java/org/kontalk/crypto/PGPUserID.java index b7757cdb9..4e924afb7 100644 --- a/app/src/main/java/org/kontalk/crypto/PGPUserID.java +++ b/app/src/main/java/org/kontalk/crypto/PGPUserID.java @@ -29,6 +29,7 @@ public class PGPUserID { private static final Pattern PATTERN_UID_FULL = Pattern.compile("^(.*) \\((.*)\\) <(.*)>$"); private static final Pattern PATTERN_UID_NO_COMMENT = Pattern.compile("^(.*) <(.*)>$"); + private static final Pattern PATTERN_UID_EMAIL_ONLY = Pattern.compile("^(.*@.*)$"); private final String name; private final String comment; @@ -62,13 +63,19 @@ public String getEmail() { @Override public String toString() { - StringBuilder out = new StringBuilder(name); + StringBuilder out = new StringBuilder(); - if (comment != null) - out.append(" (").append(comment).append(')'); + if (name == null && comment == null && email != null) { + out.append(email); + } + else if (name != null) { + out.append(name); + if (comment != null) + out.append(" (").append(comment).append(')'); - if (email != null) - out.append(" <").append(email).append('>'); + if (email != null) + out.append(" <").append(email).append('>'); + } return out.toString(); } @@ -96,6 +103,15 @@ public static PGPUserID parse(String uid) { } } + // try again with email only + match = PATTERN_UID_EMAIL_ONLY.matcher(uid); + while (match.find()) { + if (match.groupCount() >= 1) { + String email = match.group(1); + return new PGPUserID(null, null, email); + } + } + // no match found return null; } diff --git a/app/src/main/java/org/kontalk/crypto/PersonalKey.java b/app/src/main/java/org/kontalk/crypto/PersonalKey.java index d381da040..d5764c7ff 100644 --- a/app/src/main/java/org/kontalk/crypto/PersonalKey.java +++ b/app/src/main/java/org/kontalk/crypto/PersonalKey.java @@ -126,24 +126,12 @@ public String getFingerprint() { return PGP.getFingerprint(mPair.authKey.getPublicKey()); } - public PGPKeyPairRing storeNetwork(String userId, String network, String name, String passphrase) throws PGPException, IOException { - return store(name, userId + '@' + network, null, passphrase); + public PGPKeyPairRing storeNetwork(String userId, String network, String passphrase) throws PGPException, IOException { + return store(null, userId + '@' + network, null, passphrase); } public PGPKeyPairRing store(String name, String email, String comment, String passphrase) throws PGPException, IOException { - // name[ (comment)] <[email]> - StringBuilder userid = new StringBuilder(name); - - if (comment != null) userid - .append(" (") - .append(comment) - .append(')'); - - userid.append(" <"); - if (email != null) - userid.append(email); - userid.append('>'); - + PGPUserID userid = new PGPUserID(name, comment, email); return PGP.store(mPair, userid.toString(), passphrase); } diff --git a/app/src/main/java/org/kontalk/crypto/PersonalKeyExporter.java b/app/src/main/java/org/kontalk/crypto/PersonalKeyExporter.java index 342db0840..49d29ffc9 100644 --- a/app/src/main/java/org/kontalk/crypto/PersonalKeyExporter.java +++ b/app/src/main/java/org/kontalk/crypto/PersonalKeyExporter.java @@ -48,7 +48,7 @@ public class PersonalKeyExporter implements PersonalKeyPack { public void save(byte[] privateKey, byte[] publicKey, OutputStream dest, String passphrase, String exportPassphrase, byte[] bridgeCert, - Map trustedKeys, String phoneNumber) + Map trustedKeys, String phoneNumber, String displayName) throws PGPException, IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException { // put everything in a zip file @@ -127,6 +127,7 @@ public void save(byte[] privateKey, byte[] publicKey, OutputStream dest, String // export account info Properties info = new Properties(); info.setProperty("phoneNumber", phoneNumber); + info.setProperty("displayName", displayName); zip.putNextEntry(new ZipEntry(ACCOUNT_INFO_FILENAME)); info.store(zip, null); zip.closeEntry(); diff --git a/app/src/main/java/org/kontalk/service/msgcenter/RegenerateKeyPairListener.java b/app/src/main/java/org/kontalk/service/msgcenter/RegenerateKeyPairListener.java index cd95aa5ed..24a42eb82 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/RegenerateKeyPairListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/RegenerateKeyPairListener.java @@ -103,10 +103,9 @@ public void run(PersonalKey key) { Context context = getContext(); AccountManager am = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); Account acc = Authenticator.getDefaultAccount(am); - String name = Authenticator.getDisplayName(am, acc); String userId = XMPPUtils.createLocalpart(acc.name); - mKeyRing = key.storeNetwork(userId, getServer().getNetwork(), name, + mKeyRing = key.storeNetwork(userId, getServer().getNetwork(), // TODO should we ask passphrase to the user? Kontalk.get().getCachedPassphrase()); diff --git a/app/src/main/java/org/kontalk/service/registration/RegistrationService.java b/app/src/main/java/org/kontalk/service/registration/RegistrationService.java index b62c07139..9efd5a347 100644 --- a/app/src/main/java/org/kontalk/service/registration/RegistrationService.java +++ b/app/src/main/java/org/kontalk/service/registration/RegistrationService.java @@ -730,6 +730,10 @@ public void onImportKeyRequest(ImportKeyRequest request) { if (!TextUtils.isEmpty(phoneNumber)) { cstate.phoneNumber = phoneNumber; } + String displayName = accountInfo.get("displayName"); + if (!TextUtils.isEmpty(displayName)) { + cstate.displayName = displayName; + } } // personal key corrupted or too old @@ -743,7 +747,6 @@ public void onImportKeyRequest(ImportKeyRequest request) { cstate.server = (request.server != null) ? request.server : new EndpointServer(XmppStringUtils.parseDomain(uid.getEmail())); - cstate.displayName = uid.getName(); cstate.passphrase = request.passphrase; // copy over the parsed keys (imported keys may be armored) cstate.privateKey = privateKeyBuf.toByteArray(); @@ -962,7 +965,7 @@ public void onChallengeRequest(ChallengeRequest request) { // needed for connection String userId = XMPPUtils.createLocalpart(cstate.phoneNumber); keyRing = cstate.key.storeNetwork(userId, mConnector.getNetwork(), - cstate.displayName, cstate.passphrase); + cstate.passphrase); } else { keyRing = PersonalKey.test(cstate.privateKey, cstate.publicKey, cstate.passphrase, null); @@ -1192,7 +1195,6 @@ private void loadRetrievedKey() { cstate.server = (cstate.server != null) ? cstate.server : new EndpointServer(XmppStringUtils.parseDomain(uid.getEmail())); - cstate.displayName = uid.getName(); // copy over the parsed keys (it should be the same, but you never know...) cstate.privateKey = privateKeyBuf.toByteArray(); cstate.publicKey = publicKeyBuf.toByteArray(); From 691b4a3e0ab030e3d17590512a61454fa0a59493 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 11 Aug 2019 14:26:57 +0200 Subject: [PATCH 22/30] Init instance only when sure Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/crypto/OmemoCoder.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java index d39c409f8..c2ef3afce 100644 --- a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -111,11 +111,12 @@ public OmemoCoder(XMPPConnection connection, Jid sender) throws XMPPException.XM private void init(XMPPConnection connection) throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { - mManager = OmemoManager.getInstanceFor(connection); // FIXME should be: if (!OmemoManager.serverSupportsOmemo(connection, connection.getXMPPServiceDomain())) { if (!((KontalkConnection) connection).supportsFeature(PubSub.NAMESPACE)) { throw new UnsupportedOperationException("Server does not support OMEMO"); } + + mManager = OmemoManager.getInstanceFor(connection); } @Override From 5330d974b1c6f67714494a1b0b1f9129b95a7747 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Thu, 19 Mar 2020 19:15:08 +0100 Subject: [PATCH 23/30] Handle disconnections during OMEMO stuff (#1260) Signed-off-by: Daniele Ricci --- app/src/main/java/org/kontalk/provider/Keyring.java | 11 ++++++++++- .../service/msgcenter/MessageCenterService.java | 11 +++++++++++ app/src/main/java/org/kontalk/util/MessageUtils.java | 3 ++- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/kontalk/provider/Keyring.java b/app/src/main/java/org/kontalk/provider/Keyring.java index aea16bebe..d9339538d 100644 --- a/app/src/main/java/org/kontalk/provider/Keyring.java +++ b/app/src/main/java/org/kontalk/provider/Keyring.java @@ -24,6 +24,7 @@ import java.util.Iterator; import java.util.Map; +import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPConnection; import org.jxmpp.jid.Jid; import org.bouncycastle.openpgp.PGPException; @@ -68,7 +69,10 @@ private Keyring() { } /** Returns a {@link Coder} instance for encrypting data. */ - public static Coder getEncryptCoder(Context context, int securityFlags, XMPPConnection connection, EndpointServer server, PersonalKey key, Jid[] recipients) { + public static Coder getEncryptCoder(Context context, int securityFlags, + XMPPConnection connection, EndpointServer server, PersonalKey key, Jid[] recipients) + throws SmackException.NotConnectedException { + if ((securityFlags & Coder.SECURITY_ADVANCED) != 0) { try { if (recipients.length == 1 && recipients[0].equals(connection.getUser().asBareJid())) { @@ -76,6 +80,11 @@ public static Coder getEncryptCoder(Context context, int securityFlags, XMPPConn } return new OmemoCoder(connection, getTrustedRecipients(context, recipients)); } + catch (SmackException.NotConnectedException e) { + // not connected to the server, notify to the caller + // so it can skip the message and let it retry later + throw e; + } catch (Exception e) { Log.w(TAG, "unable to setup advanced coder, falling back to basic", e); securityFlags = Coder.SECURITY_BASIC; diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java index dfc0aab82..fde02a4f6 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java @@ -1357,6 +1357,11 @@ public void handleUploadAttachment(UploadAttachmentRequest request) { fileLength = MediaStorage.getLength(this, preMediaUri); } } + catch (SmackException.NotConnectedException e) { + // will retry at next reconnection + Log.w(TAG, "not connected, encryption failed, will send message later", e); + return; + } catch (Exception e) { Log.w(TAG, "error preprocessing media: " + preMediaUri, e); // simulate upload error @@ -2563,6 +2568,12 @@ else if (groupCmdComponent.isPartCommand()) { } encryptError = true; } + catch (SmackException.NotConnectedException e) { + // will retry at next reconnection + Log.w(TAG, "not connected, encryption failed, will send message later", e); + mIdleHandler.release(); + return; + } if (encryptError) { // message was not encrypted for some reason, mark it pending user review diff --git a/app/src/main/java/org/kontalk/util/MessageUtils.java b/app/src/main/java/org/kontalk/util/MessageUtils.java index 20e6aaa4c..aa90bac69 100644 --- a/app/src/main/java/org/kontalk/util/MessageUtils.java +++ b/app/src/main/java/org/kontalk/util/MessageUtils.java @@ -33,6 +33,7 @@ import com.google.i18n.phonenumbers.PhoneNumberUtil; import com.google.i18n.phonenumbers.Phonenumber; +import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.util.StringUtils; import org.jxmpp.jid.Jid; import org.bouncycastle.openpgp.PGPException; @@ -581,7 +582,7 @@ public static String messageId() { } public static File encryptFile(Context context, InputStream in, Jid[] users) - throws GeneralSecurityException, IOException, PGPException { + throws GeneralSecurityException, IOException, PGPException, SmackException.NotConnectedException { PersonalKey key = Kontalk.get().getPersonalKey(); EndpointServer server = Preferences.getEndpointServer(context); // TODO advanced coder not supported yet From 780ad2fc987e6b179d0c915fbdb6a33ad80c0cf7 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sun, 22 Mar 2020 19:50:45 +0100 Subject: [PATCH 24/30] Cache contact security flags Signed-off-by: Daniele Ricci --- .../main/java/org/kontalk/crypto/Coder.java | 36 +++++++++++++++++++ .../main/java/org/kontalk/data/Contact.java | 28 ++++++++++++++- .../java/org/kontalk/provider/Keyring.java | 5 +++ .../msgcenter/MessageCenterService.java | 5 +++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/crypto/Coder.java b/app/src/main/java/org/kontalk/crypto/Coder.java index e3e5e5e4f..7f38974ac 100644 --- a/app/src/main/java/org/kontalk/crypto/Coder.java +++ b/app/src/main/java/org/kontalk/crypto/Coder.java @@ -119,6 +119,42 @@ public static boolean isError(int securityFlags) { (securityFlags & SECURITY_ERROR_PUBLIC_KEY_UNAVAILABLE) != 0; } + /** + * Calculate the common security flags supported by all given users flags. + * @param requestedFlags what flags we would like + * @param supportedFlags what flags users support + * @return calculated flags + */ + public static int getCompatibleSecurityFlags(int requestedFlags, int[] supportedFlags) { + // FIXME not really checking flags, but I know what I'm doing here and also I'm lazy + int finalFlags = -1; + for (int flags : supportedFlags) { + if (flags < 0) { + // unknown security, just skip it + continue; + } + int calculatedFlags = getCompatibleSecurityFlags(requestedFlags, flags); + if (finalFlags >= 0) { + finalFlags = Math.min(finalFlags, calculatedFlags); + } + else { + finalFlags = calculatedFlags; + } + } + return finalFlags < 0 ? requestedFlags : finalFlags; + } + + /** + * Calculate the security flags supported by the given users flags. + * @param requestedFlags what flags we would like + * @param supportedFlags what flags the user supports + * @return calculated flags + */ + public static int getCompatibleSecurityFlags(int requestedFlags, int supportedFlags) { + // FIXME not really checking flags, but I know what I'm doing here and also I'm lazy + return Math.min(requestedFlags, supportedFlags); + } + public static class DecryptOutput { public final String mime; public final Message cleartext; diff --git a/app/src/main/java/org/kontalk/data/Contact.java b/app/src/main/java/org/kontalk/data/Contact.java index 5dd7d0bb1..22d07bade 100644 --- a/app/src/main/java/org/kontalk/data/Contact.java +++ b/app/src/main/java/org/kontalk/data/Contact.java @@ -30,6 +30,7 @@ import com.amulyakhare.textdrawable.util.ColorGenerator; import org.bouncycastle.util.Arrays; +import org.jxmpp.jid.Jid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.stringprep.XmppStringprepException; import org.jxmpp.util.XmppStringUtils; @@ -62,7 +63,6 @@ import org.kontalk.crypto.PGPLazyPublicKeyRingLoader; import org.kontalk.provider.Keyring; import org.kontalk.provider.MessagesProviderClient; -import org.kontalk.provider.MyUsers.Keys; import org.kontalk.provider.MyUsers.Users; import org.kontalk.util.MediaStorage; import org.kontalk.util.MessageUtils; @@ -133,6 +133,9 @@ public class Contact { /** Cached name information from system contacts. It will override our internal name. */ private StructuredName mStructuredName; + /** Supported security flags. Detected by {@link org.kontalk.crypto.Coder}s. */ + private int mSecurityFlags = -1; + private static final class StructuredName { public final String displayName; public final String givenName; @@ -505,6 +508,14 @@ public void setLastSeen(long lastSeen) { mLastSeen = lastSeen; } + public int getSecurityFlags() { + return mSecurityFlags; + } + + public void setSecurityFlags(int securityFlags) { + mSecurityFlags = securityFlags; + } + public boolean isSelf(Context context) { try { return Authenticator.isSelfJID(context, JidCreate.bareFrom(mJID)); @@ -857,6 +868,21 @@ private static byte[] loadAvatarData(Context context, Uri contactUri) { return data; } + public static int[] getSecurityFlags(Jid[] users) { + final int[] flags = new int[users.length]; + for (int i = 0; i < users.length; i++) { + // hit the cache directly since security flags are not in the database + Contact contact = cache.get(users[i].toString()); + if (contact == null) { + flags[i] = -1; + } + else { + flags[i] = contact.getSecurityFlags(); + } + } + return flags; + } + public static Cursor queryContacts(Context context) { String selection = Users.REGISTERED + " <> 0"; if (!Preferences.getShowBlockedUsers(context)) { diff --git a/app/src/main/java/org/kontalk/provider/Keyring.java b/app/src/main/java/org/kontalk/provider/Keyring.java index d9339538d..74ab31640 100644 --- a/app/src/main/java/org/kontalk/provider/Keyring.java +++ b/app/src/main/java/org/kontalk/provider/Keyring.java @@ -44,6 +44,7 @@ import org.kontalk.crypto.PGP; import org.kontalk.crypto.PGPCoder; import org.kontalk.crypto.PersonalKey; +import org.kontalk.data.Contact; /** @@ -73,6 +74,10 @@ public static Coder getEncryptCoder(Context context, int securityFlags, XMPPConnection connection, EndpointServer server, PersonalKey key, Jid[] recipients) throws SmackException.NotConnectedException { + // calculate supported security flags given previous discoveries + int[] supportedFlags = Contact.getSecurityFlags(recipients); + securityFlags = Coder.getCompatibleSecurityFlags(securityFlags, supportedFlags); + if ((securityFlags & Coder.SECURITY_ADVANCED) != 0) { try { if (recipients.length == 1 && recipients[0].equals(connection.getUser().asBareJid())) { diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java index 15c1e6670..1e425bc7a 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java @@ -2535,6 +2535,11 @@ else if (groupCmdComponent.isPartCommand()) { try { Coder coder = Keyring.getEncryptCoder(this, message.getSecurityFlags(), mConnection, mServer, key, toGroup); + // cache security flags + for (Jid jid : toGroup) { + Contact.findByUserId(this, jid.asBareJid().toString()) + .setSecurityFlags(coder.getSupportedFlags()); + } // security flags changed (most probably because of coder fallback) // update message accordingly From 035d47bacd4c4db50ac91a955f81a9db9b5b3cbb Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sat, 28 Mar 2020 20:44:53 +0100 Subject: [PATCH 25/30] Revert ZXing to 3.3.3 to allow API level < 24 Sorry people, I missed this... Signed-off-by: Daniele Ricci --- app/build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f1f18e421..f40768452 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -215,7 +215,8 @@ dependencies { // other libraries implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.0' - implementation 'com.google.zxing:core:3.4.0' + // TODO version 3.4 requires API level 24 (Java 8) + implementation 'com.google.zxing:core:3.3.3' implementation 'me.dm7.barcodescanner:zxing:1.9.8' implementation 'com.github.instacart.truetime-android:library:3.3' implementation 'org.greenrobot:eventbus:3.2.0' From 992fb68e952da82b3b826603a85c3bf6f39c2915 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sat, 28 Mar 2020 20:45:48 +0100 Subject: [PATCH 26/30] Upgrade dependencies Signed-off-by: Daniele Ricci --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f40768452..cf6c414cf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -209,7 +209,7 @@ dependencies { implementation 'me.leolin:ShortcutBadger:1.1.22@aar' implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.paging:paging-runtime:2.1.1' + implementation 'androidx.paging:paging-runtime:2.1.2' implementation 'androidx.preference:preference:1.1.0' implementation 'de.hdodenhof:circleimageview:3.1.0' @@ -230,11 +230,11 @@ dependencies { googleplayImplementation "com.github.kontalk.AnyMaps:anymaps-google:$anyMapsVersion" // Google Play Services - googleplayImplementation "com.google.android.gms:play-services-base:17.1.0" + googleplayImplementation "com.google.android.gms:play-services-base:17.2.1" googleplayImplementation "com.google.android.gms:play-services-maps:17.0.0" googleplayImplementation "com.google.android.gms:play-services-location:17.0.0" googleplayImplementation 'com.google.firebase:firebase-core:17.2.3' - googleplayImplementation 'com.google.firebase:firebase-messaging:20.1.2' + googleplayImplementation 'com.google.firebase:firebase-messaging:20.1.3' googleplayImplementation('com.crashlytics.sdk.android:crashlytics:2.10.1@aar') { transitive = true } From a3667c22aafd8eed262cce5c0d1b06c7db2e1f04 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Sat, 28 Mar 2020 20:52:02 +0100 Subject: [PATCH 27/30] Upgrade dependencies Signed-off-by: Daniele Ricci --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index cf6c414cf..29d46e010 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -170,7 +170,7 @@ dependencies { implementation "org.igniterealtime.smack:smack-android:$smackVersion" implementation fileTree(dir: 'libs', include: "smack-omemo-${smackVersion}.jar") implementation fileTree(dir: 'libs', include: "smack-omemo-signal-${smackVersion}.jar") - implementation 'org.whispersystems:signal-protocol-java:2.4.0' + implementation 'org.whispersystems:signal-protocol-java:2.8.1' implementation 'info.guardianproject.netcipher:netcipher:1.2.1' implementation 'com.squareup.okhttp3:okhttp:4.4.1' implementation 'com.segment.backo:backo:1.0.0' From 3e135a8bef86b70cb3d2567223ea2bd328de3f43 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Wed, 22 Apr 2020 00:06:52 +0200 Subject: [PATCH 28/30] Merge branch 'master' into feature/omemo Signed-off-by: Daniele Ricci --- .../java/org/kontalk/service/msgcenter/MessageListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index abbc545b1..14d073676 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -365,7 +365,7 @@ else if (m.hasExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage. Context context = getContext(); EndpointServer server = getServer(); if (server == null) - server = Preferences.getEndpointServer(context); + server = Kontalk.get().getEndpointServer(); PersonalKey key = Kontalk.get().getPersonalKey(); From 58fdd3553822f2b9169e79ee71439eef746de212 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Wed, 22 Apr 2020 20:58:24 +0200 Subject: [PATCH 29/30] Preliminary porting to Smack 4.4 Signed-off-by: Daniele Ricci --- client-common-java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-common-java b/client-common-java index 4fae0985f..74c9a74c5 160000 --- a/client-common-java +++ b/client-common-java @@ -1 +1 @@ -Subproject commit 4fae0985f53837ee409b4fd4d7f49290a8cbf6dd +Subproject commit 74c9a74c599b99a307a9885b858cb736efbec346 From aa8c9b1ab52f87ffb056b9a08f527283f3bebee7 Mon Sep 17 00:00:00 2001 From: Daniele Ricci Date: Wed, 22 Apr 2020 20:59:06 +0200 Subject: [PATCH 30/30] Preliminary porting to Smack 4.4 Brings Java to 1.8 and minimum API level to 19 Signed-off-by: Daniele Ricci --- app/build.gradle | 10 +- app/proguard.cfg | 3 + .../org/kontalk/client/SmackInitializer.java | 13 +- .../org/kontalk/client/smack/SmackFuture.java | 35 +- .../client/smack/XMPPTCPConnection.java | 458 +++++------------- .../java/org/kontalk/crypto/OmemoCoder.java | 70 +-- .../java/org/kontalk/crypto/PGPCoder.java | 2 +- .../msgcenter/LastActivityListener.java | 4 +- .../msgcenter/MessageCenterService.java | 8 +- .../service/msgcenter/MessageListener.java | 20 +- .../service/msgcenter/PresenceListener.java | 10 +- .../msgcenter/PrivateKeyUploadListener.java | 16 +- .../msgcenter/RegisterKeyPairListener.java | 25 +- .../msgcenter/event/PublicKeyRequest.java | 4 +- .../registration/RegistrationService.java | 68 ++- .../kontalk/upload/HTTPFileUploadService.java | 22 +- .../main/java/org/kontalk/util/XMPPUtils.java | 27 +- build.gradle | 2 +- 18 files changed, 295 insertions(+), 502 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 09d4e2684..2e5d1457a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -165,7 +165,7 @@ android { dependencies { api (project(':client-common-java')) { - exclude group: 'net.sf.kxml' + exclude group: 'xpp3', module: 'xpp3_min' } // support libraries @@ -187,9 +187,11 @@ dependencies { // network/protocol libraries implementation "org.igniterealtime.smack:smack-tcp:$smackVersion" implementation "org.igniterealtime.smack:smack-experimental:$smackVersion" - implementation "org.igniterealtime.smack:smack-android:$smackVersion" - implementation fileTree(dir: 'libs', include: "smack-omemo-${smackVersion}.jar") - implementation fileTree(dir: 'libs', include: "smack-omemo-signal-${smackVersion}.jar") + implementation ("org.igniterealtime.smack:smack-android:$smackVersion") { + exclude group: 'xpp3', module: 'xpp3_min' + } + implementation "org.igniterealtime.smack:smack-omemo:$smackVersion" + implementation "org.igniterealtime.smack:smack-omemo-signal:$smackVersion" implementation 'org.whispersystems:signal-protocol-java:2.8.1' implementation 'info.guardianproject.netcipher:netcipher:1.2.1' implementation 'com.squareup.okhttp3:okhttp:4.5.0' diff --git a/app/proguard.cfg b/app/proguard.cfg index 0a93a080e..0522f2894 100644 --- a/app/proguard.cfg +++ b/app/proguard.cfg @@ -50,7 +50,10 @@ -keep class org.bouncycastle.openpgp.** { *; } # Smack Core classes should be figured out by Proguard +# FIXME not really! -keep class org.jivesoftware.smack.initializer.** { *; } +-keep class org.jivesoftware.smack.packet.** { *; } +-keep class org.jivesoftware.smack.**.packet.** { *; } # keep Smack IM -keep class org.jivesoftware.smack.im.** { *; } diff --git a/app/src/main/java/org/kontalk/client/SmackInitializer.java b/app/src/main/java/org/kontalk/client/SmackInitializer.java index 24963c053..6606b4972 100644 --- a/app/src/main/java/org/kontalk/client/SmackInitializer.java +++ b/app/src/main/java/org/kontalk/client/SmackInitializer.java @@ -28,7 +28,8 @@ import org.jivesoftware.smack.roster.Roster; import org.jivesoftware.smackx.iqregister.provider.RegistrationProvider; import org.jivesoftware.smackx.iqversion.VersionManager; -import org.jivesoftware.smackx.omemo.OmemoConfiguration; +import org.jivesoftware.smackx.omemo.signal.SignalCachingOmemoStore; +import org.jivesoftware.smackx.omemo.signal.SignalFileBasedOmemoStore; import org.jivesoftware.smackx.omemo.signal.SignalOmemoService; import org.jivesoftware.smackx.xdata.provider.DataFormProvider; import org.minidns.dnsserverlookup.android21.AndroidUsingLinkProperties; @@ -45,6 +46,8 @@ public class SmackInitializer { private static boolean sInitialized; + // TODO not sure what to do with this for now + private static SignalOmemoService sOmemoService; public static void initialize(Context context) { if (!sInitialized) { @@ -76,8 +79,12 @@ public static void initialize(Context context) { SignalOmemoService.acknowledgeLicense(); try { SignalOmemoService.setup(); - OmemoConfiguration.setFileBasedOmemoStoreDefaultPath - (new File(context.getFilesDir(), "omemo")); + sOmemoService = (SignalOmemoService) SignalOmemoService.getInstance(); + sOmemoService.setOmemoStoreBackend( + new SignalCachingOmemoStore( + new SignalFileBasedOmemoStore(new File(context.getFilesDir(), "omemo")) + ) + ); } catch (Exception e) { // this shouldn't happen, so we just crash for now diff --git a/app/src/main/java/org/kontalk/client/smack/SmackFuture.java b/app/src/main/java/org/kontalk/client/smack/SmackFuture.java index d8756ef4c..08c7b6d96 100644 --- a/app/src/main/java/org/kontalk/client/smack/SmackFuture.java +++ b/app/src/main/java/org/kontalk/client/smack/SmackFuture.java @@ -1,6 +1,6 @@ /** * - * Copyright 2017-2018 Florian Schmaus + * Copyright 2017-2020 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,9 @@ import java.io.IOException; import java.net.Socket; import java.net.SocketAddress; +import java.util.Collection; import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -29,10 +31,10 @@ import javax.net.SocketFactory; -import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.util.CallbackRecipient; +import org.jivesoftware.smack.util.Consumer; import org.jivesoftware.smack.util.ExceptionCallback; import org.jivesoftware.smack.util.SuccessCallback; @@ -50,6 +52,8 @@ public abstract class SmackFuture implements Future, private ExceptionCallback exceptionCallback; + private Consumer> completionCallback; + @Override public final synchronized boolean cancel(boolean mayInterruptIfRunning) { if (isDone()) { @@ -89,8 +93,13 @@ public CallbackRecipient onError(ExceptionCallback exceptionCallback) { return this; } + public void onCompletion(Consumer> completionCallback) { + this.completionCallback = completionCallback; + maybeInvokeCallbacks(); + } + private V getOrThrowExecutionException() throws ExecutionException { - assert (result != null || exception != null || cancelled); + assert result != null || exception != null || cancelled; if (result != null) { return result; } @@ -98,7 +107,7 @@ private V getOrThrowExecutionException() throws ExecutionException { throw new ExecutionException(exception); } - assert (cancelled); + assert cancelled; throw new CancellationException(); } @@ -150,11 +159,19 @@ public final synchronized V get(long timeout, TimeUnit unit) return getOrThrowExecutionException(); } + public V getIfAvailable() { + return result; + } + protected final synchronized void maybeInvokeCallbacks() { if (cancelled) { return; } + if ((result != null || exception != null) && completionCallback != null) { + completionCallback.accept(this); + } + if (result != null && successCallback != null) { AbstractXMPPConnectionWrapper.asyncGo(new Runnable() { @Override @@ -294,7 +311,7 @@ public final synchronized void processStanza(Stanza stanza) { * A simple version of InternalSmackFuture which implements isNonFatalException(E) as always returning * false method. * - * @param + * @param the return value of the future. */ public abstract static class SimpleInternalProcessStanzaSmackFuture extends InternalProcessStanzaSmackFuture { @@ -310,4 +327,12 @@ public static SmackFuture from(V result) { return future; } + public static boolean await(Collection> futures, long timeout, TimeUnit unit) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(futures.size()); + for (SmackFuture future : futures) { + future.onCompletion(f -> latch.countDown()); + } + + return latch.await(timeout, unit); + } } diff --git a/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java b/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java index 464374586..bbe0fb5b6 100644 --- a/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java +++ b/app/src/main/java/org/kontalk/client/smack/XMPPTCPConnection.java @@ -17,26 +17,19 @@ package org.kontalk.client.smack; import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; -import java.lang.reflect.Constructor; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.security.KeyManagementException; -import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import java.security.Provider; -import java.security.SecureRandom; -import java.security.Security; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.ArrayList; @@ -59,32 +52,24 @@ import javax.net.SocketFactory; import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.PasswordCallback; import org.jivesoftware.smack.AbstractConnectionListener; import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.ConnectionConfiguration; -import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.AlreadyConnectedException; import org.jivesoftware.smack.SmackException.AlreadyLoggedInException; import org.jivesoftware.smack.SmackException.ConnectionException; +import org.jivesoftware.smack.SmackException.EndpointConnectionException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.SmackException.SecurityRequiredByServerException; -import org.jivesoftware.smack.SmackException.SmackWrappedException; +import org.jivesoftware.smack.SmackFuture; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.SynchronizationPoint; import org.jivesoftware.smack.XMPPConnection; @@ -94,6 +79,7 @@ import org.jivesoftware.smack.compress.packet.Compress; import org.jivesoftware.smack.compress.packet.Compressed; import org.jivesoftware.smack.compression.XMPPInputOutputStream; +import org.jivesoftware.smack.datatypes.UInt16; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.packet.Element; import org.jivesoftware.smack.packet.IQ; @@ -103,12 +89,8 @@ import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StartTls; import org.jivesoftware.smack.packet.StreamError; -import org.jivesoftware.smack.packet.StreamOpen; import org.jivesoftware.smack.proxy.ProxyInfo; -import org.jivesoftware.smack.sasl.packet.SaslStreamElements; -import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Challenge; -import org.jivesoftware.smack.sasl.packet.SaslStreamElements.SASLFailure; -import org.jivesoftware.smack.sasl.packet.SaslStreamElements.Success; +import org.jivesoftware.smack.sasl.packet.SaslNonza; import org.jivesoftware.smack.sm.SMUtils; import org.jivesoftware.smack.sm.StreamManagementException; import org.jivesoftware.smack.sm.StreamManagementException.StreamIdDoesNotMatchException; @@ -126,24 +108,24 @@ import org.jivesoftware.smack.sm.predicates.Predicate; import org.jivesoftware.smack.sm.provider.ParseStreamManagement; import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jivesoftware.smack.tcp.rce.RemoteXmppTcpConnectionEndpoints; +import org.jivesoftware.smack.tcp.rce.Rfc6120TcpRemoteConnectionEndpoint; import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; import org.jivesoftware.smack.util.Async; -import org.jivesoftware.smack.util.DNSUtil; +import org.jivesoftware.smack.util.CloseableUtil; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.TLSUtils; import org.jivesoftware.smack.util.XmlStringBuilder; -import org.jivesoftware.smack.util.dns.HostAddress; -import org.jivesoftware.smack.util.dns.SmackDaneProvider; -import org.jivesoftware.smack.util.dns.SmackDaneVerifier; +import org.jivesoftware.smack.util.rce.RemoteConnectionException; +import org.jivesoftware.smack.xml.SmackXmlParser; +import org.jivesoftware.smack.xml.XmlPullParser; +import org.jivesoftware.smack.xml.XmlPullParserException; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.stringprep.XmppStringprepException; -import org.jxmpp.util.XmppStringUtils; import org.minidns.dnsname.DnsName; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; /** * Creates a socket connection to an XMPP server. This is the default connection @@ -181,9 +163,6 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { */ protected final PacketReader packetReader = new PacketReader(); - private final SynchronizationPoint initialOpenStreamSend = new SynchronizationPoint<>( - this, "initial open stream element send to server"); - /** * */ @@ -196,16 +175,9 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { private final SynchronizationPoint compressSyncPoint = new SynchronizationPoint<>( this, "stream compression"); - /** - * A synchronization point which is successful if this connection has received the closing - * stream element from the remote end-point, i.e. the server. - */ - private final SynchronizationPoint closingStreamReceived = new SynchronizationPoint<>( - this, "stream closing element received"); - /** * The default bundle and defer callback, used for new connections. - * @see #bundleAndDeferCallback + * @see bundleAndDeferCallback */ private static BundleAndDeferCallback defaultBundleAndDeferCallback; @@ -346,6 +318,10 @@ public void connectionClosedOnError(Exception e) { } } }); + + // Re-init the reader and writer in case of SASL . This is done to reset the parser since a new stream + // is initiated. + buildNonzaCallback().listenFor(SaslNonza.Success.class, s -> resetParser()).install(); } /** @@ -358,10 +334,10 @@ public void connectionClosedOnError(Exception e) { * * @param jid the bare JID used by the client. * @param password the password or authentication token. - * @throws XmppStringprepException + * @throws XmppStringprepException if the provided string is invalid. */ public XMPPTCPConnection(CharSequence jid, String password) throws XmppStringprepException { - this(XmppStringUtils.parseLocalpart(jid.toString()), password, XmppStringUtils.parseDomain(jid.toString())); + this(XMPPTCPConnectionConfiguration.builder().setXmppAddressAndPassword(jid, password).build()); } /** @@ -371,10 +347,10 @@ public XMPPTCPConnection(CharSequence jid, String password) throws XmppStringpre * you can get fine-grained control over connection settings using the * {@link #XMPPTCPConnection(XMPPTCPConnectionConfiguration)} constructor. *

- * @param username - * @param password - * @param serviceName - * @throws XmppStringprepException + * @param username TODO javadoc me please + * @param password TODO javadoc me please + * @param serviceName TODO javadoc me please + * @throws XmppStringprepException if the provided string is invalid. */ public XMPPTCPConnection(CharSequence username, String password, String serviceName) throws XmppStringprepException { this(XMPPTCPConnectionConfiguration.builder().setUsernameAndPassword(username, password).setXmppDomain( @@ -415,7 +391,7 @@ protected synchronized void loginInternal(String username, String password, Reso SmackException, IOException, InterruptedException { // Authenticate using SASL SSLSession sslSession = secureSocket != null ? secureSocket.getSession() : null; - saslAuthentication.authenticate(username, password, config.getAuthzid(), sslSession); + authenticate(username, password, config.getAuthzid(), sslSession); // Wait for stream features after the authentication. // TODO: The name of this synchronization point "maybeCompressFeaturesReceived" is not perfect. It should be @@ -532,29 +508,14 @@ private void shutdown(boolean instant) { LOGGER.finer("PacketWriter has been shut down"); if (!instant) { - try { - // After we send the closing stream element, check if there was already a - // closing stream element sent by the server or wait with a timeout for a - // closing stream element to be received from the server. - @SuppressWarnings("unused") - Exception res = closingStreamReceived.checkIfSuccessOrWait(); - } catch (InterruptedException | NoResponseException e) { - LOGGER.log(Level.INFO, "Exception while waiting for closing stream element from the server " + this, e); - } + waitForClosingStreamTagFromServer(); } LOGGER.finer("PacketReader shutdown()"); packetReader.shutdown(); LOGGER.finer("PacketReader has been shut down"); - final Socket socket = this.socket; - if (socket != null && socket.isConnected()) { - try { - socket.close(); - } catch (Exception e) { - LOGGER.log(Level.WARNING, "shutdown", e); - } - } + CloseableUtil.maybeClose(socket, LOGGER); setWasAuthenticated(); @@ -595,7 +556,6 @@ protected void initState() { compressSyncPoint.init(); smResumedSyncPoint.init(); smEnabledSyncPoint.init(); - initialOpenStreamSend.init(); } @Override @@ -617,20 +577,24 @@ protected void sendStanzaInternal(Stanza packet) throws NotConnectedException, I } private void connectUsingConfiguration() throws ConnectionException, IOException, InterruptedException { - List failedAddresses = populateHostAddresses(); + RemoteXmppTcpConnectionEndpoints.Result result = RemoteXmppTcpConnectionEndpoints.lookup(config); + + List> connectionExceptions = new ArrayList<>(); + SocketFactory socketFactory = config.getSocketFactory(); ProxyInfo proxyInfo = config.getProxyInfo(); int timeout = config.getConnectTimeout(); if (socketFactory == null) { socketFactory = SocketFactory.getDefault(); } - for (HostAddress hostAddress : hostAddresses) { - Iterator inetAddresses; - String host = hostAddress.getHost(); - int port = hostAddress.getPort(); + for (Rfc6120TcpRemoteConnectionEndpoint endpoint : result.discoveredRemoteConnectionEndpoints) { + Iterator inetAddresses; + String host = endpoint.getHost().toString(); + UInt16 portUint16 = endpoint.getPort(); + int port = portUint16.intValue(); if (proxyInfo == null) { - inetAddresses = hostAddress.getInetAddresses().iterator(); - assert (inetAddresses.hasNext()); + inetAddresses = endpoint.getInetAddresses().iterator(); + assert inetAddresses.hasNext(); innerloop: while (inetAddresses.hasNext()) { // Create a *new* Socket before every connection attempt, i.e. connect() call, since Sockets are not @@ -645,7 +609,9 @@ private void connectUsingConfiguration() throws ConnectionException, IOException try { socket = socketFuture.getOrThrow(); } catch (IOException e) { - hostAddress.setException(inetAddress, e); + RemoteConnectionException rce = new RemoteConnectionException<>( + endpoint, inetAddress, e); + connectionExceptions.add(rce); if (inetAddresses.hasNext()) { continue innerloop; } else { @@ -655,33 +621,36 @@ private void connectUsingConfiguration() throws ConnectionException, IOException LOGGER.finer("Established TCP connection to " + inetSocketAddress); // We found a host to connect to, return here this.host = host; - this.port = port; + this.port = portUint16; return; } - failedAddresses.add(hostAddress); } else { + // TODO: Move this into the inner-loop above. There appears no reason why we should not try a proxy + // connection to every inet address of each connection endpoint. socket = socketFactory.createSocket(); - StringUtils.requireNotNullOrEmpty(host, "Host of HostAddress " + hostAddress + " must not be null when using a Proxy"); + StringUtils.requireNotNullNorEmpty(host, "Host of endpoint " + endpoint + " must not be null when using a Proxy"); final String hostAndPort = host + " at port " + port; LOGGER.finer("Trying to establish TCP connection via Proxy to " + hostAndPort); try { proxyInfo.getProxySocketConnection().connect(socket, host, port, timeout); } catch (IOException e) { - hostAddress.setException(e); - failedAddresses.add(hostAddress); + CloseableUtil.maybeClose(socket, LOGGER); + RemoteConnectionException rce = new RemoteConnectionException<>(endpoint, null, e); + connectionExceptions.add(rce); continue; } LOGGER.finer("Established TCP connection to " + hostAndPort); // We found a host to connect to, return here this.host = host; - this.port = port; + this.port = portUint16; return; } } + // There are no more host addresses to try // throw an exception and report all tried // HostAddresses in the exception - throw ConnectionException.from(failedAddresses); + throw EndpointConnectionException.from(result.lookupFailures, connectionExceptions); } /** @@ -690,8 +659,8 @@ private void connectUsingConfiguration() throws ConnectionException, IOException * * @throws XMPPException if establishing a connection to the server fails. * @throws SmackException if the server fails to respond back or if there is anther error. - * @throws IOException - * @throws InterruptedException + * @throws IOException if an I/O error occurred. + * @throws InterruptedException if the calling thread was interrupted. */ private void initConnection() throws IOException, InterruptedException { compressionHandler = null; @@ -734,129 +703,23 @@ private void initReaderAndWriter() throws IOException { * The server has indicated that TLS negotiation can start. We now need to secure the * existing plain connection and perform a handshake. This method won't return until the * connection has finished the handshake or an error occurred while securing the connection. - * @throws IOException + * @throws IOException if an I/O error occurred. * @throws CertificateException - * @throws NoSuchAlgorithmException + * @throws NoSuchAlgorithmException if no such algorithm is available. * @throws NoSuchProviderException * @throws KeyStoreException * @throws UnrecoverableKeyException - * @throws KeyManagementException - * @throws SmackException + * @throws KeyManagementException if there was a key mangement error. + * @throws SmackException if Smack detected an exceptional situation. * @throws Exception if an exception occurs. */ @SuppressWarnings("LiteralClassName") private void proceedTLSReceived() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException, NoSuchProviderException, UnrecoverableKeyException, KeyManagementException, SmackException { - SmackDaneVerifier daneVerifier = null; - - if (config.getDnssecMode() == DnssecMode.needsDnssecAndDane) { - SmackDaneProvider daneProvider = DNSUtil.getDaneProvider(); - if (daneProvider == null) { - throw new UnsupportedOperationException("DANE enabled but no SmackDaneProvider configured"); - } - daneVerifier = daneProvider.newInstance(); - if (daneVerifier == null) { - throw new IllegalStateException("DANE requested but DANE provider did not return a DANE verifier"); - } - } - - SSLContext context = this.config.getCustomSSLContext(); - KeyStore ks = null; - PasswordCallback pcb = null; - - if (context == null) { - final String keyStoreType = config.getKeystoreType(); - final CallbackHandler callbackHandler = config.getCallbackHandler(); - final String keystorePath = config.getKeystorePath(); - if ("PKCS11".equals(keyStoreType)) { - try { - Constructor c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class); - String pkcs11Config = "name = SmartCard\nlibrary = " + config.getPKCS11Library(); - ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes(StringUtils.UTF8)); - Provider p = (Provider) c.newInstance(config); - Security.addProvider(p); - ks = KeyStore.getInstance("PKCS11",p); - pcb = new PasswordCallback("PKCS11 Password: ",false); - callbackHandler.handle(new Callback[] {pcb}); - ks.load(null,pcb.getPassword()); - } - catch (Exception e) { - LOGGER.log(Level.WARNING, "Exception", e); - ks = null; - } - } - else if ("Apple".equals(keyStoreType)) { - ks = KeyStore.getInstance("KeychainStore","Apple"); - ks.load(null,null); - // pcb = new PasswordCallback("Apple Keychain",false); - // pcb.setPassword(null); - } - else if (keyStoreType != null) { - ks = KeyStore.getInstance(keyStoreType); - if (callbackHandler != null && StringUtils.isNotEmpty(keystorePath)) { - try { - pcb = new PasswordCallback("Keystore Password: ", false); - callbackHandler.handle(new Callback[] { pcb }); - ks.load(new FileInputStream(keystorePath), pcb.getPassword()); - } - catch (Exception e) { - LOGGER.log(Level.WARNING, "Exception", e); - ks = null; - } - } else { - ks.load(null, null); - } - } - - KeyManager[] kms = null; - - if (ks != null) { - String keyManagerFactoryAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); - KeyManagerFactory kmf = null; - try { - kmf = KeyManagerFactory.getInstance(keyManagerFactoryAlgorithm); - } - catch (NoSuchAlgorithmException e) { - LOGGER.log(Level.FINE, "Could get the default KeyManagerFactory for the '" - + keyManagerFactoryAlgorithm + "' algorithm", e); - } - if (kmf != null) { - try { - if (pcb == null) { - kmf.init(ks, null); - } - else { - kmf.init(ks, pcb.getPassword()); - pcb.clearPassword(); - } - kms = kmf.getKeyManagers(); - } - catch (NullPointerException npe) { - LOGGER.log(Level.WARNING, "NullPointerException", npe); - } - } - } - - // If the user didn't specify a SSLContext, use the default one - context = SSLContext.getInstance("TLS"); - - final SecureRandom secureRandom = new java.security.SecureRandom(); - X509TrustManager customTrustManager = config.getCustomX509TrustManager(); - - if (daneVerifier != null) { - // User requested DANE verification. - daneVerifier.init(context, kms, customTrustManager, secureRandom); - } else { - TrustManager[] customTrustManagers = null; - if (customTrustManager != null) { - customTrustManagers = new TrustManager[] { customTrustManager }; - } - context.init(kms, customTrustManagers, secureRandom); - } - } + SmackTlsContext smackTlsContext = getSmackTlsContext(); Socket plain = socket; // Secure the plain connection - socket = context.getSocketFactory().createSocket(plain, + socket = smackTlsContext.sslContext.getSocketFactory().createSocket(plain, config.getXMPPServiceDomain().toString(), plain.getPort(), true); final SSLSocket sslSocket = (SSLSocket) socket; @@ -871,8 +734,8 @@ else if (keyStoreType != null) { // Proceed to do the handshake sslSocket.startHandshake(); - if (daneVerifier != null) { - daneVerifier.finish(sslSocket); + if (smackTlsContext.daneVerifier != null) { + smackTlsContext.daneVerifier.finish(sslSocket.getSession()); } final HostnameVerifier verifier = getConfiguration().getHostnameVerifier(); @@ -942,10 +805,10 @@ public boolean isUsingCompression() { * before authentication took place. *

* - * @throws NotConnectedException - * @throws SmackException - * @throws NoResponseException - * @throws InterruptedException + * @throws NotConnectedException if the XMPP connection is not connected. + * @throws SmackException if Smack detected an exceptional situation. + * @throws NoResponseException if there was no response from the remote entity. + * @throws InterruptedException if the calling thread was interrupted. */ private void maybeEnableCompression() throws SmackException, InterruptedException { if (!config.isCompressionEnabled()) { @@ -975,13 +838,12 @@ private void maybeEnableCompression() throws SmackException, InterruptedExceptio *

* * @throws XMPPException if an error occurs while trying to establish the connection. - * @throws SmackException - * @throws IOException - * @throws InterruptedException + * @throws SmackException if Smack detected an exceptional situation. + * @throws IOException if an I/O error occurred. + * @throws InterruptedException if the calling thread was interrupted. */ @Override protected void connectInternal() throws SmackException, IOException, XMPPException, InterruptedException { - closingStreamReceived.init(); // Establishes the TCP connection to the server and does setup the reader and writer. Throws an exception if // there is an error establishing the connection connectUsingConfiguration(); @@ -996,54 +858,10 @@ protected void connectInternal() throws SmackException, IOException, XMPPExcepti saslFeatureReceived.checkIfSuccessOrWaitOrThrow(); } - /** - * Sends out a notification that there was an error with the connection - * and closes the connection. Also prints the stack trace of the given exception - * - * @param e the exception that causes the connection close event. - */ - private void notifyConnectionError(final Exception e) { - ASYNC_BUT_ORDERED.performAsyncButOrdered(this, new Runnable() { - @Override - public void run() { - // Listeners were already notified of the exception, return right here. - if (packetReader.done || packetWriter.done()) return; - - // Report the failure outside the synchronized block, so that a thread waiting within a synchronized - // function like connect() throws the wrapped exception. - SmackWrappedException smackWrappedException = new SmackWrappedException(e); - tlsHandled.reportGenericFailure(smackWrappedException); - saslFeatureReceived.reportGenericFailure(smackWrappedException); - maybeCompressFeaturesReceived.reportGenericFailure(smackWrappedException); - lastFeaturesReceived.reportGenericFailure(smackWrappedException); - - synchronized (XMPPTCPConnection.this) { - // Within this synchronized block, either *both* reader and writer threads must be terminated, or - // none. - assert ((packetReader.done && packetWriter.done()) - || (!packetReader.done && !packetWriter.done())); - - // Closes the connection temporary. A reconnection is possible - // Note that a connection listener of XMPPTCPConnection will drop the SM state in - // case the Exception is a StreamErrorException. - instantShutdown(); - } - - Async.go(new Runnable() { - @Override - public void run() { - // Notify connection listeners of the error. - callConnectionClosedOnErrorListener(e); - } - }, XMPPTCPConnection.this + " callConnectionClosedOnErrorListener()"); - } - }); - } - /** * For unit testing purposes * - * @param writer + * @param writer TODO javadoc me please */ protected void setWriter(Writer writer) { this.writer = writer; @@ -1068,7 +886,7 @@ protected void afterFeaturesReceived() throws NotConnectedException, Interrupted tlsHandled.reportSuccess(); } - if (getSASLAuthentication().authenticationSuccessful()) { + if (isSaslAuthenticated()) { // If we have received features after the SASL has been successfully completed, then we // have also *maybe* received, as it is an optional feature, the compression feature // from the server. @@ -1076,33 +894,17 @@ protected void afterFeaturesReceived() throws NotConnectedException, Interrupted } } - /** - * Resets the parser using the latest connection's reader. Resetting the parser is necessary - * when the plain connection has been secured or when a new opening stream element is going - * to be sent by the server. - * - * @throws SmackException if the parser could not be reset. - * @throws InterruptedException - */ - void openStream() throws SmackException, InterruptedException { - // If possible, provide the receiving entity of the stream open tag, i.e. the server, as much information as - // possible. The 'to' attribute is *always* available. The 'from' attribute if set by the user and no external - // mechanism is used to determine the local entity (user). And the 'id' attribute is available after the first - // response from the server (see e.g. RFC 6120 ยง 9.1.1 Step 2.) - CharSequence to = getXMPPServiceDomain(); - CharSequence from = null; - CharSequence localpart = config.getUsername(); - if (localpart != null) { - from = XmppStringUtils.completeJidFrom(localpart, to); - } - String id = getStreamId(); - sendNonza(new StreamOpen(to, from, id)); + private void resetParser() throws IOException { try { - packetReader.parser = PacketParserUtils.newXmppParser(reader); + packetReader.parser = SmackXmlParser.newXmlParser(reader); + } catch (XmlPullParserException e) { + throw new IOException(e); } - catch (XmlPullParserException e) { - throw new SmackException(e); } + + private void openStreamAndResetParser() throws IOException, NotConnectedException, InterruptedException { + sendStreamOpen(); + resetParser(); } protected class PacketReader { @@ -1145,12 +947,14 @@ void shutdown() { * Parse top-level packets in order to process them further. */ private void parsePackets() { + boolean initialStreamOpenSend = false; try { - initialOpenStreamSend.checkIfSuccessOrWait(); - int eventType = parser.getEventType(); + openStreamAndResetParser(); + initialStreamOpenSend = true; + XmlPullParser.Event eventType = parser.getEventType(); while (!done) { switch (eventType) { - case XmlPullParser.START_TAG: + case START_ELEMENT: final String name = parser.getName(); switch (name) { case Message.ELEMENT: @@ -1163,12 +967,7 @@ private void parsePackets() { } break; case "stream": - // We found an opening stream. - if ("jabber:client".equals(parser.getNamespace(null))) { - streamId = parser.getAttributeValue("", "id"); - String reportedServerDomain = parser.getAttributeValue("", "from"); - assert (config.getXMPPServiceDomain().equals(reportedServerDomain)); - } + onStreamOpen(parser); break; case "error": StreamError streamError = PacketParserUtils.parseStreamError(parser); @@ -1179,17 +978,17 @@ private void parsePackets() { tlsHandled.reportSuccess(); throw new StreamErrorException(streamError); case "features": - parseFeatures(parser); + parseFeaturesAndNotify(parser); break; case "proceed": try { // Secure the connection by negotiating TLS proceedTLSReceived(); // Send a new opening stream to the server - openStream(); + openStreamAndResetParser(); } catch (Exception e) { - SmackException smackException = new SmackException(e); + SmackException.SmackWrappedException smackException = new SmackException.SmackWrappedException(e); tlsHandled.reportFailure(smackException); throw e; } @@ -1200,44 +999,26 @@ private void parsePackets() { case "urn:ietf:params:xml:ns:xmpp-tls": // TLS negotiation has failed. The server will close the connection // TODO Parse failure stanza - throw new SmackException("TLS negotiation has failed"); + throw new SmackException.SmackMessageException("TLS negotiation has failed"); case "http://jabber.org/protocol/compress": // Stream compression has been denied. This is a recoverable // situation. It is still possible to authenticate and // use the connection but using an uncompressed connection // TODO Parse failure stanza - compressSyncPoint.reportFailure(new SmackException( + compressSyncPoint.reportFailure(new SmackException.SmackMessageException( "Could not establish compression")); break; - case SaslStreamElements.NAMESPACE: - // SASL authentication has failed. The server may close the connection - // depending on the number of retries - final SASLFailure failure = PacketParserUtils.parseSASLFailure(parser); - getSASLAuthentication().authenticationFailed(failure); - break; + default: + parseAndProcessNonza(parser); } break; - case Challenge.ELEMENT: - // The server is challenging the SASL authentication made by the client - String challengeData = parser.nextText(); - getSASLAuthentication().challengeReceived(challengeData); - break; - case Success.ELEMENT: - Success success = new Success(parser.nextText()); - // We now need to bind a resource for the connection - // Open a new stream and wait for the response - openStream(); - // The SASL authentication with the server was successful. The next step - // will be to bind the resource - getSASLAuthentication().authenticated(success); - break; case Compressed.ELEMENT: // Server confirmed that it's possible to use stream compression. Start // stream compression // Initialize the reader and writer with the new compressed version initReaderAndWriter(); // Send a new opening stream to the server - openStream(); + openStreamAndResetParser(); // Notify that compression is being used compressSyncPoint.reportSuccess(); break; @@ -1246,7 +1027,7 @@ private void parsePackets() { if (enabled.isResumeSet()) { smSessionId = enabled.getId(); if (StringUtils.isNullOrEmpty(smSessionId)) { - SmackException xmppException = new SmackException("Stream Management 'enabled' element with resume attribute but without session id received"); + SmackException xmppException = new SmackException.SmackMessageException("Stream Management 'enabled' element with resume attribute but without session id received"); smEnabledSyncPoint.reportFailure(xmppException); throw xmppException; } @@ -1276,7 +1057,7 @@ private void parsePackets() { if (!smEnabledSyncPoint.requestSent()) { throw new IllegalStateException("Failed element received but SM was not previously enabled"); } - smEnabledSyncPoint.reportFailure(new SmackException(xmppException)); + smEnabledSyncPoint.reportFailure(new SmackException.SmackWrappedException(xmppException)); // Report success for last lastFeaturesReceived so that in case a // failed resumption, we can continue with normal resource binding. // See text of XEP-198 5. below Example 11. @@ -1328,12 +1109,12 @@ private void parsePackets() { LOGGER.warning("SM Ack Request received while SM is not enabled"); } break; - default: - LOGGER.warning("Unknown top level stream element: " + name); - break; + default: + parseAndProcessNonza(parser); + break; } break; - case XmlPullParser.END_TAG: + case END_ELEMENT: final String endTagName = parser.getName(); if ("stream".equals(endTagName)) { if (!parser.getNamespace().equals("http://etherx.jabber.org/streams")) { @@ -1369,20 +1150,25 @@ public void run() { } } break; - case XmlPullParser.END_DOCUMENT: + case END_DOCUMENT: // END_DOCUMENT only happens in an error case, as otherwise we would see a // closing stream element before. - throw new SmackException( + throw new SmackException.SmackMessageException( "Parser got END_DOCUMENT event. This could happen e.g. if the server closed the connection without sending a closing stream element"); + default: + // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement. + break; } eventType = parser.next(); } } catch (Exception e) { + // TODO: Move the call closingStreamReceived.reportFailure(e) into notifyConnectionError? closingStreamReceived.reportFailure(e); // The exception can be ignored if the the connection is 'done' - // or if the it was caused because the socket got closed - if (!(done || packetWriter.queue.isShutdown())) { + // or if the it was caused because the socket got closed. It can not be ignored if it + // happened before (or while) the initial stream opened was send. + if (!(done || packetWriter.queue.isShutdown()) || !initialStreamOpenSend) { // Close the connection and notify connection listeners of the // error. notifyConnectionError(e); @@ -1393,6 +1179,8 @@ public void run() { protected class PacketWriter { public static final int QUEUE_SIZE = XMPPTCPConnection.QUEUE_SIZE; + public static final int UNACKKNOWLEDGED_STANZAS_QUEUE_SIZE = 1024; + public static final int UNACKKNOWLEDGED_STANZAS_QUEUE_SIZE_HIGH_WATER_MARK = (int) (0.3 * UNACKKNOWLEDGED_STANZAS_QUEUE_SIZE); private final String threadName = "Smack Writer (" + getConnectionCounter() + ')'; @@ -1416,7 +1204,7 @@ protected class PacketWriter { * True if some preconditions are given to start the bundle and defer mechanism. *

* This will likely get set to true right after the start of the writer thread, because - * {@link #nextStreamElement()} will check if {@link #queue} is empty, which is probably the case, and then set + * {@link #nextStreamElement()} will check if {@link queue} is empty, which is probably the case, and then set * this field to true. *

*/ @@ -1472,8 +1260,8 @@ protected void throwNotConnectedExceptionIfDoneAndResumptionNotPossible() throws * Sends the specified element to the server. * * @param element the element to send. - * @throws NotConnectedException - * @throws InterruptedException + * @throws NotConnectedException if the XMPP connection is not connected. + * @throws InterruptedException if the calling thread was interrupted. */ protected void sendStreamElement(Element element) throws NotConnectedException, InterruptedException { throwNotConnectedExceptionIfDoneAndResumptionNotPossible(); @@ -1494,7 +1282,7 @@ protected void sendStreamElement(Element element) throws NotConnectedException, /** * Shuts down the stanza writer. Once this method has been called, no further * packets will be written to the server. - * @throws InterruptedException + * @throws InterruptedException if the calling thread was interrupted. */ void shutdown(boolean instant) { instantShutdown = instant; @@ -1537,8 +1325,6 @@ private Element nextStreamElement() { private void writePackets() { Exception writerException = null; try { - openStream(); - initialOpenStreamSend.reportSuccess(); // Write out packets from the queue. while (!done()) { Element element = nextStreamElement(); @@ -1580,13 +1366,13 @@ else if (element instanceof Enable) { // The client needs to add messages to the unacknowledged stanzas queue // right after it sent 'enabled'. Stanza will be added once // unacknowledgedStanzas is not null. - unacknowledgedStanzas = new ArrayBlockingQueue<>(QUEUE_SIZE); + unacknowledgedStanzas = new ArrayBlockingQueue<>(UNACKKNOWLEDGED_STANZAS_QUEUE_SIZE); } maybeAddToUnacknowledgedStanzas(packet); - CharSequence elementXml = element.toXML(StreamOpen.CLIENT_NAMESPACE); + CharSequence elementXml = element.toXML(outgoingStreamXmlEnvironment); if (elementXml instanceof XmlStringBuilder) { - ((XmlStringBuilder) elementXml).write(writer, StreamOpen.CLIENT_NAMESPACE); + ((XmlStringBuilder) elementXml).write(writer, outgoingStreamXmlEnvironment); } else { writer.write(elementXml.toString()); @@ -1608,7 +1394,7 @@ else if (element instanceof Enable) { Stanza stanza = (Stanza) packet; maybeAddToUnacknowledgedStanzas(stanza); } - writer.write(packet.toXML(null).toString()); + writer.write(packet.toXML().toString()); } writer.flush(); } @@ -1682,12 +1468,12 @@ private void maybeAddToUnacknowledgedStanzas(Stanza stanza) throws IOException { // packet order is not stable at this point (sendStanzaInternal() can be // called concurrently). if (unacknowledgedStanzas != null && stanza != null) { - // If the unacknowledgedStanza queue is nearly full, request an new ack + // If the unacknowledgedStanza queue reaching its high water mark, request an new ack // from the server in order to drain it - if (unacknowledgedStanzas.size() == 0.8 * XMPPTCPConnection.QUEUE_SIZE) { - writer.write(AckRequest.INSTANCE.toXML(null).toString()); - writer.flush(); + if (unacknowledgedStanzas.size() == UNACKKNOWLEDGED_STANZAS_QUEUE_SIZE_HIGH_WATER_MARK) { + writer.write(AckRequest.INSTANCE.toXML().toString()); } + try { // It is important the we put the stanza in the unacknowledged stanza // queue before we put it on the wire @@ -1807,7 +1593,7 @@ public void removeAllRequestAckPredicates() { * * @throws StreamManagementNotEnabledException if Stream Management is not enabled. * @throws NotConnectedException if the connection is not connected. - * @throws InterruptedException + * @throws InterruptedException if the calling thread was interrupted. */ public void requestSmAcknowledgement() throws StreamManagementNotEnabledException, NotConnectedException, InterruptedException { if (!isSmEnabled()) { @@ -1830,7 +1616,7 @@ private void requestSmAcknowledgementInternal() throws NotConnectedException, In * * @throws StreamManagementNotEnabledException if Stream Management is not enabled. * @throws NotConnectedException if the connection is not connected. - * @throws InterruptedException + * @throws InterruptedException if the calling thread was interrupted. */ public void sendSmAcknowledgement() throws StreamManagementNotEnabledException, NotConnectedException, InterruptedException { if (!isSmEnabled()) { @@ -2107,7 +1893,7 @@ public void run() { /** * Set the default bundle and defer callback used for new connections. * - * @param defaultBundleAndDeferCallback + * @param defaultBundleAndDeferCallback TODO javadoc me please * @see BundleAndDeferCallback * @since 4.1 */ diff --git a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java index c2ef3afce..4dd16953a 100644 --- a/app/src/main/java/org/kontalk/crypto/OmemoCoder.java +++ b/app/src/main/java/org/kontalk/crypto/OmemoCoder.java @@ -18,29 +18,30 @@ package org.kontalk.crypto; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.GeneralSecurityException; -import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; -import org.jivesoftware.smackx.delay.packet.DelayInformation; -import org.jivesoftware.smackx.omemo.OmemoFingerprint; +import org.jivesoftware.smack.packet.MessageBuilder; +import org.jivesoftware.smackx.omemo.OmemoMessage; +import org.jivesoftware.smackx.omemo.exceptions.CorruptedOmemoKeyException; +import org.jivesoftware.smackx.omemo.trust.OmemoFingerprint; import org.jivesoftware.smackx.omemo.OmemoManager; -import org.jivesoftware.smackx.omemo.element.OmemoElement; import org.jivesoftware.smackx.omemo.exceptions.CannotEstablishOmemoSessionException; -import org.jivesoftware.smackx.omemo.exceptions.CryptoFailedException; import org.jivesoftware.smackx.omemo.exceptions.UndecidedOmemoIdentityException; -import org.jivesoftware.smackx.omemo.internal.ClearTextMessage; import org.jivesoftware.smackx.omemo.internal.OmemoDevice; -import org.jivesoftware.smackx.omemo.util.OmemoConstants; +import org.jivesoftware.smackx.pubsub.PubSubException; import org.jivesoftware.smackx.pubsub.packet.PubSub; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; @@ -61,8 +62,13 @@ public class OmemoCoder extends Coder { private OmemoManager mManager; /** For encryption. */ - public OmemoCoder(XMPPConnection connection, TrustedRecipient[] recipients) throws XMPPException.XMPPErrorException, - SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException { + public OmemoCoder(XMPPConnection connection, TrustedRecipient[] recipients) + throws XMPPException.XMPPErrorException, SmackException.NotConnectedException, + InterruptedException, SmackException.NoResponseException, + SmackException.NotLoggedInException, CorruptedOmemoKeyException, + IOException, CannotEstablishOmemoSessionException, + PubSubException.NotALeafNodeException { + init(connection); mSender = null; @@ -128,56 +134,49 @@ public int getSupportedFlags() { @Override public Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException { Message output; - ArrayList recipients = new ArrayList(mRecipients.length); + Set recipients = new HashSet<>(mRecipients.length); for (TrustedRecipient rcpt : mRecipients) { recipients.add(rcpt.jid.asBareJid()); } + OmemoMessage.Sent sentMessage; try { - output = mManager.encrypt(recipients, message.getBody()); + sentMessage = mManager.encrypt(recipients, message.getBody()); } catch (UndecidedOmemoIdentityException e) { // TODO experimenting; crash for now throw new RuntimeException("Impossible: we should have decided already!", e); } - catch (CannotEstablishOmemoSessionException e) { - try { - output = mManager.encryptForExistingSessions(e, message.getBody()); - } - catch (UndecidedOmemoIdentityException e1) { - // TODO experimenting; crash for now - throw new RuntimeException("Impossible: we should have decided already!", e1); - } - catch (CryptoFailedException e1) { - throw new GeneralSecurityException(e1); - } - } catch (Exception e) { throw new GeneralSecurityException(e); } - if (output != null) { - output.setBody(placeholder); - output.setStanzaId(message.getStanzaId()); - output.setFrom(message.getFrom()); - output.setTo(message.getTo()); - output.setType(message.getType()); - output.addExtensions(message.getExtensions()); + if (sentMessage.isMissingRecipients()) { + // some devices were skipped for some reason + // TODO experimenting; crash for now + throw new RuntimeException("some recipients were missed", + sentMessage.getSkippedDevices().values().iterator().next()); + } + else { + return sentMessage.buildMessage(MessageBuilder + .buildMessage(message.getStanzaId()) + .ofType(message.getType()) + .addExtensions(message.getExtensions()), message.getTo()); } - - return output; } /** - * For now just here to fool {@link org.jivesoftware.smack.MessageListener}. + * For now just here to fool {@link org.kontalk.service.msgcenter.MessageListener}. */ @Override public DecryptOutput decryptMessage(Message message, boolean verify) throws GeneralSecurityException { + // TODO we need to understand how this is working + /* if (message.hasExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE)) { // offline message - decrypt manually ClearTextMessage cleartext; try { - cleartext = mManager.decrypt(mSender, message); + cleartext = mManager.decrypt(mSender, message.getExtension(OmemoElement.NAME_ENCRYPTED, ...)); } catch (Exception e) { throw new GeneralSecurityException("OMEMO decryption failed", e); @@ -200,9 +199,10 @@ public DecryptOutput decryptMessage(Message message, boolean verify) throws Gene return new DecryptOutput(output, "text/plain", new Date(), SECURITY_ADVANCED, Collections.emptyList()); } else { + */ // online message - already decrypted by smack-omemo return new DecryptOutput(message, "text/plain", new Date(), SECURITY_ADVANCED, Collections.emptyList()); - } + //} } @Override diff --git a/app/src/main/java/org/kontalk/crypto/PGPCoder.java b/app/src/main/java/org/kontalk/crypto/PGPCoder.java index 77c1cae19..1c0e49792 100644 --- a/app/src/main/java/org/kontalk/crypto/PGPCoder.java +++ b/app/src/main/java/org/kontalk/crypto/PGPCoder.java @@ -155,7 +155,7 @@ private byte[] encryptStanza(CharSequence xml) throws GeneralSecurityException { @Override public Message encryptMessage(Message message, String placeholder) throws GeneralSecurityException { - byte[] toMessage = encryptStanza(message.toXML(null)); + byte[] toMessage = encryptStanza(message.toXML()); org.jivesoftware.smack.packet.Message encMsg = new org.jivesoftware.smack.packet.Message(message.getTo(), message.getType()); diff --git a/app/src/main/java/org/kontalk/service/msgcenter/LastActivityListener.java b/app/src/main/java/org/kontalk/service/msgcenter/LastActivityListener.java index ae59b0ea0..3e0c449aa 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/LastActivityListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/LastActivityListener.java @@ -45,9 +45,9 @@ public void processStanza(Stanza packet) { public void processException(Exception exception) { if (exception instanceof XMPPException.XMPPErrorException) { String id = ((XMPPException.XMPPErrorException) exception) - .getStanzaError().getStanza().getStanzaId(); + .getRequest().getStanzaId(); Jid jid = ((XMPPException.XMPPErrorException) exception) - .getStanzaError().getStanza().getFrom(); + .getRequest().getFrom(); MessageCenterService.bus() .post(new LastActivityEvent(exception, jid, id)); } diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java index a0ad0d67f..553c9c151 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageCenterService.java @@ -982,7 +982,7 @@ private synchronized void quit(boolean restarting) { } if (mOmemoManager != null) { - mOmemoManager.shutdown(); + mOmemoManager.stopStanzaAndPEPListeners(); } // clear the connection only if we are quitting @@ -1760,9 +1760,10 @@ public boolean accept(Stanza stanza) { // ignore OMEMO messages, they will be processed by smack-omemo // except delayed messages which must be processed manually via decrypt() // .......ARGH!!! - return stanza instanceof org.jivesoftware.smack.packet.Message && + // FIXME is this still applicable to 4.4.0? + return stanza instanceof org.jivesoftware.smack.packet.Message/* && (!OmemoManager.stanzaContainsOmemoElement(stanza) || - stanza.hasExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE)); + stanza.hasExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE))*/; } }; connection.addSyncStanzaListener(new MessageListener(this), filter); @@ -1873,6 +1874,7 @@ public void run() { if (supported) { // we logged in so we can now initialize OMEMO mOmemoManager = OmemoManager.getInstanceFor(connection); + mOmemoManager.resumeStanzaAndPEPListeners(); try { mOmemoManager.initialize(); } diff --git a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java index 14d073676..b6d88e0b9 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/MessageListener.java @@ -28,13 +28,13 @@ import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smackx.carbons.packet.CarbonExtension; import org.jivesoftware.smackx.chatstates.ChatState; import org.jivesoftware.smackx.chatstates.packet.ChatStateExtension; import org.jivesoftware.smackx.forward.packet.Forwarded; import org.jivesoftware.smackx.omemo.OmemoManager; +import org.jivesoftware.smackx.omemo.OmemoMessage; import org.jivesoftware.smackx.omemo.element.OmemoElement; -import org.jivesoftware.smackx.omemo.internal.CipherAndAuthTag; -import org.jivesoftware.smackx.omemo.internal.OmemoMessageInformation; import org.jivesoftware.smackx.omemo.listener.OmemoMessageListener; import org.jivesoftware.smackx.omemo.util.OmemoConstants; import org.jivesoftware.smackx.receipts.DeliveryReceipt; @@ -265,10 +265,12 @@ private ChatStateEvent processChatState(Message m) { * Process an incoming OMEMO message. */ @Override - public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessage, Message wrappingMessage, OmemoMessageInformation omemoInformation) { + public void onOmemoMessageReceived(Stanza stanza, OmemoMessage.Received decryptedMessage) { // duplicates the message to fool real processing - Message output = new Message(encryptedMessage); - output.setBody(decryptedBody); + Message output = ((Message) stanza).asBuilder() + // TODO ignoring other decrypted message information + .setBody(decryptedMessage.getBody()) + .build(); try { processWakefulStanza(output); @@ -278,8 +280,8 @@ public void onOmemoMessageReceived(String decryptedBody, Message encryptedMessag } @Override - public void onOmemoKeyTransportReceived(CipherAndAuthTag cipherAndAuthTag, Message message, Message wrappingMessage, OmemoMessageInformation omemoInformation) { - // not used for now. + public void onOmemoCarbonCopyReceived(CarbonExtension.Direction direction, Message carbonCopy, Message wrappingMessage, OmemoMessage.Received decryptedCarbonCopy) { + // TODO will be used one day } /** @@ -348,7 +350,7 @@ private ChatStateEvent processChatMessage(Message m, @Nullable ChatStateEvent ch securityFlags = Coder.SECURITY_BASIC; } - else if (m.hasExtension(OmemoElement.ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL)) { + else if (m.hasExtension(OmemoElement.NAME_ENCRYPTED, OmemoConstants.OMEMO_NAMESPACE_V_AXOLOTL)) { securityFlags = Coder.SECURITY_ADVANCED; } @@ -431,7 +433,7 @@ else if (m.hasExtension(OpenPGPSignedMessage.ELEMENT_NAME, OpenPGPSignedMessage. // raw component for encrypted data // reuse security flags msg.clearComponents(); - msg.addComponent(new RawComponent(m.toXML(null).toString().getBytes(), true, msg.getSecurityFlags())); + msg.addComponent(new RawComponent(m.toXML().toString().getBytes(), true, msg.getSecurityFlags())); // and body placeholder if (body != null) msg.addComponent(new TextComponent(body)); diff --git a/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java b/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java index c4cd1a0f1..d441a9691 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/PresenceListener.java @@ -242,7 +242,7 @@ public static PresenceEvent createEvent(Context ctx, Presence p, RosterEntry ent String jid = p.getFrom().asBareJid().toString(); Date delayTime; - DelayInformation delay = p.getExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE); + DelayInformation delay = DelayInformation.from(p); if (delay != null) { delayTime = delay.getStamp(); } @@ -305,7 +305,7 @@ int updateUsersDatabase(Presence p) { // delay long timestamp; - DelayInformation delay = p.getExtension(DelayInformation.ELEMENT, DelayInformation.NAMESPACE); + DelayInformation delay = DelayInformation.from(p); if (delay != null) { // delay from presence (rare) timestamp = delay.getStamp().getTime(); @@ -319,9 +319,9 @@ int updateUsersDatabase(Presence p) { values.put(Users.LAST_SEEN, timestamp); // public key extension (for fingerprint) - PublicKeyPresence pkey = p.getExtension(PublicKeyPresence.ELEMENT_NAME, PublicKeyPresence.NAMESPACE); - if (pkey != null) { - String fingerprint = pkey.getFingerprint(); + ExtensionElement pkey = p.getExtension(PublicKeyPresence.ELEMENT_NAME, PublicKeyPresence.NAMESPACE); + if (pkey instanceof PublicKeyPresence) { + String fingerprint = ((PublicKeyPresence) pkey).getFingerprint(); if (fingerprint != null) { // insert new key with empty key data Keyring.setKey(getContext(), jid, fingerprint, new Date()); diff --git a/app/src/main/java/org/kontalk/service/msgcenter/PrivateKeyUploadListener.java b/app/src/main/java/org/kontalk/service/msgcenter/PrivateKeyUploadListener.java index fec16939c..42292fc16 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/PrivateKeyUploadListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/PrivateKeyUploadListener.java @@ -86,7 +86,7 @@ public void processStanza(Stanza packet) { return; } - DataForm response = iq.getExtension("x", "jabber:x:data"); + DataForm response = DataForm.from(iq); if (response == null) { finish(StanzaError.Condition.internal_server_error); return; @@ -133,16 +133,14 @@ private Stanza prepareKeyPacket() { Form form = new Form(DataForm.Type.submit); // form type: register#privatekey - FormField type = new FormField("FORM_TYPE"); - type.setType(FormField.Type.hidden); - type.addValue("http://kontalk.org/protocol/register#privatekey"); - form.addField(type); + form.addField(FormField.hiddenFormType("http://kontalk.org/protocol/register#privatekey")); // private key - FormField fieldKey = new FormField("privatekey"); - fieldKey.setLabel("Private key"); - fieldKey.setType(FormField.Type.text_single); - fieldKey.addValue(privatekey); + FormField fieldKey = FormField.builder("privatekey") + .setLabel("Private key") + .setType(FormField.Type.text_single) + .addValue(privatekey) + .build(); form.addField(fieldKey); iq.addExtension(form.getDataFormToSend()); diff --git a/app/src/main/java/org/kontalk/service/msgcenter/RegisterKeyPairListener.java b/app/src/main/java/org/kontalk/service/msgcenter/RegisterKeyPairListener.java index 44125d631..fb7f96472 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/RegisterKeyPairListener.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/RegisterKeyPairListener.java @@ -97,26 +97,25 @@ private Stanza prepareKeyPacket() { Form form = new Form(DataForm.Type.submit); // form type: register#key - FormField type = new FormField("FORM_TYPE"); - type.setType(FormField.Type.hidden); - type.addValue("http://kontalk.org/protocol/register#key"); - form.addField(type); + form.addField(FormField.hiddenFormType("http://kontalk.org/protocol/register#key")); // new (to-be-signed) public key - FormField fieldKey = new FormField("publickey"); - fieldKey.setLabel("Public key"); - fieldKey.setType(FormField.Type.text_single); - fieldKey.addValue(publicKey); + FormField fieldKey = FormField.builder("publickey") + .setLabel("Public key") + .setType(FormField.Type.text_single) + .addValue(publicKey) + .build(); form.addField(fieldKey); // old (revoked) public key if (mRevoked != null) { String revokedKey = Base64.encodeToString(mRevoked.getEncoded(), Base64.NO_WRAP); - FormField fieldRevoked = new FormField("revoked"); - fieldRevoked.setLabel("Revoked public key"); - fieldRevoked.setType(FormField.Type.text_single); - fieldRevoked.addValue(revokedKey); + FormField fieldRevoked = FormField.builder("revoked") + .setLabel("Revoked public key") + .setType(FormField.Type.text_single) + .addValue(revokedKey) + .build(); form.addField(fieldRevoked); } @@ -173,7 +172,7 @@ protected void revokeCurrentKey() public void processStanza(Stanza packet) { IQ iq = (IQ) packet; if (iq.getType() == IQ.Type.result) { - DataForm response = iq.getExtension("x", "jabber:x:data"); + DataForm response = DataForm.from(iq); if (response != null) { String publicKey = null; diff --git a/app/src/main/java/org/kontalk/service/msgcenter/event/PublicKeyRequest.java b/app/src/main/java/org/kontalk/service/msgcenter/event/PublicKeyRequest.java index ef07e11bc..787ccf796 100644 --- a/app/src/main/java/org/kontalk/service/msgcenter/event/PublicKeyRequest.java +++ b/app/src/main/java/org/kontalk/service/msgcenter/event/PublicKeyRequest.java @@ -18,7 +18,7 @@ package org.kontalk.service.msgcenter.event; -import org.jivesoftware.smack.packet.id.StanzaIdUtil; +import org.jivesoftware.smack.packet.id.StandardStanzaIdSource; import org.jxmpp.jid.Jid; @@ -31,7 +31,7 @@ public class PublicKeyRequest extends RequestEvent { public final Jid jid; public PublicKeyRequest(Jid jid) { - this(StanzaIdUtil.newStanzaId(), jid); + this(StandardStanzaIdSource.DEFAULT.getNewStanzaId(), jid); } /** Use null jid to request public keys for the whole roster. */ diff --git a/app/src/main/java/org/kontalk/service/registration/RegistrationService.java b/app/src/main/java/org/kontalk/service/registration/RegistrationService.java index c94a566a6..0e99c6c38 100644 --- a/app/src/main/java/org/kontalk/service/registration/RegistrationService.java +++ b/app/src/main/java/org/kontalk/service/registration/RegistrationService.java @@ -674,7 +674,7 @@ public void onVerificationRequest(VerificationRequest request) { disconnect(); } - DataForm response = result.getExtension("x", "jabber:x:data"); + DataForm response = DataForm.from(result); if (response != null && response.hasField("accept-terms")) { FormField termsUrlField = response.getField("terms"); if (termsUrlField != null) { @@ -838,7 +838,7 @@ public void onImportKeyRequest(ImportKeyRequest request) { disconnect(); } - DataForm response = result.getExtension("x", "jabber:x:data"); + DataForm response = DataForm.from(result); if (response != null && response.hasField("accept-terms")) { FormField termsUrlField = response.getField("terms"); if (termsUrlField != null) { @@ -1011,7 +1011,7 @@ public void onChallengeRequest(ChallengeRequest request) { disconnect(); } - DataForm response = result.getExtension("x", "jabber:x:data"); + DataForm response = DataForm.from(result); if (response != null) { String publicKey = null; @@ -1075,7 +1075,7 @@ private void requestRegistration() { disconnect(); } - DataForm response = result.getExtension("x", "jabber:x:data"); + DataForm response = DataForm.from(result); if (response != null) { // ok! message will be sent String smsFrom = null, challenge = null, @@ -1349,42 +1349,39 @@ private IQ createRegistrationForm(String phoneNumber, boolean acceptTerms, boole iq.setTo(mConnector.getConnection().getXMPPServiceDomain()); Form form = new Form(DataForm.Type.submit); - FormField type = new FormField("FORM_TYPE"); - type.setType(FormField.Type.hidden); - type.addValue(Registration.NAMESPACE); - form.addField(type); + form.addField(FormField.hiddenFormType(Registration.NAMESPACE)); - FormField phone = new FormField("phone"); - phone.setType(FormField.Type.text_single); - phone.addValue(phoneNumber); - form.addField(phone); + form.addField(FormField.builder("phone") + .setType(FormField.Type.text_single) + .addValue(phoneNumber) + .build()); if (acceptTerms) { - FormField fAcceptTerms = new FormField("accept-terms"); - fAcceptTerms.setType(FormField.Type.bool); - fAcceptTerms.addValue(Boolean.TRUE.toString()); - form.addField(fAcceptTerms); + form.addField(FormField.builder("accept-terms") + .setType(FormField.Type.bool) + .addValue(Boolean.TRUE.toString()) + .build()); } if (force) { - FormField fForce = new FormField("force"); - fForce.setType(FormField.Type.bool); - fForce.addValue(Boolean.TRUE.toString()); - form.addField(fForce); + form.addField(FormField.builder("force") + .setType(FormField.Type.bool) + .addValue(Boolean.TRUE.toString()) + .build()); } if (fallback) { - FormField fFallback = new FormField("fallback"); - fFallback.setType(FormField.Type.bool); - fFallback.addValue(Boolean.TRUE.toString()); - form.addField(fFallback); + form.addField(FormField.builder("fallback") + .setType(FormField.Type.bool) + .addValue(Boolean.TRUE.toString()) + .build()); } else { // not falling back, ask for our preferred challenge - FormField challenge = new FormField("challenge"); - challenge.setType(FormField.Type.text_single); - challenge.addValue(DEFAULT_CHALLENGE); - form.addField(challenge); + form.addField(FormField.builder("challenge") + .setType(FormField.Type.text_single) + .addValue(DEFAULT_CHALLENGE) + .build()); } iq.addExtension(form.getDataFormToSend()); @@ -1397,17 +1394,14 @@ private IQ createChallengeForm(CharSequence code) { iq.setTo(mConnector.getConnection().getXMPPServiceDomain()); Form form = new Form(DataForm.Type.submit); - FormField type = new FormField("FORM_TYPE"); - type.setType(FormField.Type.hidden); - type.addValue("http://kontalk.org/protocol/register#code"); - form.addField(type); + form.addField(FormField.hiddenFormType("http://kontalk.org/protocol/register#code")); if (code != null) { - FormField codeField = new FormField("code"); - codeField.setLabel("Validation code"); - codeField.setType(FormField.Type.text_single); - codeField.addValue(code.toString()); - form.addField(codeField); + form.addField(FormField.builder("code") + .setLabel("Validation code") + .setType(FormField.Type.text_single) + .addValue(code.toString()) + .build()); } iq.addExtension(form.getDataFormToSend()); diff --git a/app/src/main/java/org/kontalk/upload/HTTPFileUploadService.java b/app/src/main/java/org/kontalk/upload/HTTPFileUploadService.java index 77201a0e5..2597257f8 100644 --- a/app/src/main/java/org/kontalk/upload/HTTPFileUploadService.java +++ b/app/src/main/java/org/kontalk/upload/HTTPFileUploadService.java @@ -20,10 +20,9 @@ import java.lang.ref.WeakReference; -import org.jivesoftware.smack.SmackException; -import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.XMPPConnection; -import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.SuccessCallback; import org.jxmpp.jid.BareJid; import org.kontalk.client.HTTPFileUpload; @@ -57,23 +56,16 @@ public boolean requiresCertificate() { public void getPostUrl(String filename, long size, String mime, final UrlCallback callback) { HTTPFileUpload.Request request = new HTTPFileUpload.Request(filename, size, mime); request.setTo(mService); - try { - connection().sendIqWithResponseCallback(request, new StanzaListener() { + connection().sendIqRequestAsync(request) + .onSuccess(new SuccessCallback() { @Override - public void processStanza(Stanza packet) throws SmackException.NotConnectedException { - if (packet instanceof HTTPFileUpload.Slot) { - HTTPFileUpload.Slot slot = (HTTPFileUpload.Slot) packet; + public void onSuccess(IQ result) { + if (result instanceof HTTPFileUpload.Slot) { + HTTPFileUpload.Slot slot = (HTTPFileUpload.Slot) result; callback.callback(slot.getPutUrl(), slot.getGetUrl()); } } }); - } - catch (SmackException.NotConnectedException e) { - // ignored - } - catch (InterruptedException e) { - // ignored - } } } diff --git a/app/src/main/java/org/kontalk/util/XMPPUtils.java b/app/src/main/java/org/kontalk/util/XMPPUtils.java index da74acb0d..7112993b2 100644 --- a/app/src/main/java/org/kontalk/util/XMPPUtils.java +++ b/app/src/main/java/org/kontalk/util/XMPPUtils.java @@ -18,7 +18,6 @@ package org.kontalk.util; -import java.io.StringReader; import java.util.Collection; import java.util.Date; @@ -32,9 +31,7 @@ import org.jxmpp.jid.Jid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.util.XmppStringUtils; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlPullParserFactory; +import org.jivesoftware.smack.xml.XmlPullParser; import android.graphics.Color; import androidx.annotation.ColorInt; @@ -50,31 +47,17 @@ public class XMPPUtils { private XMPPUtils() {} - private static XmlPullParserFactory _xmlFactory; - - private static XmlPullParser getPullParser(String data) throws XmlPullParserException { - if (_xmlFactory == null) { - _xmlFactory = XmlPullParserFactory.newInstance(); - _xmlFactory.setNamespaceAware(true); - } - - XmlPullParser parser = _xmlFactory.newPullParser(); - parser.setInput(new StringReader(data)); - - return parser; - } - /** Parses a <xmpp>-wrapped message stanza. */ public static Message parseMessageStanza(String data) throws Exception { - XmlPullParser parser = getPullParser(data); + XmlPullParser parser = XMPPParserUtils.getPullParser(data); boolean done = false, in_xmpp = false; Message msg = null; while (!done) { - int eventType = parser.next(); + XmlPullParser.Event eventType = parser.next(); - if (eventType == XmlPullParser.START_TAG) { + if (eventType == XmlPullParser.Event.START_ELEMENT) { if ("xmpp".equals(parser.getName())) in_xmpp = true; @@ -84,7 +67,7 @@ else if ("message".equals(parser.getName()) && in_xmpp) { } } - else if (eventType == XmlPullParser.END_TAG) { + else if (eventType == XmlPullParser.Event.END_ELEMENT) { if ("xmpp".equals(parser.getName())) done = true; diff --git a/build.gradle b/build.gradle index fe50d8e59..b9975bb4b 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ allprojects { applicationId = 'org.kontalk' versionCode = 463 versionName = '4.4.0-beta13' - minSdkVersion = 16 + minSdkVersion = 19 targetSdkVersion = 29 compileSdkVersion = 29 smackVersion = project(':client-common-java').smackVersion