From f58b2afcaaf1977f02c590a8832bb67f77ee7be3 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 18 Jul 2015 19:38:52 +0200 Subject: [PATCH 001/166] changed switch widget --- art/md_switch_thumb_disable.svg | 156 +++++++++++++++++ art/md_switch_thumb_off_normal.svg | 153 +++++++++++++++++ art/md_switch_thumb_off_pressed.svg | 159 +++++++++++++++++ art/md_switch_thumb_on_normal.svg | 146 ++++++++++++++++ art/md_switch_thumb_on_pressed.svg | 162 ++++++++++++++++++ art/render.rb | 7 +- build.gradle | 1 + .../ui/adapter/AccountAdapter.java | 6 +- .../siacs/conversations/ui/widget/Switch.java | 70 ++++++++ .../drawable-hdpi/switch_thumb_disable.png | Bin 0 -> 1926 bytes .../drawable-hdpi/switch_thumb_off_normal.png | Bin 0 -> 1949 bytes .../switch_thumb_off_pressed.png | Bin 0 -> 3478 bytes .../drawable-hdpi/switch_thumb_on_normal.png | Bin 0 -> 1920 bytes .../drawable-hdpi/switch_thumb_on_pressed.png | Bin 0 -> 3571 bytes .../drawable-mdpi/switch_thumb_disable.png | Bin 0 -> 1239 bytes .../drawable-mdpi/switch_thumb_off_normal.png | Bin 0 -> 1267 bytes .../switch_thumb_off_pressed.png | Bin 0 -> 2233 bytes .../drawable-mdpi/switch_thumb_on_normal.png | Bin 0 -> 1256 bytes .../drawable-mdpi/switch_thumb_on_pressed.png | Bin 0 -> 2286 bytes .../drawable-xhdpi/switch_thumb_disable.png | Bin 0 -> 2762 bytes .../switch_thumb_off_normal.png | Bin 0 -> 2817 bytes .../switch_thumb_off_pressed.png | Bin 0 -> 5050 bytes .../drawable-xhdpi/switch_thumb_on_normal.png | Bin 0 -> 2789 bytes .../switch_thumb_on_pressed.png | Bin 0 -> 5328 bytes .../drawable-xxhdpi/switch_thumb_disable.png | Bin 0 -> 4539 bytes .../switch_thumb_off_normal.png | Bin 0 -> 4602 bytes .../switch_thumb_off_pressed.png | Bin 0 -> 8315 bytes .../switch_thumb_on_normal.png | Bin 0 -> 4557 bytes .../switch_thumb_on_pressed.png | Bin 0 -> 8899 bytes .../drawable-xxxhdpi/switch_thumb_disable.png | Bin 0 -> 6854 bytes .../switch_thumb_off_normal.png | Bin 0 -> 7000 bytes .../switch_thumb_off_pressed.png | Bin 0 -> 11984 bytes .../switch_thumb_on_normal.png | Bin 0 -> 7037 bytes .../switch_thumb_on_pressed.png | Bin 0 -> 12753 bytes src/main/res/drawable/switch_back_off.xml | 15 ++ src/main/res/drawable/switch_back_on.xml | 16 ++ src/main/res/drawable/switch_thumb.xml | 12 ++ src/main/res/layout/account_row.xml | 5 +- src/main/res/values/styles.xml | 14 +- 39 files changed, 914 insertions(+), 8 deletions(-) create mode 100644 art/md_switch_thumb_disable.svg create mode 100644 art/md_switch_thumb_off_normal.svg create mode 100644 art/md_switch_thumb_off_pressed.svg create mode 100644 art/md_switch_thumb_on_normal.svg create mode 100644 art/md_switch_thumb_on_pressed.svg create mode 100644 src/main/java/eu/siacs/conversations/ui/widget/Switch.java create mode 100644 src/main/res/drawable-hdpi/switch_thumb_disable.png create mode 100644 src/main/res/drawable-hdpi/switch_thumb_off_normal.png create mode 100644 src/main/res/drawable-hdpi/switch_thumb_off_pressed.png create mode 100644 src/main/res/drawable-hdpi/switch_thumb_on_normal.png create mode 100644 src/main/res/drawable-hdpi/switch_thumb_on_pressed.png create mode 100644 src/main/res/drawable-mdpi/switch_thumb_disable.png create mode 100644 src/main/res/drawable-mdpi/switch_thumb_off_normal.png create mode 100644 src/main/res/drawable-mdpi/switch_thumb_off_pressed.png create mode 100644 src/main/res/drawable-mdpi/switch_thumb_on_normal.png create mode 100644 src/main/res/drawable-mdpi/switch_thumb_on_pressed.png create mode 100644 src/main/res/drawable-xhdpi/switch_thumb_disable.png create mode 100644 src/main/res/drawable-xhdpi/switch_thumb_off_normal.png create mode 100644 src/main/res/drawable-xhdpi/switch_thumb_off_pressed.png create mode 100644 src/main/res/drawable-xhdpi/switch_thumb_on_normal.png create mode 100644 src/main/res/drawable-xhdpi/switch_thumb_on_pressed.png create mode 100644 src/main/res/drawable-xxhdpi/switch_thumb_disable.png create mode 100644 src/main/res/drawable-xxhdpi/switch_thumb_off_normal.png create mode 100644 src/main/res/drawable-xxhdpi/switch_thumb_off_pressed.png create mode 100644 src/main/res/drawable-xxhdpi/switch_thumb_on_normal.png create mode 100644 src/main/res/drawable-xxhdpi/switch_thumb_on_pressed.png create mode 100644 src/main/res/drawable-xxxhdpi/switch_thumb_disable.png create mode 100644 src/main/res/drawable-xxxhdpi/switch_thumb_off_normal.png create mode 100644 src/main/res/drawable-xxxhdpi/switch_thumb_off_pressed.png create mode 100644 src/main/res/drawable-xxxhdpi/switch_thumb_on_normal.png create mode 100644 src/main/res/drawable-xxxhdpi/switch_thumb_on_pressed.png create mode 100644 src/main/res/drawable/switch_back_off.xml create mode 100644 src/main/res/drawable/switch_back_on.xml create mode 100644 src/main/res/drawable/switch_thumb.xml diff --git a/art/md_switch_thumb_disable.svg b/art/md_switch_thumb_disable.svg new file mode 100644 index 000000000..efd83c2d0 --- /dev/null +++ b/art/md_switch_thumb_disable.svg @@ -0,0 +1,156 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/md_switch_thumb_off_normal.svg b/art/md_switch_thumb_off_normal.svg new file mode 100644 index 000000000..25d1761db --- /dev/null +++ b/art/md_switch_thumb_off_normal.svg @@ -0,0 +1,153 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/md_switch_thumb_off_pressed.svg b/art/md_switch_thumb_off_pressed.svg new file mode 100644 index 000000000..002b47815 --- /dev/null +++ b/art/md_switch_thumb_off_pressed.svg @@ -0,0 +1,159 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/md_switch_thumb_on_normal.svg b/art/md_switch_thumb_on_normal.svg new file mode 100644 index 000000000..5e8f90f39 --- /dev/null +++ b/art/md_switch_thumb_on_normal.svg @@ -0,0 +1,146 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/md_switch_thumb_on_pressed.svg b/art/md_switch_thumb_on_pressed.svg new file mode 100644 index 000000000..e0331e7b7 --- /dev/null +++ b/art/md_switch_thumb_on_pressed.svg @@ -0,0 +1,162 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/art/render.rb b/art/render.rb index 698abea5b..312dd06b1 100755 --- a/art/render.rb +++ b/art/render.rb @@ -33,7 +33,12 @@ images = { 'ic_send_picture_online.svg' => ['ic_send_picture_online', 36], 'ic_send_picture_offline.svg' => ['ic_send_picture_offline', 36], 'ic_send_picture_away.svg' => ['ic_send_picture_away', 36], - 'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36] + 'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36], + 'md_switch_thumb_disable.svg' => ['switch_thumb_disable', 48], + 'md_switch_thumb_off_normal.svg' => ['switch_thumb_off_normal', 48], + 'md_switch_thumb_off_pressed.svg' => ['switch_thumb_off_pressed', 48], + 'md_switch_thumb_on_normal.svg' => ['switch_thumb_on_normal', 48], + 'md_switch_thumb_on_pressed.svg' => ['switch_thumb_on_pressed', 48], } images.each do |source, result| resolutions.each do |name, factor| diff --git a/build.gradle b/build.gradle index 4e23c5224..09ad7161f 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,7 @@ dependencies { compile 'de.measite.minidns:minidns:0.1.3' compile 'de.timroes.android:EnhancedListView:0.3.4' compile 'me.leolin:ShortcutBadger:1.1.1@aar' + compile 'com.kyleduo.switchbutton:library:1.2.8' } android { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index 782a1231b..226b1920e 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -6,6 +6,8 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.ManageAccountActivity; +import eu.siacs.conversations.ui.widget.Switch; + import android.content.Context; import android.view.LayoutInflater; import android.view.View; @@ -14,7 +16,6 @@ import android.widget.ArrayAdapter; import android.widget.CompoundButton; import android.widget.ImageView; import android.widget.TextView; -import android.widget.Switch; public class AccountAdapter extends ArrayAdapter { @@ -53,8 +54,7 @@ public class AccountAdapter extends ArrayAdapter { } final Switch tglAccountState = (Switch) view.findViewById(R.id.tgl_account_status); final boolean isDisabled = (account.getStatus() == Account.State.DISABLED); - tglAccountState.setOnCheckedChangeListener(null); - tglAccountState.setChecked(!isDisabled); + tglAccountState.setChecked(!isDisabled,false); tglAccountState.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { diff --git a/src/main/java/eu/siacs/conversations/ui/widget/Switch.java b/src/main/java/eu/siacs/conversations/ui/widget/Switch.java new file mode 100644 index 000000000..c72e760ef --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/widget/Switch.java @@ -0,0 +1,70 @@ +package eu.siacs.conversations.ui.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import com.kyleduo.switchbutton.SwitchButton; + +import eu.siacs.conversations.Config; + +public class Switch extends SwitchButton { + + private int mTouchSlop; + private int mClickTimeout; + private float mStartX; + private float mStartY; + private OnClickListener mOnClickListener; + + public Switch(Context context) { + super(context); + mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); + } + + public Switch(Context context, AttributeSet attrs) { + super(context, attrs); + mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); + } + + public Switch(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout(); + } + + @Override + public void setOnClickListener(OnClickListener onClickListener) { + this.mOnClickListener = onClickListener; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!isEnabled()) { + float deltaX = event.getX() - mStartX; + float deltaY = event.getY() - mStartY; + int action = event.getAction(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mStartX = event.getX(); + mStartY = event.getY(); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + float time = event.getEventTime() - event.getDownTime(); + if (deltaX < mTouchSlop && deltaY < mTouchSlop && time < mClickTimeout) { + if (mOnClickListener != null) { + this.mOnClickListener.onClick(this); + } + } + break; + default: + break; + } + return true; + } + return super.onTouchEvent(event); + } +} diff --git a/src/main/res/drawable-hdpi/switch_thumb_disable.png b/src/main/res/drawable-hdpi/switch_thumb_disable.png new file mode 100644 index 0000000000000000000000000000000000000000..1e9b151b665024f4fc06544e6a40d93e1f5b6f1a GIT binary patch literal 1926 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-hdpi/switch_thumb_off_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..ca6e490962adf2e24f5e2d9c9b04a093c2747261 GIT binary patch literal 3478 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 Ef{V1l+5i9m literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/switch_thumb_on_normal.png b/src/main/res/drawable-hdpi/switch_thumb_on_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..cbcda5d478f3e326a637242886dee86e76b4649d GIT binary patch literal 1920 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/switch_thumb_disable.png b/src/main/res/drawable-mdpi/switch_thumb_disable.png new file mode 100644 index 0000000000000000000000000000000000000000..968de345d7720405f0c465edac55a45e11167aac GIT binary patch literal 1239 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 Hu0mjf?UW;a literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/switch_thumb_on_normal.png b/src/main/res/drawable-mdpi/switch_thumb_on_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..6a93d5f759083e47f0c7fba11c2f87d82174d8e3 GIT binary patch literal 1256 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 IM6N<$f}9jMVgLXD literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/switch_thumb_disable.png b/src/main/res/drawable-xhdpi/switch_thumb_disable.png new file mode 100644 index 0000000000000000000000000000000000000000..7f6773248428bb2bf3d2660202eef8e1318f07d5 GIT binary patch literal 2762 zcmZWrc|6mP8~;p8OZqxeNUo)lgrQtRCe7q5$(5s#^hLf#bKiGEp*feMulqY!Sk5iW zT}`=?G7?LUZLv)#zxDU;dA;7x`}I8kJlFe=C(YIxd06PU5C8y&QRbJg@!8`KAcB1V zB_FHHCol$qvWM_365^H0j}Hc#J7ECe(2+j?0fog<{G@cSnPc$v`*(t|oH$ z4fq>__6+n^yC39Jv<^QG076LAWfS|*; z7KrH5h4qRb3>?ktoH#%)82EIr#3oMqWMIaDhfRwy^Oz=W~Mu7o#{cg2XVcrq@hoBiB z2R)5=IS_ndCgZJDcXv08|#+m6_e-b&I37rV4!VerAw3)sb9S<#LA+{>>eF zchnAdW*}2`N5>-h!PU27x5e@yCPEa99`~sg$lRgFf2M7)=6cm%t!-^ReuYcUR%1{c z%xNlwCj^O8LOoJK<$3Fg%-QJ^jAAywy~ALcv*!T~Tg|Q~21tl*djt4XY*7hG#!W!M z?3rDALj&{4W#~VaV=axfwPO8JJY=3}wvuvswn!2p$zFvM&Yw^^7kA&L^-2O03Ra9Q zVoOFu=nWYf8W!vS?%@)Sth{voAE5)B5R=JzOQc>*2BYNGJNK-!N}7u1CT(jR0J&#H2BzDsf&LB9VxUOIqJ7i z>iP_c36B*WUZcLZ1GaR9c7sjc$`H-yG2GQ@`6CGNO5e!{Em0f{0a<<#y!<}P_8(J+ zX7>bje~vRS>p&H!lRc@)OJ15kwGe~HvsTz$#S z@i1RrrR5z|RFwZ71+^R#=5227iXPSRN~6rAac~N*quC)b5fM9AbKj@haO#hvvY~00 zzZ|HRB2Ox_gw^*4&iJ`#|9*TD*>mNrKfBSIrjQ6+T4l{siyDK$jsmec#<1u&AXmv2 zzFGq;d~*gu!jn};tU9sG-^WzWDgF)Ru0>OrdLBbuQ@Gq-vJ#4;3w9RERvKz@^b`4X zYxMes1NLjbY%L7Va_WZ9RCU>mr`-ds1`N*V8*USv866UUm6EvUI0Lfz&%wS0w#HUB z9dlFSgUVgTTY^8Xse{f{OYxLi%9J&DY65i2vto|6}V`oLc zBpC~7bZ{&3p~>yS(^BZJG*TG@I5d0S0CcIcs)`|v4x#Tj{YBL)mnklC_&!uDBe&|Sh%fRLep7Q9%O86fBpJ31)`za_c!RC;9zv% z#?sEl#>W1vl}()AMVH;Vr8m5Jxle~eRa5f>Scskf8jS^GfBhn0ucX)NyPxZ7bo*g z%$`k7PEOb#iPhNGYG`Wy7~nUV7!l||rGbudlht5GfNV#7NjPWX9H!v4rCMIy&g^J! zZ@iw4jt-}2%B?f`{a3HPbV2d*Z{8mG8Hbh)zx?=XXLE)kxlcGNy#Cv;3O65cM?E?? zSbLAr)8ifqGcr2dpAL)%HK&}@Phtr-><`4ZpFw-zqG7Pdzk-kh&nhb`3G7f;wUQx0 zL9{THN=t_j6}TaQ+`#Q00k! znF9+Jw$R*+68R1+^T)E5&u^hpNWYM>G&wb$O~Aw@`9QZ6W4l+^Go_xD=I$;`5@|aA zpUkDvMd+q{C@MOs4nL}W*woZCsce3}XDT8>eV?*!I61w7c9^w(08Y)STQtjNBa=?` zQYehHwl*gxJP8RE(>w9kj;4fW0ZtDNFWw6cWbtBjvFqo7zGn8J>IcR>yl9Fs=+p27 zDjNd)m|mMfKY#w5)RSwuKDZcEhQ}Y9t{ZY1)5hU&5^_e7`5lpq!ws{`OnI;^Sh4TN ztCix&wQl;942)c?Mq{cQN!nV^_x-DErSaz4v^KTjg#Ko4e};@m|LrDQA0HoG3Fli% z=qATkuU=h-5v=QY>z_L#xw}6}CgwwPi4tYL4m7MaC18C3Bep9+879~aIMN`A!!^)E z)f6J{6xA2 zL?V&MJf#s-+BK(arJQT`1S+npQZ*oG%bPbY@Qp*u3i!Oua!*KD0JKGK{|q_W5g=Ve zT4E3*pIS(I?X5XO2ODwzw4%eV3jN{x3k&oup2kd ztvoo)*=wY)g#6OCs!Jjh_A)86BVox{m$j`PCn@{?D$}x?Rwcd2 z9_baGRpKgFnrSlJPG28J6VI&^B0M2@KXva22&^Me%$fLjra4Q<>moP3`f6DF-3w140S)uY`#>`C z)g?gJcHYeJQpO2`06XabwaWTrQZ)&<6e3d$)uk^)4fMVtjQ?PU7#{NngH%5yR^M2f zpL{Me%sQ5#6f5i2DMup0_vs|QThp!utQ}_FCTqR;zb|b!=7&3cttp6m-XWC#cLFFg L>&x{BkNE!qAc#^Y literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/switch_thumb_off_normal.png b/src/main/res/drawable-xhdpi/switch_thumb_off_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..4199d322b86c67cd1e6a8c1e2adce45d0102c4d7 GIT binary patch literal 2817 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 LER0Ep9#8%Y@&QU6 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-xhdpi/switch_thumb_off_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..2b86888f300e1150aa12366ef29dd5a757dc4216 GIT binary patch literal 5050 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 ba0-mX?b_NDuny4+EdV398LUq4e(e7M{lc0~ literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_off_normal.png b/src/main/res/drawable-xxhdpi/switch_thumb_off_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..f747b5596a60a401759eb3411e15683eea1993d0 GIT binary patch literal 4602 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-xxhdpi/switch_thumb_off_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..b9fe6d469eb5b139988b9074a51bec0d2bb1a85d GIT binary patch literal 8315 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 ZuM#(j_T6-6@iq~HC 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 BsLTKW literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxhdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-xxhdpi/switch_thumb_on_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..7a4fed5405ae19947ab4a73e43d231931f712377 GIT binary patch literal 8899 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 Rc%1+M002ovPDHLkV1midzO(=U literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_disable.png b/src/main/res/drawable-xxxhdpi/switch_thumb_disable.png new file mode 100644 index 0000000000000000000000000000000000000000..3970168ca2dd45cba89a2c295751ba5de593de78 GIT binary patch literal 6854 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 Ms-~@4rED4XKZv?aV*mgE literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_off_normal.png b/src/main/res/drawable-xxxhdpi/switch_thumb_off_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..ea8d4f8940c3c30d4ddd84b1292c085e0c4bc1e2 GIT binary patch literal 7000 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_off_pressed.png b/src/main/res/drawable-xxxhdpi/switch_thumb_off_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..84d667b131a0af9f19e1ca95ab0e0ae3a3eedede GIT binary patch literal 11984 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_on_normal.png b/src/main/res/drawable-xxxhdpi/switch_thumb_on_normal.png new file mode 100644 index 0000000000000000000000000000000000000000..06b190eb930ad601d98ea40fd36bdba9c06b2cc3 GIT binary patch literal 7037 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 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/switch_thumb_on_pressed.png b/src/main/res/drawable-xxxhdpi/switch_thumb_on_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..79c30d1e549127d1848f6ea901a7213506eb8a79 GIT binary patch literal 12753 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 Jxr}Mx{{gA)6ukfd literal 0 HcmV?d00001 diff --git a/src/main/res/drawable/switch_back_off.xml b/src/main/res/drawable/switch_back_off.xml new file mode 100644 index 000000000..20d2fb146 --- /dev/null +++ b/src/main/res/drawable/switch_back_off.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/switch_back_on.xml b/src/main/res/drawable/switch_back_on.xml new file mode 100644 index 000000000..45117a98e --- /dev/null +++ b/src/main/res/drawable/switch_back_on.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/drawable/switch_thumb.xml b/src/main/res/drawable/switch_thumb.xml new file mode 100644 index 000000000..ba3d1c456 --- /dev/null +++ b/src/main/res/drawable/switch_thumb.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/res/layout/account_row.xml b/src/main/res/layout/account_row.xml index 06716a10a..077170b29 100644 --- a/src/main/res/layout/account_row.xml +++ b/src/main/res/layout/account_row.xml @@ -45,13 +45,14 @@ android:textStyle="bold" /> - \ No newline at end of file diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index b98a37fcb..e8572d9d4 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -4,8 +4,18 @@ 1.5dp @color/black12 - \ No newline at end of file From 0166ced46cc6663a9fa86b60588d440ca8a7af7c Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Jul 2015 13:36:02 +0200 Subject: [PATCH 002/166] bugfix: accept status code 201 on http upload --- .../java/eu/siacs/conversations/http/HttpUploadConnection.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index dd8427541..a3ab8daba 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -157,7 +157,7 @@ public class HttpUploadConnection implements Transferable { os.close(); is.close(); int code = connection.getResponseCode(); - if (code == 200) { + if (code == 200 || code == 201) { Log.d(Config.LOGTAG, "finished uploading file"); Message.FileParams params = message.getFileParams(); if (key != null) { From 5c017e5186d9359bbfca0a05dcac8ed22516800d Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Jul 2015 14:25:30 +0200 Subject: [PATCH 003/166] bugfix: use sendIqPacket method in service instead of invoking XmppConnection directly --- .../siacs/conversations/services/XmppConnectionService.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ee212aed0..f5c54adf5 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -830,9 +830,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } else { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster"); } - iqPacket.query(Xmlns.ROSTER).setAttribute("ver", - account.getRosterVersion()); - account.getXmppConnection().sendIqPacket(iqPacket, mIqParser); + iqPacket.query(Xmlns.ROSTER).setAttribute("ver",account.getRosterVersion()); + sendIqPacket(account,iqPacket,mIqParser); } public void fetchBookmarks(final Account account) { From 9b70c7e68ccf3b1efdaff782f14b0549415f1339 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Jul 2015 14:51:04 +0200 Subject: [PATCH 004/166] bugfix: don't crash if aes key could not be set before jingle transfer --- .../eu/siacs/conversations/xmpp/jingle/JingleConnection.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index 3c355b577..65cafe791 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -368,7 +368,10 @@ public class JingleConnection implements Transferable { message, false); if (message.getEncryption() == Message.ENCRYPTION_OTR) { Conversation conversation = this.message.getConversation(); - this.mXmppConnectionService.renewSymmetricKey(conversation); + if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not set symmetric key"); + cancel(); + } content.setFileOffer(this.file, true); this.file.setKey(conversation.getSymmetricKey()); } else { From c4f3e5be3f4a24391a45d6199887d78e07359394 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sun, 19 Jul 2015 14:51:11 +0200 Subject: [PATCH 005/166] shut up linter --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 09ad7161f..69eee6e56 100644 --- a/build.gradle +++ b/build.gradle @@ -94,7 +94,7 @@ android { } lintOptions { - disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity' + disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource' } subprojects { From b8048a5538293b3855c2b2eae55d35645e614f11 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 29 May 2015 11:17:26 +0200 Subject: [PATCH 006/166] CryptoNext persistance layer mockup Initial sketch of the peripheral storage infrastructure for the new axolotl-based encryption scheme. --- build.gradle | 1 + .../crypto/axolotl/AxolotlService.java | 440 ++++++++++++++++++ .../crypto/axolotl/XmppAxolotlMessage.java | 4 + .../siacs/conversations/entities/Account.java | 4 + .../siacs/conversations/entities/Contact.java | 186 +++++--- .../siacs/conversations/entities/Message.java | 13 + .../persistance/DatabaseBackend.java | 263 ++++++++++- 7 files changed, 844 insertions(+), 67 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java create mode 100644 src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java diff --git a/build.gradle b/build.gradle index 69eee6e56..d16fd3b88 100644 --- a/build.gradle +++ b/build.gradle @@ -37,6 +37,7 @@ dependencies { compile 'de.timroes.android:EnhancedListView:0.3.4' compile 'me.leolin:ShortcutBadger:1.1.1@aar' compile 'com.kyleduo.switchbutton:library:1.2.8' + compile 'org.whispersystems:axolotl-android:1.3.4' } android { diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java new file mode 100644 index 000000000..8e3002487 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -0,0 +1,440 @@ +package eu.siacs.conversations.crypto.axolotl; + +import android.util.Log; + +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.ecc.Curve; +import org.whispersystems.libaxolotl.ecc.ECKeyPair; +import org.whispersystems.libaxolotl.state.AxolotlStore; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import org.whispersystems.libaxolotl.util.KeyHelper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class AxolotlService { + + private Account account; + private XmppConnectionService mXmppConnectionService; + private SQLiteAxolotlStore axolotlStore; + private Map sessions; + + public static class SQLiteAxolotlStore implements AxolotlStore { + + public static final String PREKEY_TABLENAME = "prekeys"; + public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; + public static final String SESSION_TABLENAME = "signed_prekeys"; + public static final String NAME = "name"; + public static final String DEVICE_ID = "device_id"; + public static final String ID = "id"; + public static final String KEY = "key"; + public static final String ACCOUNT = "account"; + + public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; + public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; + + private final Account account; + private final XmppConnectionService mXmppConnectionService; + + private final IdentityKeyPair identityKeyPair; + private final int localRegistrationId; + + + private static IdentityKeyPair generateIdentityKeyPair() { + Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair..."); + ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); + IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), + identityKeyPairKeys.getPrivateKey()); + return ownKey; + } + + private static int generateRegistrationId() { + Log.d(Config.LOGTAG, "Generating axolotl registration ID..."); + int reg_id = KeyHelper.generateRegistrationId(false); + return reg_id; + } + + public SQLiteAxolotlStore(Account account, XmppConnectionService service) { + this.account = account; + this.mXmppConnectionService = service; + this.identityKeyPair = loadIdentityKeyPair(); + this.localRegistrationId = loadRegistrationId(); + } + + // -------------------------------------- + // IdentityKeyStore + // -------------------------------------- + + private IdentityKeyPair loadIdentityKeyPair() { + String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR); + IdentityKeyPair ownKey; + if( serializedKey != null ) { + try { + ownKey = new IdentityKeyPair(serializedKey.getBytes()); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage()); + return null; + } + } else { + Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid()); + ownKey = generateIdentityKeyPair(); + boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, new String(ownKey.serialize())); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new key to the database!"); + } + } + return ownKey; + } + + private int loadRegistrationId() { + String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); + int reg_id; + if (regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid()); + reg_id = generateRegistrationId(); + boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new key to the database!"); + } + } + return reg_id; + } + + /** + * Get the local client's identity key pair. + * + * @return The local client's persistent identity key pair. + */ + @Override + public IdentityKeyPair getIdentityKeyPair() { + return identityKeyPair; + } + + /** + * Return the local client's registration ID. + *

+ * Clients should maintain a registration ID, a random number + * between 1 and 16380 that's generated once at install time. + * + * @return the local client's registration ID. + */ + @Override + public int getLocalRegistrationId() { + return localRegistrationId; + } + + /** + * Save a remote client's identity key + *

+ * Store a remote client's identity key as trusted. + * + * @param name The name of the remote client. + * @param identityKey The remote client's identity key. + */ + @Override + public void saveIdentity(String name, IdentityKey identityKey) { + try { + Jid contactJid = Jid.fromString(name); + Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); + if (conversation != null) { + conversation.getContact().addAxolotlIdentityKey(identityKey, false); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); + } + } catch (final InvalidJidException e) { + Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString()); + } + } + + /** + * Verify a remote client's identity key. + *

+ * Determine whether a remote client's identity is trusted. Convention is + * that the TextSecure protocol is 'trust on first use.' This means that + * an identity key is considered 'trusted' if there is no entry for the recipient + * in the local store, or if it matches the saved key for a recipient in the local + * store. Only if it mismatches an entry in the local store is it considered + * 'untrusted.' + * + * @param name The name of the remote client. + * @param identityKey The identity key to verify. + * @return true if trusted, false if untrusted. + */ + @Override + public boolean isTrustedIdentity(String name, IdentityKey identityKey) { + try { + Jid contactJid = Jid.fromString(name); + Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); + if (conversation != null) { + List trustedKeys = conversation.getContact().getTrustedAxolotlIdentityKeys(); + return trustedKeys.contains(identityKey); + } else { + return false; + } + } catch (final InvalidJidException e) { + Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString()); + return false; + } + } + + // -------------------------------------- + // SessionStore + // -------------------------------------- + + /** + * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, + * or a new SessionRecord if one does not currently exist. + *

+ * It is important that implementations return a copy of the current durable information. The + * returned SessionRecord may be modified, but those changes should not have an effect on the + * durable session state (what is returned by subsequent calls to this method) without the + * store method being called here first. + * + * @param address The name and device ID of the remote client. + * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or + * a new SessionRecord if one does not currently exist. + */ + @Override + public SessionRecord loadSession(AxolotlAddress address) { + SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); + return (session!=null)?session:new SessionRecord(); + } + + /** + * Returns all known devices with active sessions for a recipient + * + * @param name the name of the client. + * @return all known sub-devices with active sessions. + */ + @Override + public List getSubDeviceSessions(String name) { + return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, + new AxolotlAddress(name,0)); + } + + /** + * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @param record the current SessionRecord for the remote client. + */ + @Override + public void storeSession(AxolotlAddress address, SessionRecord record) { + mXmppConnectionService.databaseBackend.storeSession(account, address, record); + } + + /** + * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @return true if a {@link SessionRecord} exists, false otherwise. + */ + @Override + public boolean containsSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.containsSession(account, address); + } + + /** + * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + */ + @Override + public void deleteSession(AxolotlAddress address) { + mXmppConnectionService.databaseBackend.deleteSession(account, address); + } + + /** + * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. + * + * @param name the name of the remote client. + */ + @Override + public void deleteAllSessions(String name) { + mXmppConnectionService.databaseBackend.deleteAllSessions(account, + new AxolotlAddress(name,0)); + } + + // -------------------------------------- + // PreKeyStore + // -------------------------------------- + + /** + * Load a local PreKeyRecord. + * + * @param preKeyId the ID of the local PreKeyRecord. + * @return the corresponding PreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. + */ + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); + if(record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord!"); + } + return record; + } + + /** + * Store a local PreKeyRecord. + * + * @param preKeyId the ID of the PreKeyRecord to store. + * @param record the PreKeyRecord. + */ + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + mXmppConnectionService.databaseBackend.storePreKey(account, record); + } + + /** + * @param preKeyId A PreKeyRecord ID. + * @return true if the store has a record for the preKeyId, otherwise false. + */ + @Override + public boolean containsPreKey(int preKeyId) { + return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); + } + + /** + * Delete a PreKeyRecord from local storage. + * + * @param preKeyId The ID of the PreKeyRecord to remove. + */ + @Override + public void removePreKey(int preKeyId) { + mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); + } + + // -------------------------------------- + // SignedPreKeyStore + // -------------------------------------- + + /** + * Load a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the local SignedPreKeyRecord. + * @return the corresponding SignedPreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. + */ + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); + if(record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord!"); + } + return record; + } + + /** + * Load all local SignedPreKeyRecords. + * + * @return All stored SignedPreKeyRecords. + */ + @Override + public List loadSignedPreKeys() { + return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); + } + + /** + * Store a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. + * @param record the SignedPreKeyRecord. + */ + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); + } + + /** + * @param signedPreKeyId A SignedPreKeyRecord ID. + * @return true if the store has a record for the signedPreKeyId, otherwise false. + */ + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); + } + + /** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. + */ + @Override + public void removeSignedPreKey(int signedPreKeyId) { + mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); + } + } + + private static class XmppAxolotlSession { + private List untrustedMessages; + private AxolotlStore axolotlStore; + + public XmppAxolotlSession(SQLiteAxolotlStore axolotlStore) { + this.untrustedMessages = new ArrayList<>(); + this.axolotlStore = axolotlStore; + } + + public void trust() { + for (Message message : this.untrustedMessages) { + message.trust(); + } + this.untrustedMessages = null; + } + + public boolean isTrusted() { + return (this.untrustedMessages == null); + } + + public String processReceiving(XmppAxolotlMessage incomingMessage) { + return null; + } + + public XmppAxolotlMessage processSending(String outgoingMessage) { + return null; + } + } + + public AxolotlService(Account account, XmppConnectionService connectionService) { + this.mXmppConnectionService = connectionService; + this.account = account; + this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); + this.sessions = new HashMap<>(); + } + + public void trustSession(Jid counterpart) { + XmppAxolotlSession session = sessions.get(counterpart); + if(session != null) { + session.trust(); + } + } + + public boolean isTrustedSession(Jid counterpart) { + XmppAxolotlSession session = sessions.get(counterpart); + return session != null && session.isTrusted(); + } + + +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java new file mode 100644 index 000000000..b11670e4a --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -0,0 +1,4 @@ +package eu.siacs.conversations.crypto.axolotl; + +public class XmppAxolotlMessage { +} diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index f472361f2..383125667 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -254,6 +254,10 @@ public class Account extends AbstractEntity { return keys; } + public String getKey(final String name) { + return this.keys.optString(name, null); + } + public boolean setKey(final String keyName, final String keyValue) { try { this.keys.put(keyName, keyValue); diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index e546f2149..2f9b375df 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -2,15 +2,19 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; +import android.util.Log; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.InvalidKeyException; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -183,20 +187,22 @@ public class Contact implements ListItem, Blockable { } public ContentValues getContentValues() { - final ContentValues values = new ContentValues(); - values.put(ACCOUNT, accountUuid); - values.put(SYSTEMNAME, systemName); - values.put(SERVERNAME, serverName); - values.put(JID, jid.toString()); - values.put(OPTIONS, subscription); - values.put(SYSTEMACCOUNT, systemAccount); - values.put(PHOTOURI, photoUri); - values.put(KEYS, keys.toString()); - values.put(AVATAR, avatar == null ? null : avatar.getFilename()); - values.put(LAST_PRESENCE, lastseen.presence); - values.put(LAST_TIME, lastseen.time); - values.put(GROUPS, groups.toString()); - return values; + synchronized (this.keys) { + final ContentValues values = new ContentValues(); + values.put(ACCOUNT, accountUuid); + values.put(SYSTEMNAME, systemName); + values.put(SERVERNAME, serverName); + values.put(JID, jid.toString()); + values.put(OPTIONS, subscription); + values.put(SYSTEMACCOUNT, systemAccount); + values.put(PHOTOURI, photoUri); + values.put(KEYS, keys.toString()); + values.put(AVATAR, avatar == null ? null : avatar.getFilename()); + values.put(LAST_PRESENCE, lastseen.presence); + values.put(LAST_TIME, lastseen.time); + values.put(GROUPS, groups.toString()); + return values; + } } public int getSubscription() { @@ -281,63 +287,109 @@ public class Contact implements ListItem, Blockable { } public ArrayList getOtrFingerprints() { - final ArrayList fingerprints = new ArrayList(); - try { - if (this.keys.has("otr_fingerprints")) { - final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); - for (int i = 0; i < prints.length(); ++i) { - final String print = prints.isNull(i) ? null : prints.getString(i); - if (print != null && !print.isEmpty()) { - fingerprints.add(prints.getString(i)); + synchronized (this.keys) { + final ArrayList fingerprints = new ArrayList(); + try { + if (this.keys.has("otr_fingerprints")) { + final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); + for (int i = 0; i < prints.length(); ++i) { + final String print = prints.isNull(i) ? null : prints.getString(i); + if (print != null && !print.isEmpty()) { + fingerprints.add(prints.getString(i)); + } } } - } - } catch (final JSONException ignored) { + } catch (final JSONException ignored) { + } + return fingerprints; } - return fingerprints; } - public boolean addOtrFingerprint(String print) { - if (getOtrFingerprints().contains(print)) { - return false; - } - try { - JSONArray fingerprints; - if (!this.keys.has("otr_fingerprints")) { - fingerprints = new JSONArray(); - - } else { - fingerprints = this.keys.getJSONArray("otr_fingerprints"); + synchronized (this.keys) { + if (getOtrFingerprints().contains(print)) { + return false; + } + try { + JSONArray fingerprints; + if (!this.keys.has("otr_fingerprints")) { + fingerprints = new JSONArray(); + } else { + fingerprints = this.keys.getJSONArray("otr_fingerprints"); + } + fingerprints.put(print); + this.keys.put("otr_fingerprints", fingerprints); + return true; + } catch (final JSONException ignored) { + return false; } - fingerprints.put(print); - this.keys.put("otr_fingerprints", fingerprints); - return true; - } catch (final JSONException ignored) { - return false; } } public long getPgpKeyId() { - if (this.keys.has("pgp_keyid")) { - try { - return this.keys.getLong("pgp_keyid"); - } catch (JSONException e) { + synchronized (this.keys) { + if (this.keys.has("pgp_keyid")) { + try { + return this.keys.getLong("pgp_keyid"); + } catch (JSONException e) { + return 0; + } + } else { return 0; } - } else { - return 0; } } public void setPgpKeyId(long keyId) { - try { - this.keys.put("pgp_keyid", keyId); - } catch (final JSONException ignored) { - + synchronized (this.keys) { + try { + this.keys.put("pgp_keyid", keyId); + } catch (final JSONException ignored) { + } } } + public List getTrustedAxolotlIdentityKeys() { + synchronized (this.keys) { + JSONArray serializedKeyItems = this.keys.optJSONArray("axolotl_identity_key"); + List identityKeys = new ArrayList<>(); + if(serializedKeyItems != null) { + for(int i = 0; i getSubDeviceSessions(Account account, AxolotlAddress contact) { + List devices = new ArrayList<>(); + final SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {AxolotlService.SQLiteAxolotlStore.DEVICE_ID}; + String[] selectionArgs = {account.getUuid(), + contact.getName()}; + Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, + columns, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND ", + selectionArgs, + null, null, null); + + while(cursor.moveToNext()) { + devices.add(cursor.getInt( + cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.DEVICE_ID))); + } + + cursor.close(); + return devices; + } + + public boolean containsSession(Account account, AxolotlAddress contact) { + Cursor cursor = getCursorForSession(account, contact); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName()); + values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, session.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values); + } + + public void deleteSession(Account account, AxolotlAddress contact) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), + contact.getName(), + Integer.toString(contact.getDeviceId())}; + db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " = ? ", + args); + } + + public void deleteAllSessions(Account account, AxolotlAddress contact) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), contact.getName()}; + db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ?", + args); + } + + private Cursor getCursorForPreKey(Account account, int preKeyId) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)}; + Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, + columns, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + + AxolotlService.SQLiteAxolotlStore.ID + "=?", + selectionArgs, + null, null, null); + + return cursor; + } + + public PreKeyRecord loadPreKey(Account account, int preKeyId) { + PreKeyRecord record = null; + Cursor cursor = getCursorForPreKey(account, preKeyId); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + record = new PreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()); + } catch (IOException e ) { + throw new AssertionError(e); + } + } + cursor.close(); + return record; + } + + public boolean containsPreKey(Account account, int preKeyId) { + Cursor cursor = getCursorForPreKey(account, preKeyId); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storePreKey(Account account, PreKeyRecord record) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, record.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); + } + + public void deletePreKey(Account account, int preKeyId) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), Integer.toString(preKeyId)}; + db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + + AxolotlService.SQLiteAxolotlStore.ID + "=?", + args); + } + + private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)}; + Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + AxolotlService.SQLiteAxolotlStore.ID + "=?", + selectionArgs, + null, null, null); + + return cursor; + } + + public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) { + SignedPreKeyRecord record = null; + Cursor cursor = getCursorForPreKey(account, signedPreKeyId); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + record = new SignedPreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()); + } catch (IOException e ) { + throw new AssertionError(e); + } + } + cursor.close(); + return record; + } + + public List loadSignedPreKeys(Account account) { + List prekeys = new ArrayList<>(); + SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid()}; + Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + columns, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=?", + selectionArgs, + null, null, null); + + while(cursor.moveToNext()) { + try { + prekeys.add(new SignedPreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes())); + } catch (IOException ignored) { + } + } + return prekeys; + } + + public boolean containsSignedPreKey(Account account, int signedPreKeyId) { + Cursor cursor = getCursorForPreKey(account, signedPreKeyId); + int count = cursor.getCount(); + cursor.close(); + return count != 0; + } + + public void storeSignedPreKey(Account account, SignedPreKeyRecord record) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, record.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + db.insert(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); + } + + public void deleteSignedPreKey(Account account, int signedPreKeyId) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)}; + db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + "=? AND " + + AxolotlService.SQLiteAxolotlStore.ID + "=?", + args); + } } From 077932eb558853e4c3c3046c54db18c530495d8c Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 29 May 2015 11:18:25 +0200 Subject: [PATCH 007/166] CryptoNext Menu entries added --- src/main/res/menu/encryption_choices.xml | 3 +++ src/main/res/values/strings.xml | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/res/menu/encryption_choices.xml b/src/main/res/menu/encryption_choices.xml index adf0ad8dc..1b3fe76ef 100644 --- a/src/main/res/menu/encryption_choices.xml +++ b/src/main/res/menu/encryption_choices.xml @@ -11,6 +11,9 @@ + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index dc29ffd33..30a3e4d28 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -154,6 +154,7 @@ Plain text OTR OpenPGP + Axolotl Edit account Delete account Temporarily disable From f73aa1a2006beb741bc39026bfd10e6166d7951a Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 25 Jun 2015 16:56:34 +0200 Subject: [PATCH 008/166] Reworked axolotl protocol layer Numerous fixes --- .../crypto/axolotl/AxolotlService.java | 308 +++++++++++++++--- .../axolotl/NoSessionsCreatedException.java | 4 + .../crypto/axolotl/XmppAxolotlMessage.java | 180 ++++++++++ .../siacs/conversations/entities/Account.java | 7 + .../siacs/conversations/entities/Contact.java | 62 ++-- .../siacs/conversations/entities/Message.java | 14 +- .../persistance/DatabaseBackend.java | 90 +++-- .../services/XmppConnectionService.java | 7 +- 8 files changed, 578 insertions(+), 94 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 8e3002487..eae7a9abc 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1,15 +1,29 @@ package eu.siacs.conversations.crypto.axolotl; +import android.util.Base64; import android.util.Log; import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.DuplicateMessageException; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.InvalidMessageException; +import org.whispersystems.libaxolotl.InvalidVersionException; +import org.whispersystems.libaxolotl.LegacyMessageException; +import org.whispersystems.libaxolotl.NoSessionException; +import org.whispersystems.libaxolotl.SessionBuilder; +import org.whispersystems.libaxolotl.SessionCipher; +import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.protocol.CiphertextMessage; +import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; +import org.whispersystems.libaxolotl.protocol.WhisperMessage; import org.whispersystems.libaxolotl.state.AxolotlStore; +import org.whispersystems.libaxolotl.state.PreKeyBundle; import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; @@ -17,43 +31,50 @@ import org.whispersystems.libaxolotl.util.KeyHelper; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Random; +import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; public class AxolotlService { - private Account account; - private XmppConnectionService mXmppConnectionService; - private SQLiteAxolotlStore axolotlStore; - private Map sessions; + private final Account account; + private final XmppConnectionService mXmppConnectionService; + private final SQLiteAxolotlStore axolotlStore; + private final SessionMap sessions; + private int ownDeviceId; public static class SQLiteAxolotlStore implements AxolotlStore { public static final String PREKEY_TABLENAME = "prekeys"; public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; - public static final String SESSION_TABLENAME = "signed_prekeys"; - public static final String NAME = "name"; + public static final String SESSION_TABLENAME = "sessions"; + public static final String ACCOUNT = "account"; public static final String DEVICE_ID = "device_id"; public static final String ID = "id"; public static final String KEY = "key"; - public static final String ACCOUNT = "account"; + public static final String NAME = "name"; + public static final String TRUSTED = "trusted"; public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; + public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; private final Account account; private final XmppConnectionService mXmppConnectionService; private final IdentityKeyPair identityKeyPair; private final int localRegistrationId; + private int currentPreKeyId = 0; private static IdentityKeyPair generateIdentityKeyPair() { @@ -75,6 +96,14 @@ public class AxolotlService { this.mXmppConnectionService = service; this.identityKeyPair = loadIdentityKeyPair(); this.localRegistrationId = loadRegistrationId(); + this.currentPreKeyId = loadCurrentPreKeyId(); + for( SignedPreKeyRecord record:loadSignedPreKeys()) { + Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId()); + } + } + + public int getCurrentPreKeyId() { + return currentPreKeyId; } // -------------------------------------- @@ -86,21 +115,22 @@ public class AxolotlService { IdentityKeyPair ownKey; if( serializedKey != null ) { try { - ownKey = new IdentityKeyPair(serializedKey.getBytes()); + ownKey = new IdentityKeyPair(Base64.decode(serializedKey,Base64.DEFAULT)); + return ownKey; } catch (InvalidKeyException e) { Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage()); - return null; +// return null; } - } else { + } //else { Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid()); ownKey = generateIdentityKeyPair(); - boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, new String(ownKey.serialize())); + boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, Base64.encodeToString(ownKey.serialize(), Base64.DEFAULT)); if(success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { Log.e(Config.LOGTAG, "Failed to write new key to the database!"); } - } + //} return ownKey; } @@ -122,6 +152,19 @@ public class AxolotlService { return reg_id; } + private int loadCurrentPreKeyId() { + String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); + int reg_id; + if (regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.d(Config.LOGTAG, "Could not retrieve current prekey id for account " + account.getJid()); + reg_id = 0; + } + return reg_id; + } + + /** * Get the local client's identity key pair. * @@ -159,7 +202,7 @@ public class AxolotlService { Jid contactJid = Jid.fromString(name); Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); if (conversation != null) { - conversation.getContact().addAxolotlIdentityKey(identityKey, false); + conversation.getContact().addAxolotlIdentityKey(identityKey); mXmppConnectionService.updateConversationUi(); mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); } @@ -188,8 +231,8 @@ public class AxolotlService { Jid contactJid = Jid.fromString(name); Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); if (conversation != null) { - List trustedKeys = conversation.getContact().getTrustedAxolotlIdentityKeys(); - return trustedKeys.contains(identityKey); + List trustedKeys = conversation.getContact().getAxolotlIdentityKeys(); + return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); } else { return false; } @@ -274,7 +317,15 @@ public class AxolotlService { @Override public void deleteAllSessions(String name) { mXmppConnectionService.databaseBackend.deleteAllSessions(account, - new AxolotlAddress(name,0)); + new AxolotlAddress(name, 0)); + } + + public boolean isTrustedSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.isTrustedSession(this.account, address); + } + + public void setTrustedSession(AxolotlAddress address, boolean trusted) { + mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address,trusted); } // -------------------------------------- @@ -292,7 +343,7 @@ public class AxolotlService { public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); if(record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord!"); + throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); } return record; } @@ -306,6 +357,13 @@ public class AxolotlService { @Override public void storePreKey(int preKeyId, PreKeyRecord record) { mXmppConnectionService.databaseBackend.storePreKey(account, record); + currentPreKeyId = preKeyId; + boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID,Integer.toString(preKeyId)); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!"); + } } /** @@ -342,7 +400,7 @@ public class AxolotlService { public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); if(record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord!"); + throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); } return record; } @@ -388,53 +446,229 @@ public class AxolotlService { } } - private static class XmppAxolotlSession { - private List untrustedMessages; - private AxolotlStore axolotlStore; + public static class XmppAxolotlSession { + private SessionCipher cipher; + private boolean isTrusted = false; + private SQLiteAxolotlStore sqLiteAxolotlStore; + private AxolotlAddress remoteAddress; - public XmppAxolotlSession(SQLiteAxolotlStore axolotlStore) { - this.untrustedMessages = new ArrayList<>(); - this.axolotlStore = axolotlStore; + public XmppAxolotlSession(SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { + this.cipher = new SessionCipher(store, remoteAddress); + this.remoteAddress = remoteAddress; + this.sqLiteAxolotlStore = store; + this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress); } public void trust() { - for (Message message : this.untrustedMessages) { - message.trust(); - } - this.untrustedMessages = null; + sqLiteAxolotlStore.setTrustedSession(remoteAddress, true); + this.isTrusted = true; } public boolean isTrusted() { - return (this.untrustedMessages == null); + return this.isTrusted; } - public String processReceiving(XmppAxolotlMessage incomingMessage) { - return null; + public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { + byte[] plaintext = null; + try { + try { + PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); + Log.d(Config.LOGTAG,"PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + plaintext = cipher.decrypt(message); + } catch (InvalidMessageException|InvalidVersionException e) { + WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); + plaintext = cipher.decrypt(message); + } catch (InvalidKeyException|InvalidKeyIdException| UntrustedIdentityException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } + } catch (LegacyMessageException|InvalidMessageException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } catch (DuplicateMessageException|NoSessionException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } + return plaintext; } - public XmppAxolotlMessage processSending(String outgoingMessage) { - return null; + public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(byte[] outgoingMessage) { + CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); + XmppAxolotlMessage.XmppAxolotlMessageHeader header = + new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(), + ciphertextMessage.serialize()); + return header; } } + private static class AxolotlAddressMap { + protected Map> map; + protected final Object MAP_LOCK = new Object(); + + public AxolotlAddressMap() { + this.map = new HashMap<>(); + } + + public void put(AxolotlAddress address, T value) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if (devices == null) { + devices = new HashMap<>(); + map.put(address.getName(), devices); + } + devices.put(address.getDeviceId(), value); + } + } + + public T get(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if(devices == null) { + return null; + } + return devices.get(address.getDeviceId()); + } + } + + public Map getAll(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if(devices == null) { + return new HashMap<>(); + } + return devices; + } + } + + public boolean hasAny(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + return devices != null && !devices.isEmpty(); + } + } + + + } + + private static class SessionMap extends AxolotlAddressMap { + + public SessionMap(SQLiteAxolotlStore store, Account account) { + super(); + this.fillMap(store, account); + } + + private void fillMap(SQLiteAxolotlStore store, Account account) { + for(Contact contact:account.getRoster().getContacts()){ + Jid bareJid = contact.getJid().toBareJid(); + if(bareJid == null) { + continue; // FIXME: handle this? + } + String address = bareJid.toString(); + List deviceIDs = store.getSubDeviceSessions(address); + for(Integer deviceId:deviceIDs) { + AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); + this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress)); + } + } + } + + } + + } + public AxolotlService(Account account, XmppConnectionService connectionService) { this.mXmppConnectionService = connectionService; this.account = account; this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); - this.sessions = new HashMap<>(); + this.sessions = new SessionMap(axolotlStore, account); + this.ownDeviceId = axolotlStore.getLocalRegistrationId(); } - public void trustSession(Jid counterpart) { + public void trustSession(AxolotlAddress counterpart) { XmppAxolotlSession session = sessions.get(counterpart); if(session != null) { session.trust(); } } - public boolean isTrustedSession(Jid counterpart) { + public boolean isTrustedSession(AxolotlAddress counterpart) { XmppAxolotlSession session = sessions.get(counterpart); return session != null && session.isTrusted(); } + private AxolotlAddress getAddressForJid(Jid jid) { + return new AxolotlAddress(jid.toString(), 0); + } + private Set findOwnSessions() { + AxolotlAddress ownAddress = getAddressForJid(account.getJid()); + Set ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values()); + return ownDeviceSessions; + } + + private Set findSessionsforContact(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + Set sessions = new HashSet<>(this.sessions.getAll(contactAddress).values()); + return sessions; + } + + private boolean hasAny(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + return sessions.hasAny(contactAddress); + } + + public int getOwnDeviceId() { + return ownDeviceId; + } + + private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { + } + + public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException { + XmppAxolotlMessage message = new XmppAxolotlMessage(contact, ownDeviceId, outgoingMessage); + createSessionsIfNeeded(contact); + Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); + + for(XmppAxolotlSession session : findSessionsforContact(contact)) { +// if(!session.isTrusted()) { + // TODO: handle this properly + // continue; + // } + message.addHeader(session.processSending(message.getInnerKey())); + } + Log.d(Config.LOGTAG, "Building axolotl own headers..."); + for(XmppAxolotlSession session : findOwnSessions()) { + // if(!session.isTrusted()) { + // TODO: handle this properly + // continue; + // } + message.addHeader(session.processSending(message.getInnerKey())); + } + + return message; + } + + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; + AxolotlAddress senderAddress = new AxolotlAddress(message.getContact().getJid().toBareJid().toString(), + message.getSenderDeviceId()); + + XmppAxolotlSession session = sessions.get(senderAddress); + if (session == null) { + Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message); + // TODO: handle this properly + session = new XmppAxolotlSession(axolotlStore, senderAddress); + + } + + for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { + if (header.getRecipientDeviceId() == ownDeviceId) { + Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing..."); + byte[] payloadKey = session.processReceiving(header); + if (payloadKey != null) { + Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message..."); + plaintextMessage = message.decrypt(session, payloadKey); + } + } + } + + return plaintextMessage; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java new file mode 100644 index 000000000..663b42b58 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/NoSessionsCreatedException.java @@ -0,0 +1,4 @@ +package eu.siacs.conversations.crypto.axolotl; + +public class NoSessionsCreatedException extends Throwable{ +} diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index b11670e4a..4b87fc5cb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -1,4 +1,184 @@ package eu.siacs.conversations.crypto.axolotl; +import android.util.Base64; + +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.security.InvalidKeyException; +import java.util.HashSet; +import java.util.Set; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.xml.Element; + public class XmppAxolotlMessage { + private byte[] innerKey; + private byte[] ciphertext; + private byte[] iv; + private final Set headers; + private final Contact contact; + private final int sourceDeviceId; + + public static class XmppAxolotlMessageHeader { + private final int recipientDeviceId; + private final byte[] content; + + public XmppAxolotlMessageHeader(int deviceId, byte[] content) { + this.recipientDeviceId = deviceId; + this.content = content; + } + + public XmppAxolotlMessageHeader(Element header) { + if("header".equals(header.getName())) { + this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid")); + this.content = Base64.decode(header.getContent(),Base64.DEFAULT); + } else { + throw new IllegalArgumentException("Argument not a

Element!"); + } + } + + public int getRecipientDeviceId() { + return recipientDeviceId; + } + + public byte[] getContents() { + return content; + } + + public Element toXml() { + Element headerElement = new Element("header"); + // TODO: generate XML + headerElement.setAttribute("rid", getRecipientDeviceId()); + headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT)); + return headerElement; + } + } + + public static class XmppAxolotlPlaintextMessage { + private final AxolotlService.XmppAxolotlSession session; + private final String plaintext; + + public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext) { + this.session = session; + this.plaintext = plaintext; + } + + public String getPlaintext() { + return plaintext; + } + } + + public XmppAxolotlMessage(Contact contact, Element axolotlMessage) { + this.contact = contact; + this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id")); + this.headers = new HashSet<>(); + for(Element child:axolotlMessage.getChildren()) { + switch(child.getName()) { + case "header": + headers.add(new XmppAxolotlMessageHeader(child)); + break; + case "message": + iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT); + ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT); + break; + default: + break; + } + } + } + + public XmppAxolotlMessage(Contact contact, int sourceDeviceId, String plaintext) { + this.contact = contact; + this.sourceDeviceId = sourceDeviceId; + this.headers = new HashSet<>(); + this.encrypt(plaintext); + } + + private void encrypt(String plaintext) { + try { + KeyGenerator generator = KeyGenerator.getInstance("AES"); + generator.init(128); + SecretKey secretKey = generator.generateKey(); + Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + this.innerKey = secretKey.getEncoded(); + this.iv = cipher.getIV(); + this.ciphertext = cipher.doFinal(plaintext.getBytes()); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | IllegalBlockSizeException | BadPaddingException e) { + + } + } + + public Contact getContact() { + return this.contact; + } + + public int getSenderDeviceId() { + return sourceDeviceId; + } + + public byte[] getCiphertext() { + return ciphertext; + } + + public Set getHeaders() { + return headers; + } + + public void addHeader(XmppAxolotlMessageHeader header) { + headers.add(header); + } + + public byte[] getInnerKey(){ + return innerKey; + } + + public byte[] getIV() { + return this.iv; + } + + public Element toXml() { + // TODO: generate outer XML, add in header XML + Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX); + message.setAttribute("id", sourceDeviceId); + for(XmppAxolotlMessageHeader header: headers) { + message.addChild(header.toXml()); + } + Element payload = message.addChild("message"); + payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT)); + payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT)); + return message; + } + + + public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key) { + XmppAxolotlPlaintextMessage plaintextMessage = null; + try { + + Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + + String plaintext = new String(cipher.doFinal(ciphertext)); + plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext); + + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException e) { + throw new AssertionError(e); + } + return plaintextMessage; + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 383125667..7a2dc3f76 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -20,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.OtrService; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -122,6 +123,7 @@ public class Account extends AbstractEntity { protected String avatar; protected boolean online = false; private OtrService mOtrService = null; + private AxolotlService axolotlService = null; private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; private String otrFingerprint; @@ -281,8 +283,13 @@ public class Account extends AbstractEntity { return values; } + public AxolotlService getAxolotlService() { + return axolotlService; + } + public void initAccountServices(final XmppConnectionService context) { this.mOtrService = new OtrService(context, this); + this.axolotlService = new AxolotlService(this, context); } public OtrService getOtrService() { diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 2f9b375df..45b55e49d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; +import android.util.Base64; import android.util.Log; import org.json.JSONArray; @@ -349,20 +350,38 @@ public class Contact implements ListItem, Blockable { } } - public List getTrustedAxolotlIdentityKeys() { + public List getAxolotlIdentityKeys() { synchronized (this.keys) { JSONArray serializedKeyItems = this.keys.optJSONArray("axolotl_identity_key"); List identityKeys = new ArrayList<>(); + List toDelete = new ArrayList<>(); if(serializedKeyItems != null) { for(int i = 0; i= 15) { + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME); + db.execSQL(CREATE_SESSIONS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME); + db.execSQL(CREATE_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); + db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + } } public static synchronized DatabaseBackend getInstance(Context context) { @@ -547,7 +561,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { if(cursor.getCount() != 0) { cursor.moveToFirst(); try { - session = new SessionRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()); + session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e) { throw new AssertionError(e); } @@ -590,7 +604,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName()); values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); - values.put(AxolotlService.SQLiteAxolotlStore.KEY, session.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT)); values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values); } @@ -616,6 +630,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { args); } + public boolean isTrustedSession(Account account, AxolotlAddress contact) { + boolean trusted = false; + Cursor cursor = getCursorForSession(account, contact); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + trusted = cursor.getInt(cursor.getColumnIndex( + AxolotlService.SQLiteAxolotlStore.TRUSTED)) > 0; + } + cursor.close(); + return trusted; + } + + public void setTrustedSession(Account account, AxolotlAddress contact, boolean trusted) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName()); + values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); + values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trusted?1:0); + db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values); + } + private Cursor getCursorForPreKey(Account account, int preKeyId) { SQLiteDatabase db = this.getReadableDatabase(); String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; @@ -636,7 +672,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { if(cursor.getCount() != 0) { cursor.moveToFirst(); try { - record = new PreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()); + record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e ) { throw new AssertionError(e); } @@ -656,7 +692,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); - values.put(AxolotlService.SQLiteAxolotlStore.KEY, record.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); db.insert(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, null, values); } @@ -685,11 +721,11 @@ public class DatabaseBackend extends SQLiteOpenHelper { public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) { SignedPreKeyRecord record = null; - Cursor cursor = getCursorForPreKey(account, signedPreKeyId); + Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId); if(cursor.getCount() != 0) { cursor.moveToFirst(); try { - record = new SignedPreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()); + record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e ) { throw new AssertionError(e); } @@ -711,7 +747,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { while(cursor.moveToNext()) { try { - prekeys.add(new SignedPreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes())); + prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)), Base64.DEFAULT))); } catch (IOException ignored) { } } @@ -729,7 +765,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.ID, record.getId()); - values.put(AxolotlService.SQLiteAxolotlStore.KEY, record.serialize()); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT)); values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); db.insert(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index f5c54adf5..bed9267bb 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -52,6 +52,7 @@ import de.duenndns.ssl.MemorizingTrustManager; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.crypto.axolotl.NoSessionsCreatedException; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Bookmark; @@ -273,7 +274,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } syncDirtyContacts(account); - scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode()); + account.getAxolotlService().publishOwnDeviceIdIfNeeded(); + account.getAxolotlService().publishBundleIfNeeded(); + account.getAxolotlService().publishPreKeysIfNeeded(); + + scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); } else if (account.getStatus() == Account.State.OFFLINE) { resetSendingToWaiting(account); if (!account.isOptionSet(Account.OPTION_DISABLED)) { From 77619b55e47aafbe6b5a530b33ce8b6a77a615dd Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 25 Jun 2015 16:58:24 +0200 Subject: [PATCH 009/166] Added PEP and message protocol layers Can now fetch/retrieve from PEP, as well as encode/decode messages --- .../crypto/axolotl/AxolotlService.java | 208 ++++++++++++++++++ .../generator/AbstractGenerator.java | 4 +- .../conversations/generator/IqGenerator.java | 70 ++++++ .../generator/MessageGenerator.java | 21 ++ .../siacs/conversations/parser/IqParser.java | 158 ++++++++++++- .../conversations/parser/MessageParser.java | 46 +++- .../eu/siacs/conversations/xml/Element.java | 5 + .../xmpp/stanzas/MessagePacket.java | 5 + 8 files changed, 509 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index eae7a9abc..865f903ae 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -41,16 +41,26 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class AxolotlService { + public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; + public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; + public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys"; + public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle"; + private final Account account; private final XmppConnectionService mXmppConnectionService; private final SQLiteAxolotlStore axolotlStore; private final SessionMap sessions; + private final BundleMap bundleCache; private int ownDeviceId; public static class SQLiteAxolotlStore implements AxolotlStore { @@ -571,6 +581,8 @@ public class AxolotlService { } + private static class BundleMap extends AxolotlAddressMap { + } public AxolotlService(Account account, XmppConnectionService connectionService) { @@ -578,6 +590,7 @@ public class AxolotlService { this.account = account; this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); this.sessions = new SessionMap(axolotlStore, account); + this.bundleCache = new BundleMap(); this.ownDeviceId = axolotlStore.getLocalRegistrationId(); } @@ -618,7 +631,202 @@ public class AxolotlService { return ownDeviceId; } + public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) { + final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId); + if (sessions.get(address) != null) { + return; + } + + synchronized (bundleCache) { + PreKeyBundle bundle = bundleCache.get(address); + if (bundle == null) { + bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null); + bundleCache.put(address, bundle); + } + + if(bundle.getPreKey() == null) { + Log.d(Config.LOGTAG, "No preKey in cache, fetching..."); + IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId); + mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized (bundleCache) { + Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final PreKeyBundle bundle = bundleCache.get(address); + final List preKeyBundleList = parser.preKeys(packet); + if (preKeyBundleList.isEmpty()) { + Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); + return; + } + Random random = new Random(); + final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (bundle == null || newBundle == null) { + //should never happen + return; + } + + final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), + bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(), + bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + + bundleCache.put(address, mergedBundle); + } + } + }); + } + if(bundle.getIdentityKey() == null) { + Log.d(Config.LOGTAG, "No bundle in cache, fetching..."); + IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId); + mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized (bundleCache) { + Log.d(Config.LOGTAG, "Received bundle IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final PreKeyBundle bundle = bundleCache.get(address); + final PreKeyBundle newBundle = parser.bundle(packet); + if( bundle == null || newBundle == null ) { + Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet); + //should never happen + return; + } + + final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), + bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(), + newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(), + newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey()); + + axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey()); + bundleCache.put(address, mergedBundle); + } + } + }); + } + } + } + + public void publishOwnDeviceIdIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element item = mXmppConnectionService.getIqParser().getItem(packet); + List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + if(deviceIds == null) { + deviceIds = new ArrayList<>(); + } + if(!deviceIds.contains(getOwnDeviceId())) { + Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing..."); + deviceIds.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + } + }); + } + } + }); + } + + public void publishBundleIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); + if(bundle == null) { + Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing..."); + int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); + try { + SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey( + axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle( + signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), + ownDeviceId); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + Log.d(Config.LOGTAG, "Published bundle, got: " + packet); + } + }); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + } + } + } + }); + } + + public void publishPreKeysIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + if(keys == null || keys.isEmpty()) { + Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing..."); + List preKeyRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId(), 100); + for(PreKeyRecord record : preKeyRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys( + preKeyRecords, ownDeviceId); + + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, "Published prekeys, got: " + packet); + // TODO: implement this! + } + }); + } + } + }); + } + + + public boolean isContactAxolotlCapable(Contact contact) { + AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); + return sessions.hasAny(address) || bundleCache.hasAny(address); + } + + public void initiateSynchronousSession(Contact contact) { + + } + private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { + Log.d(Config.LOGTAG, "Creating axolotl sessions if needed..."); + AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); + for(Integer deviceId: bundleCache.getAll(address).keySet()) { + Log.d(Config.LOGTAG, "Processing device ID: " + deviceId); + AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId); + if(sessions.get(remoteAddress) == null) { + Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId); + SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress); + try { + builder.process(bundleCache.get(remoteAddress)); + XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress); + sessions.put(remoteAddress, session); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage()); + } catch (UntrustedIdentityException e) { + Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage()); + } + } else { + Log.d(Config.LOGTAG, "Already have session for " + deviceId); + } + } + if(!this.hasAny(contact)) { + Log.e(Config.LOGTAG, "No Axolotl sessions available!"); + throw new NoSessionsCreatedException(); // FIXME: proper error handling + } } public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException { diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 79626511a..69bb18036 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Locale; import java.util.TimeZone; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.PhoneHelper; @@ -28,7 +29,8 @@ public abstract class AbstractGenerator { "urn:xmpp:avatar:metadata+notify", "urn:xmpp:ping", "jabber:iq:version", - "http://jabber.org/protocol/chatstates"}; + "http://jabber.org/protocol/chatstates", + AxolotlService.PEP_DEVICE_LIST+"+notify"}; private final String[] MESSAGE_CONFIRMATION_FEATURES = { "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 47915e3fb..6daadc2af 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -1,9 +1,17 @@ package eu.siacs.conversations.generator; +import android.util.Base64; + +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; + import java.util.ArrayList; import java.util.List; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.DownloadableFile; @@ -115,6 +123,68 @@ public class IqGenerator extends AbstractGenerator { return packet; } + public IqPacket retrieveDeviceIds(final Jid to) { + final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null); + if(to != null) { + packet.setTo(to); + } + return packet; + } + + public IqPacket retrieveBundleForDevice(final Jid to, final int deviceid) { + final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLE+":"+deviceid, null); + if(to != null) { + packet.setTo(to); + } + return packet; + } + + public IqPacket retrievePreKeysForDevice(final Jid to, final int deviceId) { + final IqPacket packet = retrieve(AxolotlService.PEP_PREKEYS+":"+deviceId, null); + if(to != null) { + packet.setTo(to); + } + return packet; + } + + public IqPacket publishDeviceIds(final List ids) { + final Element item = new Element("item"); + final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); + for(Integer id:ids) { + final Element device = new Element("device"); + device.setAttribute("id", id); + list.addChild(device); + } + return publish(AxolotlService.PEP_DEVICE_LIST, item); + } + + public IqPacket publishBundle(final SignedPreKeyRecord signedPreKeyRecord, IdentityKey identityKey, final int deviceId) { + final Element item = new Element("item"); + final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); + final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); + signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId()); + ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey(); + signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT)); + final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature"); + signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT)); + final Element identityKeyElement = bundle.addChild("identityKey"); + identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); + + return publish(AxolotlService.PEP_BUNDLE+":"+deviceId, item); + } + + public IqPacket publishPreKeys(final List prekeyList, final int deviceId) { + final Element item = new Element("item"); + final Element prekeys = item.addChild("prekeys", AxolotlService.PEP_PREFIX); + for(PreKeyRecord preKeyRecord:prekeyList) { + final Element prekey = prekeys.addChild("preKeyPublic"); + prekey.setAttribute("preKeyId", preKeyRecord.getId()); + prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); + } + + return publish(AxolotlService.PEP_PREKEYS+":"+deviceId, item); + } + public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); final Element query = packet.query("urn:xmpp:mam:0"); diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index bc1148d9a..e6032e0cb 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.generator; +import android.util.Log; + import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; @@ -7,6 +9,11 @@ import java.util.TimeZone; import net.java.otr4j.OtrException; import net.java.otr4j.session.Session; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.NoSessionsCreatedException; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; @@ -59,6 +66,20 @@ public class MessageGenerator extends AbstractGenerator { delay.setAttribute("stamp", mDateFormat.format(date)); } + public MessagePacket generateAxolotlChat(Message message) throws NoSessionsCreatedException{ + return generateAxolotlChat(message, false); + } + + public MessagePacket generateAxolotlChat(Message message, boolean addDelay) throws NoSessionsCreatedException{ + MessagePacket packet = preparePacket(message, addDelay); + AxolotlService service = message.getConversation().getAccount().getAxolotlService(); + Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing..."); + XmppAxolotlMessage axolotlMessage = service.processSending(message.getContact(), + message.getBody()); + packet.setAxolotlMessage(axolotlMessage.toXml()); + return packet; + } + public MessagePacket generateOtrChat(Message message) { return generateOtrChat(message, false); } diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 6039d3956..00f968858 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -1,9 +1,19 @@ package eu.siacs.conversations.parser; +import android.util.Base64; import android.util.Log; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.ecc.Curve; +import org.whispersystems.libaxolotl.ecc.ECPublicKey; +import org.whispersystems.libaxolotl.state.PreKeyBundle; + import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; @@ -60,7 +70,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { public String avatarData(final IqPacket packet) { final Element pubsub = packet.findChild("pubsub", - "http://jabber.org/protocol/pubsub"); + "http://jabber.org/protocol/pubsub"); if (pubsub == null) { return null; } @@ -71,6 +81,152 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return super.avatarData(items); } + public Element getItem(final IqPacket packet) { + final Element pubsub = packet.findChild("pubsub", + "http://jabber.org/protocol/pubsub"); + if (pubsub == null) { + return null; + } + final Element items = pubsub.findChild("items"); + if (items == null) { + return null; + } + return items.findChild("item"); + } + + public List deviceIds(final Element item) { + List deviceIds = new ArrayList<>(); + if (item == null) { + return null; + } + final Element list = item.findChild("list"); + if(list == null) { + return null; + } + for(Element device : list.getChildren()) { + if(!device.getName().equals("device")) { + continue; + } + try { + Integer id = Integer.valueOf(device.getAttribute("id")); + deviceIds.add(id); + } catch (NumberFormatException e) { + Log.e(Config.LOGTAG, "Encountered nvalid node in PEP:" + device.toString() + + ", skipping..."); + continue; + } + } + return deviceIds; + } + + public Integer signedPreKeyId(final Element bundle) { + final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); + if(signedPreKeyPublic == null) { + return null; + } + return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId")); + } + + public ECPublicKey signedPreKeyPublic(final Element bundle) { + ECPublicKey publicKey = null; + final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic"); + if(signedPreKeyPublic == null) { + return null; + } + try { + publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, "Invalid signedPreKeyPublic in PEP: " + e.getMessage()); + } + return publicKey; + } + + public byte[] signedPreKeySignature(final Element bundle) { + final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature"); + if(signedPreKeySignature == null) { + return null; + } + return Base64.decode(signedPreKeySignature.getContent(),Base64.DEFAULT); + } + + public IdentityKey identityKey(final Element bundle) { + IdentityKey identityKey = null; + final Element identityKeyElement = bundle.findChild("identityKey"); + if(identityKeyElement == null) { + return null; + } + try { + identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG,"Invalid identityKey in PEP: "+e.getMessage()); + } + return identityKey; + } + + public Map preKeyPublics(final IqPacket packet) { + Map preKeyRecords = new HashMap<>(); + Element prekeysItem = getItem(packet); + if (prekeysItem == null) { + Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + return null; + } + final Element prekeysElement = prekeysItem.findChild("prekeys"); + if(prekeysElement == null) { + Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + return null; + } + for(Element preKeyPublicElement : prekeysElement.getChildren()) { + if(!preKeyPublicElement.getName().equals("preKeyPublic")){ + Log.d(Config.LOGTAG, "Encountered unexpected tag in prekeys list: " + preKeyPublicElement); + continue; + } + Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); + try { + ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0); + preKeyRecords.put(preKeyId, preKeyPublic); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, "Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping..."); + continue; + } + } + return preKeyRecords; + } + + public PreKeyBundle bundle(final IqPacket bundle) { + Element bundleItem = getItem(bundle); + if(bundleItem == null) { + return null; + } + final Element bundleElement = bundleItem.findChild("bundle"); + if(bundleElement == null) { + return null; + } + ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); + Integer signedPreKeyId = signedPreKeyId(bundleElement); + byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); + IdentityKey identityKey = identityKey(bundleElement); + if(signedPreKeyPublic == null || identityKey == null) { + return null; + } + + return new PreKeyBundle(0, 0, 0, null, + signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); + } + + public List preKeys(final IqPacket preKeys) { + List bundles = new ArrayList<>(); + Map preKeyPublics = preKeyPublics(preKeys); + if ( preKeyPublics != null) { + for (Integer preKeyId : preKeyPublics.keySet()) { + ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); + bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, + 0, null, null, null)); + } + } + + return bundles; + } + @Override public void onIqPacketReceived(final Account account, final IqPacket packet) { if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) { diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index d46ff1950..bf4f3ad4b 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -6,7 +6,11 @@ import android.util.Pair; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; +import java.util.List; + import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -94,6 +98,18 @@ public class MessageParser extends AbstractParser implements } } + private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation) { + Message finishedMessage = null; + AxolotlService service = conversation.getAccount().getAxolotlService(); + XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(conversation.getContact(), axolotlMessage); + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); + if(plaintextMessage != null) { + finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED); + } + + return finishedMessage; + } + private class Invite { Jid jid; String password; @@ -170,6 +186,18 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateAccountUi(); } + } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { + Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing..."); + Element item = items.findChild("item"); + List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + AxolotlService axolotlService = account.getAxolotlService(); + if(account.getJid().toBareJid().equals(from)) { + } else { + Contact contact = account.getRoster().getContact(from); + for (Integer deviceId : deviceIds) { + axolotlService.fetchBundleIfNeeded(contact, deviceId); + } + } } } @@ -232,8 +260,9 @@ public class MessageParser extends AbstractParser implements timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis()); } final String body = packet.getBody(); - final String encrypted = packet.findChildContent("x", "jabber:x:encrypted"); final Element mucUserElement = packet.findChild("x","http://jabber.org/protocol/muc#user"); + final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); + final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX); int status; final Jid counterpart; final Jid to = packet.getTo(); @@ -261,11 +290,11 @@ public class MessageParser extends AbstractParser implements return; } - if (extractChatState(mXmppConnectionService.find(account,from), packet)) { + if (extractChatState(mXmppConnectionService.find(account, from), packet)) { mXmppConnectionService.updateConversationUi(); } - if ((body != null || encrypted != null) && !isMucStatusMessage) { + if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) { Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat); if (isTypeGroupChat) { if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { @@ -294,9 +323,14 @@ public class MessageParser extends AbstractParser implements } else { message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } - } else if (encrypted != null) { - message = new Message(conversation, encrypted, Message.ENCRYPTION_PGP, status); - } else { + } else if (pgpEncrypted != null) { + message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); + } else if (axolotlEncrypted != null) { + message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation); + if (message == null) { + return; + } + } else { message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } message.setCounterpart(counterpart); diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 32657c66c..dc5a68f6c 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -21,6 +21,11 @@ public class Element { this.name = name; } + public Element(String name, String xmlns) { + this.name = name; + this.setAttribute("xmlns", xmlns); + } + public Element addChild(Element child) { this.content = null; children.add(child); diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index e32811afe..628f0d934 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -29,6 +29,11 @@ public class MessagePacket extends AbstractStanza { this.children.add(0, body); } + public void setAxolotlMessage(Element axolotlMessage) { + this.children.remove(findChild("body")); + this.children.add(0, axolotlMessage); + } + public void setType(int type) { switch (type) { case TYPE_CHAT: From 065519d3f39f0a7b925157b50e4435957f63d8ae Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 25 Jun 2015 17:01:42 +0200 Subject: [PATCH 010/166] Added axolotl activation code to UI --- .../services/XmppConnectionService.java | 9 ++++++++ .../ui/ContactDetailsActivity.java | 20 ++++++++++++++++ .../ui/ConversationActivity.java | 18 +++++++++++++++ .../ui/ConversationFragment.java | 10 ++++++++ .../siacs/conversations/ui/XmppActivity.java | 23 +++++++++++++++++++ 5 files changed, 80 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index bed9267bb..1ebe3a033 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -758,6 +758,15 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } break; + case Message.ENCRYPTION_AXOLOTL: + try { + packet = mMessageGenerator.generateAxolotlChat(message); + Log.d(Config.LOGTAG, "Succeeded generating axolotl chat message!"); + } catch (NoSessionsCreatedException e) { + message.setStatus(Message.STATUS_WAITING); + } + break; + } if (packet != null) { if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) { diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index c190caede..fb04946a8 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -29,6 +29,7 @@ import android.widget.QuickContactBadge; import android.widget.TextView; import org.openintents.openpgp.util.OpenPgpUtils; +import org.whispersystems.libaxolotl.IdentityKey; import java.util.List; @@ -376,6 +377,25 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } }); } + for(final IdentityKey identityKey:contact.getAxolotlIdentityKeys()) { + hasKeys = true; + View view = inflater.inflate(R.layout.contact_key, keys, false); + TextView key = (TextView) view.findViewById(R.id.key); + TextView keyType = (TextView) view.findViewById(R.id.key_type); + ImageButton remove = (ImageButton) view + .findViewById(R.id.button_remove); + remove.setVisibility(View.VISIBLE); + keyType.setText("Axolotl Fingerprint"); + key.setText(identityKey.getFingerprint()); + keys.addView(view); + remove.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + //confirmToDeleteFingerprint(otrFingerprint); + } + }); + } if (contact.getPgpKeyId() != 0) { hasKeys = true; View view = inflater.inflate(R.layout.contact_key, keys, false); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 96abf65b0..ff1355c35 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -16,6 +16,7 @@ import android.os.Bundle; import android.provider.MediaStore; import android.support.v4.widget.SlidingPaneLayout; import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -34,6 +35,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; @@ -749,6 +751,17 @@ public class ConversationActivity extends XmppActivity showInstallPgpDialog(); } break; + case R.id.encryption_choice_axolotl: + Log.d(Config.LOGTAG, "Trying to enable axolotl..."); + if(conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { + Log.d(Config.LOGTAG, "Enabled axolotl for Contact " + conversation.getContact().getJid() ); + conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); + item.setChecked(true); + } else { + Log.d(Config.LOGTAG, "Contact " + conversation.getContact().getJid() + " not axolotl capable!"); + showAxolotlNoSessionsDialog(); + } + break; default: conversation.setNextEncryption(Message.ENCRYPTION_NONE); break; @@ -780,6 +793,11 @@ public class ConversationActivity extends XmppActivity case Message.ENCRYPTION_PGP: pgp.setChecked(true); break; + case Message.ENCRYPTION_AXOLOTL: + Log.d(Config.LOGTAG, "Axolotl confirmed. Setting menu item checked!"); + popup.getMenu().findItem(R.id.encryption_choice_axolotl) + .setChecked(true); + break; default: none.setChecked(true); break; diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index d254ece7b..ec50ea548 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -303,6 +303,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa sendOtrMessage(message); } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) { sendPgpMessage(message); + } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_AXOLOTL) { + sendAxolotlMessage(message); } else { sendPlainTextMessage(message); } @@ -1120,6 +1122,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa builder.create().show(); } + protected void sendAxolotlMessage(final Message message) { + final ConversationActivity activity = (ConversationActivity) getActivity(); + final XmppConnectionService xmppService = activity.xmppConnectionService; + //message.setCounterpart(conversation.getNextCounterpart()); + xmppService.sendMessage(message); + messageSent(); + } + protected void sendOtrMessage(final Message message) { final ConversationActivity activity = (ConversationActivity) getActivity(); final XmppConnectionService xmppService = activity.xmppConnectionService; diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 7c994c31a..eebeb040a 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -266,6 +266,29 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } + public void showAxolotlNoSessionsDialog() { + Builder builder = new AlertDialog.Builder(this); + builder.setTitle("No Sessions"); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage("Your contact is not Axolotl-capable!"); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setNeutralButton("Foo", + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + } + }); + builder.setPositiveButton("Bar", + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + } + }); + builder.create().show(); + } + abstract void onBackendConnected(); protected void registerListeners() { From 299bbdf27f0144e6eed99e70a3b2e46f9a3aa301 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 26 Jun 2015 15:41:02 +0200 Subject: [PATCH 011/166] Reformat code to use tabs This really sucks to do it like this. Sorry. :( --- .../crypto/axolotl/AxolotlService.java | 1612 ++++++++--------- .../crypto/axolotl/XmppAxolotlMessage.java | 270 +-- .../siacs/conversations/entities/Contact.java | 20 +- .../conversations/generator/IqGenerator.java | 36 +- .../generator/MessageGenerator.java | 22 +- .../siacs/conversations/parser/IqParser.java | 76 +- .../conversations/parser/MessageParser.java | 56 +- .../persistance/DatabaseBackend.java | 34 +- .../services/XmppConnectionService.java | 6 +- .../ui/ConversationActivity.java | 10 +- 10 files changed, 1071 insertions(+), 1071 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 865f903ae..5c4b34a46 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -51,832 +51,832 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class AxolotlService { - public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; - public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; - public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys"; - public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle"; + public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; + public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; + public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys"; + public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle"; - private final Account account; - private final XmppConnectionService mXmppConnectionService; - private final SQLiteAxolotlStore axolotlStore; - private final SessionMap sessions; - private final BundleMap bundleCache; - private int ownDeviceId; + private final Account account; + private final XmppConnectionService mXmppConnectionService; + private final SQLiteAxolotlStore axolotlStore; + private final SessionMap sessions; + private final BundleMap bundleCache; + private int ownDeviceId; - public static class SQLiteAxolotlStore implements AxolotlStore { + public static class SQLiteAxolotlStore implements AxolotlStore { - public static final String PREKEY_TABLENAME = "prekeys"; - public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; - public static final String SESSION_TABLENAME = "sessions"; - public static final String ACCOUNT = "account"; - public static final String DEVICE_ID = "device_id"; - public static final String ID = "id"; - public static final String KEY = "key"; - public static final String NAME = "name"; - public static final String TRUSTED = "trusted"; + public static final String PREKEY_TABLENAME = "prekeys"; + public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; + public static final String SESSION_TABLENAME = "sessions"; + public static final String ACCOUNT = "account"; + public static final String DEVICE_ID = "device_id"; + public static final String ID = "id"; + public static final String KEY = "key"; + public static final String NAME = "name"; + public static final String TRUSTED = "trusted"; - public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; - public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; - public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; + public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; + public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; + public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; - private final Account account; - private final XmppConnectionService mXmppConnectionService; + private final Account account; + private final XmppConnectionService mXmppConnectionService; - private final IdentityKeyPair identityKeyPair; - private final int localRegistrationId; - private int currentPreKeyId = 0; + private final IdentityKeyPair identityKeyPair; + private final int localRegistrationId; + private int currentPreKeyId = 0; - private static IdentityKeyPair generateIdentityKeyPair() { - Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair..."); - ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); - IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), - identityKeyPairKeys.getPrivateKey()); - return ownKey; - } + private static IdentityKeyPair generateIdentityKeyPair() { + Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair..."); + ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); + IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), + identityKeyPairKeys.getPrivateKey()); + return ownKey; + } - private static int generateRegistrationId() { - Log.d(Config.LOGTAG, "Generating axolotl registration ID..."); - int reg_id = KeyHelper.generateRegistrationId(false); - return reg_id; - } + private static int generateRegistrationId() { + Log.d(Config.LOGTAG, "Generating axolotl registration ID..."); + int reg_id = KeyHelper.generateRegistrationId(false); + return reg_id; + } - public SQLiteAxolotlStore(Account account, XmppConnectionService service) { - this.account = account; - this.mXmppConnectionService = service; - this.identityKeyPair = loadIdentityKeyPair(); - this.localRegistrationId = loadRegistrationId(); - this.currentPreKeyId = loadCurrentPreKeyId(); - for( SignedPreKeyRecord record:loadSignedPreKeys()) { - Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId()); - } - } + public SQLiteAxolotlStore(Account account, XmppConnectionService service) { + this.account = account; + this.mXmppConnectionService = service; + this.identityKeyPair = loadIdentityKeyPair(); + this.localRegistrationId = loadRegistrationId(); + this.currentPreKeyId = loadCurrentPreKeyId(); + for( SignedPreKeyRecord record:loadSignedPreKeys()) { + Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId()); + } + } - public int getCurrentPreKeyId() { - return currentPreKeyId; - } + public int getCurrentPreKeyId() { + return currentPreKeyId; + } - // -------------------------------------- - // IdentityKeyStore - // -------------------------------------- + // -------------------------------------- + // IdentityKeyStore + // -------------------------------------- - private IdentityKeyPair loadIdentityKeyPair() { - String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR); - IdentityKeyPair ownKey; - if( serializedKey != null ) { - try { - ownKey = new IdentityKeyPair(Base64.decode(serializedKey,Base64.DEFAULT)); - return ownKey; - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage()); + private IdentityKeyPair loadIdentityKeyPair() { + String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR); + IdentityKeyPair ownKey; + if( serializedKey != null ) { + try { + ownKey = new IdentityKeyPair(Base64.decode(serializedKey,Base64.DEFAULT)); + return ownKey; + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage()); // return null; - } - } //else { - Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid()); - ownKey = generateIdentityKeyPair(); - boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, Base64.encodeToString(ownKey.serialize(), Base64.DEFAULT)); - if(success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, "Failed to write new key to the database!"); - } - //} - return ownKey; - } - - private int loadRegistrationId() { - String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); - int reg_id; - if (regIdString != null) { - reg_id = Integer.valueOf(regIdString); - } else { - Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid()); - reg_id = generateRegistrationId(); - boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id); - if(success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, "Failed to write new key to the database!"); - } - } - return reg_id; - } - - private int loadCurrentPreKeyId() { - String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); - int reg_id; - if (regIdString != null) { - reg_id = Integer.valueOf(regIdString); - } else { - Log.d(Config.LOGTAG, "Could not retrieve current prekey id for account " + account.getJid()); - reg_id = 0; - } - return reg_id; - } - - - /** - * Get the local client's identity key pair. - * - * @return The local client's persistent identity key pair. - */ - @Override - public IdentityKeyPair getIdentityKeyPair() { - return identityKeyPair; - } - - /** - * Return the local client's registration ID. - *

- * Clients should maintain a registration ID, a random number - * between 1 and 16380 that's generated once at install time. - * - * @return the local client's registration ID. - */ - @Override - public int getLocalRegistrationId() { - return localRegistrationId; - } - - /** - * Save a remote client's identity key - *

- * Store a remote client's identity key as trusted. - * - * @param name The name of the remote client. - * @param identityKey The remote client's identity key. - */ - @Override - public void saveIdentity(String name, IdentityKey identityKey) { - try { - Jid contactJid = Jid.fromString(name); - Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); - if (conversation != null) { - conversation.getContact().addAxolotlIdentityKey(identityKey); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); - } - } catch (final InvalidJidException e) { - Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString()); - } - } - - /** - * Verify a remote client's identity key. - *

- * Determine whether a remote client's identity is trusted. Convention is - * that the TextSecure protocol is 'trust on first use.' This means that - * an identity key is considered 'trusted' if there is no entry for the recipient - * in the local store, or if it matches the saved key for a recipient in the local - * store. Only if it mismatches an entry in the local store is it considered - * 'untrusted.' - * - * @param name The name of the remote client. - * @param identityKey The identity key to verify. - * @return true if trusted, false if untrusted. - */ - @Override - public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - try { - Jid contactJid = Jid.fromString(name); - Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); - if (conversation != null) { - List trustedKeys = conversation.getContact().getAxolotlIdentityKeys(); - return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); - } else { - return false; - } - } catch (final InvalidJidException e) { - Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString()); - return false; - } - } - - // -------------------------------------- - // SessionStore - // -------------------------------------- - - /** - * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, - * or a new SessionRecord if one does not currently exist. - *

- * It is important that implementations return a copy of the current durable information. The - * returned SessionRecord may be modified, but those changes should not have an effect on the - * durable session state (what is returned by subsequent calls to this method) without the - * store method being called here first. - * - * @param address The name and device ID of the remote client. - * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or - * a new SessionRecord if one does not currently exist. - */ - @Override - public SessionRecord loadSession(AxolotlAddress address) { - SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); - return (session!=null)?session:new SessionRecord(); - } - - /** - * Returns all known devices with active sessions for a recipient - * - * @param name the name of the client. - * @return all known sub-devices with active sessions. - */ - @Override - public List getSubDeviceSessions(String name) { - return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, - new AxolotlAddress(name,0)); - } - - /** - * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @param record the current SessionRecord for the remote client. - */ - @Override - public void storeSession(AxolotlAddress address, SessionRecord record) { - mXmppConnectionService.databaseBackend.storeSession(account, address, record); - } - - /** - * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - * @return true if a {@link SessionRecord} exists, false otherwise. - */ - @Override - public boolean containsSession(AxolotlAddress address) { - return mXmppConnectionService.databaseBackend.containsSession(account, address); - } - - /** - * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. - * - * @param address the address of the remote client. - */ - @Override - public void deleteSession(AxolotlAddress address) { - mXmppConnectionService.databaseBackend.deleteSession(account, address); - } - - /** - * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. - * - * @param name the name of the remote client. - */ - @Override - public void deleteAllSessions(String name) { - mXmppConnectionService.databaseBackend.deleteAllSessions(account, - new AxolotlAddress(name, 0)); - } - - public boolean isTrustedSession(AxolotlAddress address) { - return mXmppConnectionService.databaseBackend.isTrustedSession(this.account, address); - } - - public void setTrustedSession(AxolotlAddress address, boolean trusted) { - mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address,trusted); - } - - // -------------------------------------- - // PreKeyStore - // -------------------------------------- - - /** - * Load a local PreKeyRecord. - * - * @param preKeyId the ID of the local PreKeyRecord. - * @return the corresponding PreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. - */ - @Override - public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { - PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); - if(record == null) { - throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); - } - return record; - } - - /** - * Store a local PreKeyRecord. - * - * @param preKeyId the ID of the PreKeyRecord to store. - * @param record the PreKeyRecord. - */ - @Override - public void storePreKey(int preKeyId, PreKeyRecord record) { - mXmppConnectionService.databaseBackend.storePreKey(account, record); - currentPreKeyId = preKeyId; - boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID,Integer.toString(preKeyId)); - if(success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!"); - } - } - - /** - * @param preKeyId A PreKeyRecord ID. - * @return true if the store has a record for the preKeyId, otherwise false. - */ - @Override - public boolean containsPreKey(int preKeyId) { - return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); - } - - /** - * Delete a PreKeyRecord from local storage. - * - * @param preKeyId The ID of the PreKeyRecord to remove. - */ - @Override - public void removePreKey(int preKeyId) { - mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); - } - - // -------------------------------------- - // SignedPreKeyStore - // -------------------------------------- - - /** - * Load a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the local SignedPreKeyRecord. - * @return the corresponding SignedPreKeyRecord. - * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. - */ - @Override - public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { - SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); - if(record == null) { - throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); - } - return record; - } - - /** - * Load all local SignedPreKeyRecords. - * - * @return All stored SignedPreKeyRecords. - */ - @Override - public List loadSignedPreKeys() { - return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); - } - - /** - * Store a local SignedPreKeyRecord. - * - * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. - * @param record the SignedPreKeyRecord. - */ - @Override - public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { - mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); - } - - /** - * @param signedPreKeyId A SignedPreKeyRecord ID. - * @return true if the store has a record for the signedPreKeyId, otherwise false. - */ - @Override - public boolean containsSignedPreKey(int signedPreKeyId) { - return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); - } - - /** - * Delete a SignedPreKeyRecord from local storage. - * - * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. - */ - @Override - public void removeSignedPreKey(int signedPreKeyId) { - mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); - } - } - - public static class XmppAxolotlSession { - private SessionCipher cipher; - private boolean isTrusted = false; - private SQLiteAxolotlStore sqLiteAxolotlStore; - private AxolotlAddress remoteAddress; - - public XmppAxolotlSession(SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { - this.cipher = new SessionCipher(store, remoteAddress); - this.remoteAddress = remoteAddress; - this.sqLiteAxolotlStore = store; - this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress); - } - - public void trust() { - sqLiteAxolotlStore.setTrustedSession(remoteAddress, true); - this.isTrusted = true; - } - - public boolean isTrusted() { - return this.isTrusted; - } - - public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { - byte[] plaintext = null; - try { - try { - PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); - Log.d(Config.LOGTAG,"PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); - plaintext = cipher.decrypt(message); - } catch (InvalidMessageException|InvalidVersionException e) { - WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); - plaintext = cipher.decrypt(message); - } catch (InvalidKeyException|InvalidKeyIdException| UntrustedIdentityException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); - } - } catch (LegacyMessageException|InvalidMessageException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); - } catch (DuplicateMessageException|NoSessionException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); - } - return plaintext; - } - - public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(byte[] outgoingMessage) { - CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); - XmppAxolotlMessage.XmppAxolotlMessageHeader header = - new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(), - ciphertextMessage.serialize()); - return header; - } - } - - private static class AxolotlAddressMap { - protected Map> map; - protected final Object MAP_LOCK = new Object(); - - public AxolotlAddressMap() { - this.map = new HashMap<>(); - } - - public void put(AxolotlAddress address, T value) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if (devices == null) { - devices = new HashMap<>(); - map.put(address.getName(), devices); - } - devices.put(address.getDeviceId(), value); - } - } - - public T get(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if(devices == null) { - return null; - } - return devices.get(address.getDeviceId()); - } - } - - public Map getAll(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - if(devices == null) { - return new HashMap<>(); - } - return devices; - } - } - - public boolean hasAny(AxolotlAddress address) { - synchronized (MAP_LOCK) { - Map devices = map.get(address.getName()); - return devices != null && !devices.isEmpty(); - } - } - - - } - - private static class SessionMap extends AxolotlAddressMap { - - public SessionMap(SQLiteAxolotlStore store, Account account) { - super(); - this.fillMap(store, account); - } - - private void fillMap(SQLiteAxolotlStore store, Account account) { - for(Contact contact:account.getRoster().getContacts()){ - Jid bareJid = contact.getJid().toBareJid(); - if(bareJid == null) { - continue; // FIXME: handle this? - } - String address = bareJid.toString(); - List deviceIDs = store.getSubDeviceSessions(address); - for(Integer deviceId:deviceIDs) { - AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); - this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress)); - } - } - } - - } - - private static class BundleMap extends AxolotlAddressMap { - - } - - public AxolotlService(Account account, XmppConnectionService connectionService) { - this.mXmppConnectionService = connectionService; - this.account = account; - this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); - this.sessions = new SessionMap(axolotlStore, account); - this.bundleCache = new BundleMap(); - this.ownDeviceId = axolotlStore.getLocalRegistrationId(); - } - - public void trustSession(AxolotlAddress counterpart) { - XmppAxolotlSession session = sessions.get(counterpart); - if(session != null) { - session.trust(); - } - } - - public boolean isTrustedSession(AxolotlAddress counterpart) { - XmppAxolotlSession session = sessions.get(counterpart); - return session != null && session.isTrusted(); - } - - private AxolotlAddress getAddressForJid(Jid jid) { - return new AxolotlAddress(jid.toString(), 0); - } - - private Set findOwnSessions() { - AxolotlAddress ownAddress = getAddressForJid(account.getJid()); - Set ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values()); - return ownDeviceSessions; - } - - private Set findSessionsforContact(Contact contact) { - AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); - Set sessions = new HashSet<>(this.sessions.getAll(contactAddress).values()); - return sessions; - } - - private boolean hasAny(Contact contact) { - AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); - return sessions.hasAny(contactAddress); - } - - public int getOwnDeviceId() { - return ownDeviceId; - } - - public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) { - final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId); - if (sessions.get(address) != null) { - return; - } - - synchronized (bundleCache) { - PreKeyBundle bundle = bundleCache.get(address); - if (bundle == null) { - bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null); - bundleCache.put(address, bundle); - } - - if(bundle.getPreKey() == null) { - Log.d(Config.LOGTAG, "No preKey in cache, fetching..."); - IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId); - mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized (bundleCache) { - Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final PreKeyBundle bundle = bundleCache.get(address); - final List preKeyBundleList = parser.preKeys(packet); - if (preKeyBundleList.isEmpty()) { - Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); - return; - } - Random random = new Random(); - final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); - if (bundle == null || newBundle == null) { - //should never happen - return; - } - - final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), - bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(), - bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), - bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); - - bundleCache.put(address, mergedBundle); - } - } - }); - } - if(bundle.getIdentityKey() == null) { - Log.d(Config.LOGTAG, "No bundle in cache, fetching..."); - IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId); - mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized (bundleCache) { - Log.d(Config.LOGTAG, "Received bundle IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final PreKeyBundle bundle = bundleCache.get(address); - final PreKeyBundle newBundle = parser.bundle(packet); - if( bundle == null || newBundle == null ) { - Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet); - //should never happen - return; - } - - final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), - bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(), - newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(), - newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey()); - - axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey()); - bundleCache.put(address, mergedBundle); - } - } - }); - } - } - } - - public void publishOwnDeviceIdIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Element item = mXmppConnectionService.getIqParser().getItem(packet); - List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - if(deviceIds == null) { - deviceIds = new ArrayList<>(); - } - if(!deviceIds.contains(getOwnDeviceId())) { - Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing..."); - deviceIds.add(getOwnDeviceId()); - IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - // TODO: implement this! - } - }); - } - } - }); - } - - public void publishBundleIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); - if(bundle == null) { - Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing..."); - int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); - try { - SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey( - axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1); - axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle( - signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - ownDeviceId); - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - // TODO: implement this! - Log.d(Config.LOGTAG, "Published bundle, got: " + packet); - } - }); - } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); - } - } - } - }); - } - - public void publishPreKeysIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - if(keys == null || keys.isEmpty()) { - Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing..."); - List preKeyRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId(), 100); - for(PreKeyRecord record : preKeyRecords) { - axolotlStore.storePreKey(record.getId(), record); - } - IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys( - preKeyRecords, ownDeviceId); - - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d(Config.LOGTAG, "Published prekeys, got: " + packet); - // TODO: implement this! - } - }); - } - } - }); - } - - - public boolean isContactAxolotlCapable(Contact contact) { - AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); - return sessions.hasAny(address) || bundleCache.hasAny(address); - } - - public void initiateSynchronousSession(Contact contact) { - - } - - private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { - Log.d(Config.LOGTAG, "Creating axolotl sessions if needed..."); - AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); - for(Integer deviceId: bundleCache.getAll(address).keySet()) { - Log.d(Config.LOGTAG, "Processing device ID: " + deviceId); - AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId); - if(sessions.get(remoteAddress) == null) { - Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId); - SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress); - try { - builder.process(bundleCache.get(remoteAddress)); - XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress); - sessions.put(remoteAddress, session); - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage()); - } catch (UntrustedIdentityException e) { - Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage()); - } - } else { - Log.d(Config.LOGTAG, "Already have session for " + deviceId); - } - } - if(!this.hasAny(contact)) { - Log.e(Config.LOGTAG, "No Axolotl sessions available!"); - throw new NoSessionsCreatedException(); // FIXME: proper error handling - } - } - - public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException { - XmppAxolotlMessage message = new XmppAxolotlMessage(contact, ownDeviceId, outgoingMessage); - createSessionsIfNeeded(contact); - Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); - - for(XmppAxolotlSession session : findSessionsforContact(contact)) { + } + } //else { + Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid()); + ownKey = generateIdentityKeyPair(); + boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, Base64.encodeToString(ownKey.serialize(), Base64.DEFAULT)); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new key to the database!"); + } + //} + return ownKey; + } + + private int loadRegistrationId() { + String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); + int reg_id; + if (regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid()); + reg_id = generateRegistrationId(); + boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new key to the database!"); + } + } + return reg_id; + } + + private int loadCurrentPreKeyId() { + String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID); + int reg_id; + if (regIdString != null) { + reg_id = Integer.valueOf(regIdString); + } else { + Log.d(Config.LOGTAG, "Could not retrieve current prekey id for account " + account.getJid()); + reg_id = 0; + } + return reg_id; + } + + + /** + * Get the local client's identity key pair. + * + * @return The local client's persistent identity key pair. + */ + @Override + public IdentityKeyPair getIdentityKeyPair() { + return identityKeyPair; + } + + /** + * Return the local client's registration ID. + *

+ * Clients should maintain a registration ID, a random number + * between 1 and 16380 that's generated once at install time. + * + * @return the local client's registration ID. + */ + @Override + public int getLocalRegistrationId() { + return localRegistrationId; + } + + /** + * Save a remote client's identity key + *

+ * Store a remote client's identity key as trusted. + * + * @param name The name of the remote client. + * @param identityKey The remote client's identity key. + */ + @Override + public void saveIdentity(String name, IdentityKey identityKey) { + try { + Jid contactJid = Jid.fromString(name); + Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); + if (conversation != null) { + conversation.getContact().addAxolotlIdentityKey(identityKey); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); + } + } catch (final InvalidJidException e) { + Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString()); + } + } + + /** + * Verify a remote client's identity key. + *

+ * Determine whether a remote client's identity is trusted. Convention is + * that the TextSecure protocol is 'trust on first use.' This means that + * an identity key is considered 'trusted' if there is no entry for the recipient + * in the local store, or if it matches the saved key for a recipient in the local + * store. Only if it mismatches an entry in the local store is it considered + * 'untrusted.' + * + * @param name The name of the remote client. + * @param identityKey The identity key to verify. + * @return true if trusted, false if untrusted. + */ + @Override + public boolean isTrustedIdentity(String name, IdentityKey identityKey) { + try { + Jid contactJid = Jid.fromString(name); + Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); + if (conversation != null) { + List trustedKeys = conversation.getContact().getAxolotlIdentityKeys(); + return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); + } else { + return false; + } + } catch (final InvalidJidException e) { + Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString()); + return false; + } + } + + // -------------------------------------- + // SessionStore + // -------------------------------------- + + /** + * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple, + * or a new SessionRecord if one does not currently exist. + *

+ * It is important that implementations return a copy of the current durable information. The + * returned SessionRecord may be modified, but those changes should not have an effect on the + * durable session state (what is returned by subsequent calls to this method) without the + * store method being called here first. + * + * @param address The name and device ID of the remote client. + * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or + * a new SessionRecord if one does not currently exist. + */ + @Override + public SessionRecord loadSession(AxolotlAddress address) { + SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); + return (session!=null)?session:new SessionRecord(); + } + + /** + * Returns all known devices with active sessions for a recipient + * + * @param name the name of the client. + * @return all known sub-devices with active sessions. + */ + @Override + public List getSubDeviceSessions(String name) { + return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, + new AxolotlAddress(name,0)); + } + + /** + * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @param record the current SessionRecord for the remote client. + */ + @Override + public void storeSession(AxolotlAddress address, SessionRecord record) { + mXmppConnectionService.databaseBackend.storeSession(account, address, record); + } + + /** + * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + * @return true if a {@link SessionRecord} exists, false otherwise. + */ + @Override + public boolean containsSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.containsSession(account, address); + } + + /** + * Remove a {@link SessionRecord} for a recipientId + deviceId tuple. + * + * @param address the address of the remote client. + */ + @Override + public void deleteSession(AxolotlAddress address) { + mXmppConnectionService.databaseBackend.deleteSession(account, address); + } + + /** + * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId. + * + * @param name the name of the remote client. + */ + @Override + public void deleteAllSessions(String name) { + mXmppConnectionService.databaseBackend.deleteAllSessions(account, + new AxolotlAddress(name, 0)); + } + + public boolean isTrustedSession(AxolotlAddress address) { + return mXmppConnectionService.databaseBackend.isTrustedSession(this.account, address); + } + + public void setTrustedSession(AxolotlAddress address, boolean trusted) { + mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address,trusted); + } + + // -------------------------------------- + // PreKeyStore + // -------------------------------------- + + /** + * Load a local PreKeyRecord. + * + * @param preKeyId the ID of the local PreKeyRecord. + * @return the corresponding PreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord. + */ + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); + if(record == null) { + throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); + } + return record; + } + + /** + * Store a local PreKeyRecord. + * + * @param preKeyId the ID of the PreKeyRecord to store. + * @param record the PreKeyRecord. + */ + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + mXmppConnectionService.databaseBackend.storePreKey(account, record); + currentPreKeyId = preKeyId; + boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID,Integer.toString(preKeyId)); + if(success) { + mXmppConnectionService.databaseBackend.updateAccount(account); + } else { + Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!"); + } + } + + /** + * @param preKeyId A PreKeyRecord ID. + * @return true if the store has a record for the preKeyId, otherwise false. + */ + @Override + public boolean containsPreKey(int preKeyId) { + return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId); + } + + /** + * Delete a PreKeyRecord from local storage. + * + * @param preKeyId The ID of the PreKeyRecord to remove. + */ + @Override + public void removePreKey(int preKeyId) { + mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId); + } + + // -------------------------------------- + // SignedPreKeyStore + // -------------------------------------- + + /** + * Load a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the local SignedPreKeyRecord. + * @return the corresponding SignedPreKeyRecord. + * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord. + */ + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); + if(record == null) { + throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); + } + return record; + } + + /** + * Load all local SignedPreKeyRecords. + * + * @return All stored SignedPreKeyRecords. + */ + @Override + public List loadSignedPreKeys() { + return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account); + } + + /** + * Store a local SignedPreKeyRecord. + * + * @param signedPreKeyId the ID of the SignedPreKeyRecord to store. + * @param record the SignedPreKeyRecord. + */ + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record); + } + + /** + * @param signedPreKeyId A SignedPreKeyRecord ID. + * @return true if the store has a record for the signedPreKeyId, otherwise false. + */ + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId); + } + + /** + * Delete a SignedPreKeyRecord from local storage. + * + * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove. + */ + @Override + public void removeSignedPreKey(int signedPreKeyId) { + mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId); + } + } + + public static class XmppAxolotlSession { + private SessionCipher cipher; + private boolean isTrusted = false; + private SQLiteAxolotlStore sqLiteAxolotlStore; + private AxolotlAddress remoteAddress; + + public XmppAxolotlSession(SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { + this.cipher = new SessionCipher(store, remoteAddress); + this.remoteAddress = remoteAddress; + this.sqLiteAxolotlStore = store; + this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress); + } + + public void trust() { + sqLiteAxolotlStore.setTrustedSession(remoteAddress, true); + this.isTrusted = true; + } + + public boolean isTrusted() { + return this.isTrusted; + } + + public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { + byte[] plaintext = null; + try { + try { + PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); + Log.d(Config.LOGTAG,"PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + plaintext = cipher.decrypt(message); + } catch (InvalidMessageException|InvalidVersionException e) { + WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); + plaintext = cipher.decrypt(message); + } catch (InvalidKeyException|InvalidKeyIdException| UntrustedIdentityException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } + } catch (LegacyMessageException|InvalidMessageException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } catch (DuplicateMessageException|NoSessionException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } + return plaintext; + } + + public XmppAxolotlMessage.XmppAxolotlMessageHeader processSending(byte[] outgoingMessage) { + CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage); + XmppAxolotlMessage.XmppAxolotlMessageHeader header = + new XmppAxolotlMessage.XmppAxolotlMessageHeader(remoteAddress.getDeviceId(), + ciphertextMessage.serialize()); + return header; + } + } + + private static class AxolotlAddressMap { + protected Map> map; + protected final Object MAP_LOCK = new Object(); + + public AxolotlAddressMap() { + this.map = new HashMap<>(); + } + + public void put(AxolotlAddress address, T value) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if (devices == null) { + devices = new HashMap<>(); + map.put(address.getName(), devices); + } + devices.put(address.getDeviceId(), value); + } + } + + public T get(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if(devices == null) { + return null; + } + return devices.get(address.getDeviceId()); + } + } + + public Map getAll(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + if(devices == null) { + return new HashMap<>(); + } + return devices; + } + } + + public boolean hasAny(AxolotlAddress address) { + synchronized (MAP_LOCK) { + Map devices = map.get(address.getName()); + return devices != null && !devices.isEmpty(); + } + } + + + } + + private static class SessionMap extends AxolotlAddressMap { + + public SessionMap(SQLiteAxolotlStore store, Account account) { + super(); + this.fillMap(store, account); + } + + private void fillMap(SQLiteAxolotlStore store, Account account) { + for(Contact contact:account.getRoster().getContacts()){ + Jid bareJid = contact.getJid().toBareJid(); + if(bareJid == null) { + continue; // FIXME: handle this? + } + String address = bareJid.toString(); + List deviceIDs = store.getSubDeviceSessions(address); + for(Integer deviceId:deviceIDs) { + AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); + this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress)); + } + } + } + + } + + private static class BundleMap extends AxolotlAddressMap { + + } + + public AxolotlService(Account account, XmppConnectionService connectionService) { + this.mXmppConnectionService = connectionService; + this.account = account; + this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); + this.sessions = new SessionMap(axolotlStore, account); + this.bundleCache = new BundleMap(); + this.ownDeviceId = axolotlStore.getLocalRegistrationId(); + } + + public void trustSession(AxolotlAddress counterpart) { + XmppAxolotlSession session = sessions.get(counterpart); + if(session != null) { + session.trust(); + } + } + + public boolean isTrustedSession(AxolotlAddress counterpart) { + XmppAxolotlSession session = sessions.get(counterpart); + return session != null && session.isTrusted(); + } + + private AxolotlAddress getAddressForJid(Jid jid) { + return new AxolotlAddress(jid.toString(), 0); + } + + private Set findOwnSessions() { + AxolotlAddress ownAddress = getAddressForJid(account.getJid()); + Set ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values()); + return ownDeviceSessions; + } + + private Set findSessionsforContact(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + Set sessions = new HashSet<>(this.sessions.getAll(contactAddress).values()); + return sessions; + } + + private boolean hasAny(Contact contact) { + AxolotlAddress contactAddress = getAddressForJid(contact.getJid()); + return sessions.hasAny(contactAddress); + } + + public int getOwnDeviceId() { + return ownDeviceId; + } + + public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) { + final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId); + if (sessions.get(address) != null) { + return; + } + + synchronized (bundleCache) { + PreKeyBundle bundle = bundleCache.get(address); + if (bundle == null) { + bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null); + bundleCache.put(address, bundle); + } + + if(bundle.getPreKey() == null) { + Log.d(Config.LOGTAG, "No preKey in cache, fetching..."); + IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId); + mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized (bundleCache) { + Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final PreKeyBundle bundle = bundleCache.get(address); + final List preKeyBundleList = parser.preKeys(packet); + if (preKeyBundleList.isEmpty()) { + Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); + return; + } + Random random = new Random(); + final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (bundle == null || newBundle == null) { + //should never happen + return; + } + + final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), + bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(), + bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + + bundleCache.put(address, mergedBundle); + } + } + }); + } + if(bundle.getIdentityKey() == null) { + Log.d(Config.LOGTAG, "No bundle in cache, fetching..."); + IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId); + mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + synchronized (bundleCache) { + Log.d(Config.LOGTAG, "Received bundle IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final PreKeyBundle bundle = bundleCache.get(address); + final PreKeyBundle newBundle = parser.bundle(packet); + if( bundle == null || newBundle == null ) { + Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet); + //should never happen + return; + } + + final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), + bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(), + newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(), + newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey()); + + axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey()); + bundleCache.put(address, mergedBundle); + } + } + }); + } + } + } + + public void publishOwnDeviceIdIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Element item = mXmppConnectionService.getIqParser().getItem(packet); + List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + if(deviceIds == null) { + deviceIds = new ArrayList<>(); + } + if(!deviceIds.contains(getOwnDeviceId())) { + Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing..."); + deviceIds.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + } + }); + } + } + }); + } + + public void publishBundleIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); + if(bundle == null) { + Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing..."); + int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); + try { + SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey( + axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle( + signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), + ownDeviceId); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + Log.d(Config.LOGTAG, "Published bundle, got: " + packet); + } + }); + } catch (InvalidKeyException e) { + Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + } + } + } + }); + } + + public void publishPreKeysIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId); + mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + if(keys == null || keys.isEmpty()) { + Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing..."); + List preKeyRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId(), 100); + for(PreKeyRecord record : preKeyRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys( + preKeyRecords, ownDeviceId); + + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, "Published prekeys, got: " + packet); + // TODO: implement this! + } + }); + } + } + }); + } + + + public boolean isContactAxolotlCapable(Contact contact) { + AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); + return sessions.hasAny(address) || bundleCache.hasAny(address); + } + + public void initiateSynchronousSession(Contact contact) { + + } + + private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { + Log.d(Config.LOGTAG, "Creating axolotl sessions if needed..."); + AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); + for(Integer deviceId: bundleCache.getAll(address).keySet()) { + Log.d(Config.LOGTAG, "Processing device ID: " + deviceId); + AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId); + if(sessions.get(remoteAddress) == null) { + Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId); + SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress); + try { + builder.process(bundleCache.get(remoteAddress)); + XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress); + sessions.put(remoteAddress, session); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage()); + } catch (UntrustedIdentityException e) { + Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage()); + } + } else { + Log.d(Config.LOGTAG, "Already have session for " + deviceId); + } + } + if(!this.hasAny(contact)) { + Log.e(Config.LOGTAG, "No Axolotl sessions available!"); + throw new NoSessionsCreatedException(); // FIXME: proper error handling + } + } + + public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException { + XmppAxolotlMessage message = new XmppAxolotlMessage(contact, ownDeviceId, outgoingMessage); + createSessionsIfNeeded(contact); + Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); + + for(XmppAxolotlSession session : findSessionsforContact(contact)) { // if(!session.isTrusted()) { - // TODO: handle this properly + // TODO: handle this properly // continue; - // } - message.addHeader(session.processSending(message.getInnerKey())); - } - Log.d(Config.LOGTAG, "Building axolotl own headers..."); - for(XmppAxolotlSession session : findOwnSessions()) { - // if(!session.isTrusted()) { - // TODO: handle this properly - // continue; - // } - message.addHeader(session.processSending(message.getInnerKey())); - } + // } + message.addHeader(session.processSending(message.getInnerKey())); + } + Log.d(Config.LOGTAG, "Building axolotl own headers..."); + for(XmppAxolotlSession session : findOwnSessions()) { + // if(!session.isTrusted()) { + // TODO: handle this properly + // continue; + // } + message.addHeader(session.processSending(message.getInnerKey())); + } - return message; - } + return message; + } - public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { - XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; - AxolotlAddress senderAddress = new AxolotlAddress(message.getContact().getJid().toBareJid().toString(), - message.getSenderDeviceId()); + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; + AxolotlAddress senderAddress = new AxolotlAddress(message.getContact().getJid().toBareJid().toString(), + message.getSenderDeviceId()); - XmppAxolotlSession session = sessions.get(senderAddress); - if (session == null) { - Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message); - // TODO: handle this properly - session = new XmppAxolotlSession(axolotlStore, senderAddress); + XmppAxolotlSession session = sessions.get(senderAddress); + if (session == null) { + Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message); + // TODO: handle this properly + session = new XmppAxolotlSession(axolotlStore, senderAddress); - } + } - for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { - if (header.getRecipientDeviceId() == ownDeviceId) { - Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing..."); - byte[] payloadKey = session.processReceiving(header); - if (payloadKey != null) { - Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message..."); - plaintextMessage = message.decrypt(session, payloadKey); - } - } - } + for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { + if (header.getRecipientDeviceId() == ownDeviceId) { + Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing..."); + byte[] payloadKey = session.processReceiving(header); + if (payloadKey != null) { + Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message..."); + plaintextMessage = message.decrypt(session, payloadKey); + } + } + } - return plaintextMessage; - } + return plaintextMessage; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index 4b87fc5cb..e1b956507 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -21,164 +21,164 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.xml.Element; public class XmppAxolotlMessage { - private byte[] innerKey; - private byte[] ciphertext; - private byte[] iv; - private final Set headers; - private final Contact contact; - private final int sourceDeviceId; + private byte[] innerKey; + private byte[] ciphertext; + private byte[] iv; + private final Set headers; + private final Contact contact; + private final int sourceDeviceId; - public static class XmppAxolotlMessageHeader { - private final int recipientDeviceId; - private final byte[] content; + public static class XmppAxolotlMessageHeader { + private final int recipientDeviceId; + private final byte[] content; - public XmppAxolotlMessageHeader(int deviceId, byte[] content) { - this.recipientDeviceId = deviceId; - this.content = content; - } + public XmppAxolotlMessageHeader(int deviceId, byte[] content) { + this.recipientDeviceId = deviceId; + this.content = content; + } - public XmppAxolotlMessageHeader(Element header) { - if("header".equals(header.getName())) { - this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid")); - this.content = Base64.decode(header.getContent(),Base64.DEFAULT); - } else { - throw new IllegalArgumentException("Argument not a

Element!"); - } - } - - public int getRecipientDeviceId() { - return recipientDeviceId; - } + public XmppAxolotlMessageHeader(Element header) { + if("header".equals(header.getName())) { + this.recipientDeviceId = Integer.parseInt(header.getAttribute("rid")); + this.content = Base64.decode(header.getContent(),Base64.DEFAULT); + } else { + throw new IllegalArgumentException("Argument not a
Element!"); + } + } - public byte[] getContents() { - return content; - } + public int getRecipientDeviceId() { + return recipientDeviceId; + } - public Element toXml() { - Element headerElement = new Element("header"); - // TODO: generate XML - headerElement.setAttribute("rid", getRecipientDeviceId()); - headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT)); - return headerElement; - } - } + public byte[] getContents() { + return content; + } - public static class XmppAxolotlPlaintextMessage { - private final AxolotlService.XmppAxolotlSession session; - private final String plaintext; + public Element toXml() { + Element headerElement = new Element("header"); + // TODO: generate XML + headerElement.setAttribute("rid", getRecipientDeviceId()); + headerElement.setContent(Base64.encodeToString(getContents(), Base64.DEFAULT)); + return headerElement; + } + } - public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext) { - this.session = session; - this.plaintext = plaintext; - } + public static class XmppAxolotlPlaintextMessage { + private final AxolotlService.XmppAxolotlSession session; + private final String plaintext; - public String getPlaintext() { - return plaintext; - } - } + public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext) { + this.session = session; + this.plaintext = plaintext; + } - public XmppAxolotlMessage(Contact contact, Element axolotlMessage) { - this.contact = contact; - this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id")); - this.headers = new HashSet<>(); - for(Element child:axolotlMessage.getChildren()) { - switch(child.getName()) { - case "header": - headers.add(new XmppAxolotlMessageHeader(child)); - break; - case "message": - iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT); - ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT); - break; - default: - break; - } - } - } + public String getPlaintext() { + return plaintext; + } + } - public XmppAxolotlMessage(Contact contact, int sourceDeviceId, String plaintext) { - this.contact = contact; - this.sourceDeviceId = sourceDeviceId; - this.headers = new HashSet<>(); - this.encrypt(plaintext); - } + public XmppAxolotlMessage(Contact contact, Element axolotlMessage) { + this.contact = contact; + this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id")); + this.headers = new HashSet<>(); + for(Element child:axolotlMessage.getChildren()) { + switch(child.getName()) { + case "header": + headers.add(new XmppAxolotlMessageHeader(child)); + break; + case "message": + iv = Base64.decode(child.getAttribute("iv"),Base64.DEFAULT); + ciphertext = Base64.decode(child.getContent(),Base64.DEFAULT); + break; + default: + break; + } + } + } - private void encrypt(String plaintext) { - try { - KeyGenerator generator = KeyGenerator.getInstance("AES"); - generator.init(128); - SecretKey secretKey = generator.generateKey(); - Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); - cipher.init(Cipher.ENCRYPT_MODE, secretKey); - this.innerKey = secretKey.getEncoded(); - this.iv = cipher.getIV(); - this.ciphertext = cipher.doFinal(plaintext.getBytes()); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | IllegalBlockSizeException | BadPaddingException e) { + public XmppAxolotlMessage(Contact contact, int sourceDeviceId, String plaintext) { + this.contact = contact; + this.sourceDeviceId = sourceDeviceId; + this.headers = new HashSet<>(); + this.encrypt(plaintext); + } - } - } + private void encrypt(String plaintext) { + try { + KeyGenerator generator = KeyGenerator.getInstance("AES"); + generator.init(128); + SecretKey secretKey = generator.generateKey(); + Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + this.innerKey = secretKey.getEncoded(); + this.iv = cipher.getIV(); + this.ciphertext = cipher.doFinal(plaintext.getBytes()); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | IllegalBlockSizeException | BadPaddingException e) { - public Contact getContact() { - return this.contact; - } + } + } - public int getSenderDeviceId() { - return sourceDeviceId; - } - - public byte[] getCiphertext() { - return ciphertext; - } + public Contact getContact() { + return this.contact; + } - public Set getHeaders() { - return headers; - } + public int getSenderDeviceId() { + return sourceDeviceId; + } - public void addHeader(XmppAxolotlMessageHeader header) { - headers.add(header); - } + public byte[] getCiphertext() { + return ciphertext; + } - public byte[] getInnerKey(){ - return innerKey; - } + public Set getHeaders() { + return headers; + } - public byte[] getIV() { - return this.iv; - } + public void addHeader(XmppAxolotlMessageHeader header) { + headers.add(header); + } - public Element toXml() { - // TODO: generate outer XML, add in header XML - Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX); - message.setAttribute("id", sourceDeviceId); - for(XmppAxolotlMessageHeader header: headers) { - message.addChild(header.toXml()); - } - Element payload = message.addChild("message"); - payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT)); - payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT)); - return message; - } + public byte[] getInnerKey(){ + return innerKey; + } + + public byte[] getIV() { + return this.iv; + } + + public Element toXml() { + // TODO: generate outer XML, add in header XML + Element message= new Element("axolotl_message", AxolotlService.PEP_PREFIX); + message.setAttribute("id", sourceDeviceId); + for(XmppAxolotlMessageHeader header: headers) { + message.addChild(header.toXml()); + } + Element payload = message.addChild("message"); + payload.setAttribute("iv",Base64.encodeToString(iv, Base64.DEFAULT)); + payload.setContent(Base64.encodeToString(ciphertext,Base64.DEFAULT)); + return message; + } - public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key) { - XmppAxolotlPlaintextMessage plaintextMessage = null; - try { + public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key) { + XmppAxolotlPlaintextMessage plaintextMessage = null; + try { - Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); - SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); - IvParameterSpec ivSpec = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); + SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); - cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); - String plaintext = new String(cipher.doFinal(ciphertext)); - plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext); + String plaintext = new String(cipher.doFinal(ciphertext)); + plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext); - } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException - | InvalidAlgorithmParameterException | IllegalBlockSizeException - | BadPaddingException e) { - throw new AssertionError(e); - } - return plaintextMessage; - } + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException + | InvalidAlgorithmParameterException | IllegalBlockSizeException + | BadPaddingException e) { + throw new AssertionError(e); + } + return plaintextMessage; + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 45b55e49d..240d5223c 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -310,7 +310,7 @@ public class Contact implements ListItem, Blockable { synchronized (this.keys) { if (getOtrFingerprints().contains(print)) { return false; - } + } try { JSONArray fingerprints; if (!this.keys.has("otr_fingerprints")) { @@ -392,12 +392,12 @@ public class Contact implements ListItem, Blockable { public boolean addAxolotlIdentityKey(IdentityKey identityKey) { synchronized (this.keys) { if(!getAxolotlIdentityKeys().contains(identityKey)) { - JSONArray keysList; - try { - keysList = this.keys.getJSONArray("axolotl_identity_key"); - } catch (JSONException e) { - keysList = new JSONArray(); - } + JSONArray keysList; + try { + keysList = this.keys.getJSONArray("axolotl_identity_key"); + } catch (JSONException e) { + keysList = new JSONArray(); + } keysList.put(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); try { @@ -406,10 +406,10 @@ public class Contact implements ListItem, Blockable { Log.e(Config.LOGTAG, "Error adding Identity Key to Contact " + this.getJid() + ": " + e.getMessage()); return false; } - return true; + return true; } else { - return false; - } + return false; + } } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 6daadc2af..5b3bde6a5 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -147,16 +147,16 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket publishDeviceIds(final List ids) { - final Element item = new Element("item"); - final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); - for(Integer id:ids) { - final Element device = new Element("device"); - device.setAttribute("id", id); - list.addChild(device); - } - return publish(AxolotlService.PEP_DEVICE_LIST, item); - } + public IqPacket publishDeviceIds(final List ids) { + final Element item = new Element("item"); + final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); + for(Integer id:ids) { + final Element device = new Element("device"); + device.setAttribute("id", id); + list.addChild(device); + } + return publish(AxolotlService.PEP_DEVICE_LIST, item); + } public IqPacket publishBundle(final SignedPreKeyRecord signedPreKeyRecord, IdentityKey identityKey, final int deviceId) { final Element item = new Element("item"); @@ -173,17 +173,17 @@ public class IqGenerator extends AbstractGenerator { return publish(AxolotlService.PEP_BUNDLE+":"+deviceId, item); } - public IqPacket publishPreKeys(final List prekeyList, final int deviceId) { - final Element item = new Element("item"); - final Element prekeys = item.addChild("prekeys", AxolotlService.PEP_PREFIX); - for(PreKeyRecord preKeyRecord:prekeyList) { - final Element prekey = prekeys.addChild("preKeyPublic"); + public IqPacket publishPreKeys(final List prekeyList, final int deviceId) { + final Element item = new Element("item"); + final Element prekeys = item.addChild("prekeys", AxolotlService.PEP_PREFIX); + for(PreKeyRecord preKeyRecord:prekeyList) { + final Element prekey = prekeys.addChild("preKeyPublic"); prekey.setAttribute("preKeyId", preKeyRecord.getId()); - prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); - } + prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); + } return publish(AxolotlService.PEP_PREKEYS+":"+deviceId, item); - } + } public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index e6032e0cb..0b6a7c614 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -66,19 +66,19 @@ public class MessageGenerator extends AbstractGenerator { delay.setAttribute("stamp", mDateFormat.format(date)); } - public MessagePacket generateAxolotlChat(Message message) throws NoSessionsCreatedException{ - return generateAxolotlChat(message, false); - } + public MessagePacket generateAxolotlChat(Message message) throws NoSessionsCreatedException{ + return generateAxolotlChat(message, false); + } public MessagePacket generateAxolotlChat(Message message, boolean addDelay) throws NoSessionsCreatedException{ - MessagePacket packet = preparePacket(message, addDelay); - AxolotlService service = message.getConversation().getAccount().getAxolotlService(); - Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing..."); - XmppAxolotlMessage axolotlMessage = service.processSending(message.getContact(), - message.getBody()); - packet.setAxolotlMessage(axolotlMessage.toXml()); - return packet; - } + MessagePacket packet = preparePacket(message, addDelay); + AxolotlService service = message.getConversation().getAccount().getAxolotlService(); + Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing..."); + XmppAxolotlMessage axolotlMessage = service.processSending(message.getContact(), + message.getBody()); + packet.setAxolotlMessage(axolotlMessage.toXml()); + return packet; + } public MessagePacket generateOtrChat(Message message) { return generateOtrChat(message, false); diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 00f968858..df143a411 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -70,7 +70,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { public String avatarData(final IqPacket packet) { final Element pubsub = packet.findChild("pubsub", - "http://jabber.org/protocol/pubsub"); + "http://jabber.org/protocol/pubsub"); if (pubsub == null) { return null; } @@ -165,19 +165,19 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { public Map preKeyPublics(final IqPacket packet) { Map preKeyRecords = new HashMap<>(); - Element prekeysItem = getItem(packet); - if (prekeysItem == null) { - Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); - return null; - } - final Element prekeysElement = prekeysItem.findChild("prekeys"); - if(prekeysElement == null) { - Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); - return null; - } + Element prekeysItem = getItem(packet); + if (prekeysItem == null) { + Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + return null; + } + final Element prekeysElement = prekeysItem.findChild("prekeys"); + if(prekeysElement == null) { + Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + return null; + } for(Element preKeyPublicElement : prekeysElement.getChildren()) { if(!preKeyPublicElement.getName().equals("preKeyPublic")){ - Log.d(Config.LOGTAG, "Encountered unexpected tag in prekeys list: " + preKeyPublicElement); + Log.d(Config.LOGTAG, "Encountered unexpected tag in prekeys list: " + preKeyPublicElement); continue; } Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); @@ -192,37 +192,37 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return preKeyRecords; } - public PreKeyBundle bundle(final IqPacket bundle) { - Element bundleItem = getItem(bundle); - if(bundleItem == null) { - return null; - } - final Element bundleElement = bundleItem.findChild("bundle"); - if(bundleElement == null) { - return null; - } - ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); - Integer signedPreKeyId = signedPreKeyId(bundleElement); - byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); - IdentityKey identityKey = identityKey(bundleElement); - if(signedPreKeyPublic == null || identityKey == null) { - return null; - } + public PreKeyBundle bundle(final IqPacket bundle) { + Element bundleItem = getItem(bundle); + if(bundleItem == null) { + return null; + } + final Element bundleElement = bundleItem.findChild("bundle"); + if(bundleElement == null) { + return null; + } + ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement); + Integer signedPreKeyId = signedPreKeyId(bundleElement); + byte[] signedPreKeySignature = signedPreKeySignature(bundleElement); + IdentityKey identityKey = identityKey(bundleElement); + if(signedPreKeyPublic == null || identityKey == null) { + return null; + } - return new PreKeyBundle(0, 0, 0, null, - signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); - } + return new PreKeyBundle(0, 0, 0, null, + signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey); + } public List preKeys(final IqPacket preKeys) { List bundles = new ArrayList<>(); Map preKeyPublics = preKeyPublics(preKeys); - if ( preKeyPublics != null) { - for (Integer preKeyId : preKeyPublics.keySet()) { - ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); - bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, - 0, null, null, null)); - } - } + if ( preKeyPublics != null) { + for (Integer preKeyId : preKeyPublics.keySet()) { + ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId); + bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic, + 0, null, null, null)); + } + } return bundles; } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index bf4f3ad4b..de7057308 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -98,17 +98,17 @@ public class MessageParser extends AbstractParser implements } } - private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation) { - Message finishedMessage = null; - AxolotlService service = conversation.getAccount().getAxolotlService(); - XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(conversation.getContact(), axolotlMessage); - XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); - if(plaintextMessage != null) { - finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED); - } + private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation) { + Message finishedMessage = null; + AxolotlService service = conversation.getAccount().getAxolotlService(); + XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(conversation.getContact(), axolotlMessage); + XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); + if(plaintextMessage != null) { + finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED); + } - return finishedMessage; - } + return finishedMessage; + } private class Invite { Jid jid; @@ -187,17 +187,17 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.updateAccountUi(); } } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { - Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing..."); - Element item = items.findChild("item"); + Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing..."); + Element item = items.findChild("item"); List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - AxolotlService axolotlService = account.getAxolotlService(); - if(account.getJid().toBareJid().equals(from)) { - } else { - Contact contact = account.getRoster().getContact(from); - for (Integer deviceId : deviceIds) { - axolotlService.fetchBundleIfNeeded(contact, deviceId); - } - } + AxolotlService axolotlService = account.getAxolotlService(); + if(account.getJid().toBareJid().equals(from)) { + } else { + Contact contact = account.getRoster().getContact(from); + for (Integer deviceId : deviceIds) { + axolotlService.fetchBundleIfNeeded(contact, deviceId); + } + } } } @@ -262,7 +262,7 @@ public class MessageParser extends AbstractParser implements final String body = packet.getBody(); final Element mucUserElement = packet.findChild("x","http://jabber.org/protocol/muc#user"); final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); - final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX); + final Element axolotlEncrypted = packet.findChild("axolotl_message", AxolotlService.PEP_PREFIX); int status; final Jid counterpart; final Jid to = packet.getTo(); @@ -324,13 +324,13 @@ public class MessageParser extends AbstractParser implements message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } } else if (pgpEncrypted != null) { - message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); - } else if (axolotlEncrypted != null) { - message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation); - if (message == null) { - return; - } - } else { + message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); + } else if (axolotlEncrypted != null) { + message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation); + if (message == null) { + return; + } + } else { message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } message.setCounterpart(counterpart); diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index af0e2fa88..ee6c76368 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -42,7 +42,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Contact.JID + " TEXT," + Contact.KEYS + " TEXT," + Contact.PHOTOURI + " TEXT," + Contact.OPTIONS + " NUMBER," + Contact.SYSTEMACCOUNT + " NUMBER, " + Contact.AVATAR + " TEXT, " - + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " + + Contact.LAST_PRESENCE + " TEXT, " + Contact.LAST_TIME + " NUMBER, " + Contact.GROUPS + " TEXT, FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", " @@ -75,14 +75,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME + "(" + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " - + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " + + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " + AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" - + AxolotlService.SQLiteAxolotlStore.ACCOUNT + + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " - + "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " - + AxolotlService.SQLiteAxolotlStore.NAME + ", " + + "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " + + AxolotlService.SQLiteAxolotlStore.NAME + ", " + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + ") ON CONFLICT REPLACE" +");"; @@ -157,12 +157,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("ALTER TABLE " + Conversation.TABLENAME + " ADD COLUMN " + Conversation.ATTRIBUTES + " TEXT"); } - if (oldVersion < 9 && newVersion >= 9) { - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_TIME + " NUMBER"); - db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " - + Contact.LAST_PRESENCE + " TEXT"); - } + if (oldVersion < 9 && newVersion >= 9) { + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.LAST_TIME + " NUMBER"); + db.execSQL("ALTER TABLE " + Contact.TABLENAME + " ADD COLUMN " + + Contact.LAST_PRESENCE + " TEXT"); + } if (oldVersion < 10 && newVersion >= 10) { db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.RELATIVE_FILE_PATH + " TEXT"); @@ -557,7 +557,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public SessionRecord loadSession(Account account, AxolotlAddress contact) { SessionRecord session = null; - Cursor cursor = getCursorForSession(account, contact); + Cursor cursor = getCursorForSession(account, contact); if(cursor.getCount() != 0) { cursor.moveToFirst(); try { @@ -565,8 +565,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { } catch (IOException e) { throw new AssertionError(e); } - } - cursor.close(); + } + cursor.close(); return session; } @@ -635,7 +635,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { Cursor cursor = getCursorForSession(account, contact); if(cursor.getCount() != 0) { cursor.moveToFirst(); - trusted = cursor.getInt(cursor.getColumnIndex( + trusted = cursor.getInt(cursor.getColumnIndex( AxolotlService.SQLiteAxolotlStore.TRUSTED)) > 0; } cursor.close(); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 1ebe3a033..9a4cc276f 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -274,9 +274,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } syncDirtyContacts(account); - account.getAxolotlService().publishOwnDeviceIdIfNeeded(); - account.getAxolotlService().publishBundleIfNeeded(); - account.getAxolotlService().publishPreKeysIfNeeded(); + account.getAxolotlService().publishOwnDeviceIdIfNeeded(); + account.getAxolotlService().publishBundleIfNeeded(); + account.getAxolotlService().publishPreKeysIfNeeded(); scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); } else if (account.getStatus() == Account.State.OFFLINE) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index ff1355c35..e4ce4a0f1 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -752,16 +752,16 @@ public class ConversationActivity extends XmppActivity } break; case R.id.encryption_choice_axolotl: - Log.d(Config.LOGTAG, "Trying to enable axolotl..."); + Log.d(Config.LOGTAG, "Trying to enable axolotl..."); if(conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { - Log.d(Config.LOGTAG, "Enabled axolotl for Contact " + conversation.getContact().getJid() ); + Log.d(Config.LOGTAG, "Enabled axolotl for Contact " + conversation.getContact().getJid() ); conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); item.setChecked(true); } else { - Log.d(Config.LOGTAG, "Contact " + conversation.getContact().getJid() + " not axolotl capable!"); + Log.d(Config.LOGTAG, "Contact " + conversation.getContact().getJid() + " not axolotl capable!"); showAxolotlNoSessionsDialog(); } - break; + break; default: conversation.setNextEncryption(Message.ENCRYPTION_NONE); break; @@ -794,7 +794,7 @@ public class ConversationActivity extends XmppActivity pgp.setChecked(true); break; case Message.ENCRYPTION_AXOLOTL: - Log.d(Config.LOGTAG, "Axolotl confirmed. Setting menu item checked!"); + Log.d(Config.LOGTAG, "Axolotl confirmed. Setting menu item checked!"); popup.getMenu().findItem(R.id.encryption_choice_axolotl) .setChecked(true); break; From 9e07fc5651015dc60e54bcb2f796d0f932d5f925 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 13:40:56 +0200 Subject: [PATCH 012/166] DatabaseBackend bugfixes Don't leak cursors, initially create tables --- .../eu/siacs/conversations/persistance/DatabaseBackend.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index ee6c76368..966734bae 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -123,6 +123,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { + ") ON DELETE CASCADE);"); db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL(CREATE_SESSIONS_STATEMENT); + db.execSQL(CREATE_PREKEYS_STATEMENT); + db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); } @Override @@ -563,6 +566,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (IOException e) { + cursor.close(); throw new AssertionError(e); } } @@ -751,6 +755,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } catch (IOException ignored) { } } + cursor.close(); return prekeys; } From 74026b742bd7e1d4b79fd839f887a0beb940b0dc Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 13:53:39 +0200 Subject: [PATCH 013/166] Save IdentityKeys in database --- .../crypto/axolotl/AxolotlService.java | 70 +++++---------- .../persistance/DatabaseBackend.java | 85 +++++++++++++++++++ 2 files changed, 108 insertions(+), 47 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 5c4b34a46..d788ed066 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -68,12 +68,14 @@ public class AxolotlService { public static final String PREKEY_TABLENAME = "prekeys"; public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys"; public static final String SESSION_TABLENAME = "sessions"; + public static final String IDENTITIES_TABLENAME = "identities"; public static final String ACCOUNT = "account"; public static final String DEVICE_ID = "device_id"; public static final String ID = "id"; public static final String KEY = "key"; public static final String NAME = "name"; public static final String TRUSTED = "trusted"; + public static final String OWN = "ownkey"; public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; @@ -82,7 +84,7 @@ public class AxolotlService { private final Account account; private final XmppConnectionService mXmppConnectionService; - private final IdentityKeyPair identityKeyPair; + private IdentityKeyPair identityKeyPair; private final int localRegistrationId; private int currentPreKeyId = 0; @@ -104,10 +106,9 @@ public class AxolotlService { public SQLiteAxolotlStore(Account account, XmppConnectionService service) { this.account = account; this.mXmppConnectionService = service; - this.identityKeyPair = loadIdentityKeyPair(); this.localRegistrationId = loadRegistrationId(); this.currentPreKeyId = loadCurrentPreKeyId(); - for( SignedPreKeyRecord record:loadSignedPreKeys()) { + for (SignedPreKeyRecord record : loadSignedPreKeys()) { Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId()); } } @@ -121,26 +122,17 @@ public class AxolotlService { // -------------------------------------- private IdentityKeyPair loadIdentityKeyPair() { - String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR); - IdentityKeyPair ownKey; - if( serializedKey != null ) { - try { - ownKey = new IdentityKeyPair(Base64.decode(serializedKey,Base64.DEFAULT)); - return ownKey; - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage()); -// return null; - } - } //else { - Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid()); + String ownName = account.getJid().toBareJid().toString(); + IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account, + ownName); + + if (ownKey != null) { + return ownKey; + } else { + Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + ownName); ownKey = generateIdentityKeyPair(); - boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, Base64.encodeToString(ownKey.serialize(), Base64.DEFAULT)); - if(success) { - mXmppConnectionService.databaseBackend.updateAccount(account); - } else { - Log.e(Config.LOGTAG, "Failed to write new key to the database!"); - } - //} + mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey); + } return ownKey; } @@ -152,8 +144,8 @@ public class AxolotlService { } else { Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid()); reg_id = generateRegistrationId(); - boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id); - if(success) { + boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); + if (success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { Log.e(Config.LOGTAG, "Failed to write new key to the database!"); @@ -182,6 +174,9 @@ public class AxolotlService { */ @Override public IdentityKeyPair getIdentityKeyPair() { + if(identityKeyPair == null) { + identityKeyPair = loadIdentityKeyPair(); + } return identityKeyPair; } @@ -208,16 +203,8 @@ public class AxolotlService { */ @Override public void saveIdentity(String name, IdentityKey identityKey) { - try { - Jid contactJid = Jid.fromString(name); - Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); - if (conversation != null) { - conversation.getContact().addAxolotlIdentityKey(identityKey); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); - } - } catch (final InvalidJidException e) { - Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString()); + if(!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) { + mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey); } } @@ -237,19 +224,8 @@ public class AxolotlService { */ @Override public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - try { - Jid contactJid = Jid.fromString(name); - Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid); - if (conversation != null) { - List trustedKeys = conversation.getContact().getAxolotlIdentityKeys(); - return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); - } else { - return false; - } - } catch (final InvalidJidException e) { - Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString()); - return false; - } + Set trustedKeys = mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name); + return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); } // -------------------------------------- diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 966734bae..c70ffad2c 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -10,13 +10,18 @@ import android.util.Base64; import android.util.Log; import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import java.io.IOException; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import eu.siacs.conversations.Config; @@ -87,6 +92,16 @@ public class DatabaseBackend extends SQLiteOpenHelper { + ") ON CONFLICT REPLACE" +");"; + private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE " + + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME + "(" + + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + + AxolotlService.SQLiteAxolotlStore.OWN + " INTEGER, " + + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + + AxolotlService.SQLiteAxolotlStore.ACCOUNT + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE " + +");"; + private DatabaseBackend(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @@ -126,6 +141,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_SESSIONS_STATEMENT); db.execSQL(CREATE_PREKEYS_STATEMENT); db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + db.execSQL(CREATE_IDENTITIES_STATEMENT); } @Override @@ -273,6 +289,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL(CREATE_PREKEYS_STATEMENT); db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME); + db.execSQL(CREATE_IDENTITIES_STATEMENT); } } @@ -783,4 +801,71 @@ public class DatabaseBackend extends SQLiteOpenHelper { + AxolotlService.SQLiteAxolotlStore.ID + "=?", args); } + + private Cursor getIdentityKeyCursor(Account account, String name, boolean own) { + final SQLiteDatabase db = this.getReadableDatabase(); + String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; + String[] selectionArgs = {account.getUuid(), + name, + own?"1":"0"}; + Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, + columns, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.OWN + " = ? ", + selectionArgs, + null, null, null); + + return cursor; + } + + public IdentityKeyPair loadOwnIdentityKeyPair(Account account, String name) { + IdentityKeyPair identityKeyPair = null; + Cursor cursor = getIdentityKeyCursor(account, name, true); + if(cursor.getCount() != 0) { + cursor.moveToFirst(); + try { + identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); + } + } + cursor.close(); + + return identityKeyPair; + } + + public Set loadIdentityKeys(Account account, String name) { + Set identityKeys = new HashSet<>(); + Cursor cursor = getIdentityKeyCursor(account, name, false); + + while(cursor.moveToNext()) { + try { + identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0)); + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name); + } + } + cursor.close(); + + return identityKeys; + } + + private void storeIdentityKey(Account account, String name, boolean own, String base64Serialized) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); + values.put(AxolotlService.SQLiteAxolotlStore.NAME, name); + values.put(AxolotlService.SQLiteAxolotlStore.OWN, own?1:0); + values.put(AxolotlService.SQLiteAxolotlStore.KEY, base64Serialized); + db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); + } + + public void storeIdentityKey(Account account, String name, IdentityKey identityKey) { + storeIdentityKey(account, name, false, Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); + } + + public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { + storeIdentityKey(account, name, true, Base64.encodeToString(identityKeyPair.serialize(),Base64.DEFAULT)); + } } From 6492801b89cea496b41fe77e6b2a9be18abb2081 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 13:55:45 +0200 Subject: [PATCH 014/166] Formatting fixes --- .../crypto/axolotl/AxolotlService.java | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index d788ed066..8879a0fe1 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -248,7 +248,7 @@ public class AxolotlService { @Override public SessionRecord loadSession(AxolotlAddress address) { SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address); - return (session!=null)?session:new SessionRecord(); + return (session != null) ? session : new SessionRecord(); } /** @@ -260,7 +260,7 @@ public class AxolotlService { @Override public List getSubDeviceSessions(String name) { return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account, - new AxolotlAddress(name,0)); + new AxolotlAddress(name, 0)); } /** @@ -311,7 +311,7 @@ public class AxolotlService { } public void setTrustedSession(AxolotlAddress address, boolean trusted) { - mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address,trusted); + mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address, trusted); } // -------------------------------------- @@ -328,7 +328,7 @@ public class AxolotlService { @Override public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId); - if(record == null) { + if (record == null) { throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId); } return record; @@ -344,8 +344,8 @@ public class AxolotlService { public void storePreKey(int preKeyId, PreKeyRecord record) { mXmppConnectionService.databaseBackend.storePreKey(account, record); currentPreKeyId = preKeyId; - boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID,Integer.toString(preKeyId)); - if(success) { + boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId)); + if (success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!"); @@ -385,7 +385,7 @@ public class AxolotlService { @Override public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId); - if(record == null) { + if (record == null) { throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId); } return record; @@ -459,18 +459,18 @@ public class AxolotlService { try { try { PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); - Log.d(Config.LOGTAG,"PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + Log.d(Config.LOGTAG, "PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); plaintext = cipher.decrypt(message); - } catch (InvalidMessageException|InvalidVersionException e) { + } catch (InvalidMessageException | InvalidVersionException e) { WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); plaintext = cipher.decrypt(message); - } catch (InvalidKeyException|InvalidKeyIdException| UntrustedIdentityException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); } - } catch (LegacyMessageException|InvalidMessageException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); - } catch (DuplicateMessageException|NoSessionException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header: " + e.getMessage()); + } catch (LegacyMessageException | InvalidMessageException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); + } catch (DuplicateMessageException | NoSessionException e) { + Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); } return plaintext; } @@ -485,7 +485,7 @@ public class AxolotlService { } private static class AxolotlAddressMap { - protected Map> map; + protected Map> map; protected final Object MAP_LOCK = new Object(); public AxolotlAddressMap() { @@ -506,7 +506,7 @@ public class AxolotlService { public T get(AxolotlAddress address) { synchronized (MAP_LOCK) { Map devices = map.get(address.getName()); - if(devices == null) { + if (devices == null) { return null; } return devices.get(address.getDeviceId()); @@ -516,7 +516,7 @@ public class AxolotlService { public Map getAll(AxolotlAddress address) { synchronized (MAP_LOCK) { Map devices = map.get(address.getName()); - if(devices == null) { + if (devices == null) { return new HashMap<>(); } return devices; @@ -541,14 +541,14 @@ public class AxolotlService { } private void fillMap(SQLiteAxolotlStore store, Account account) { - for(Contact contact:account.getRoster().getContacts()){ + for (Contact contact : account.getRoster().getContacts()) { Jid bareJid = contact.getJid().toBareJid(); - if(bareJid == null) { + if (bareJid == null) { continue; // FIXME: handle this? } String address = bareJid.toString(); List deviceIDs = store.getSubDeviceSessions(address); - for(Integer deviceId:deviceIDs) { + for (Integer deviceId : deviceIDs) { AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress)); } @@ -572,7 +572,7 @@ public class AxolotlService { public void trustSession(AxolotlAddress counterpart) { XmppAxolotlSession session = sessions.get(counterpart); - if(session != null) { + if (session != null) { session.trust(); } } From c1d23b2395bfc9570aed07ba3413f988f08d84f5 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:18:11 +0200 Subject: [PATCH 015/166] Migrate to new PEP layout Merge prekeys into bundle node --- .../crypto/axolotl/AxolotlService.java | 254 ++++++++++-------- .../conversations/generator/IqGenerator.java | 29 +- .../siacs/conversations/parser/IqParser.java | 20 +- .../services/XmppConnectionService.java | 3 +- 4 files changed, 158 insertions(+), 148 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 8879a0fe1..22e959eb8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -30,6 +30,7 @@ import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.util.KeyHelper; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -53,14 +54,16 @@ public class AxolotlService { public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl"; public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; - public static final String PEP_PREKEYS = PEP_PREFIX + ".prekeys"; - public static final String PEP_BUNDLE = PEP_PREFIX + ".bundle"; + public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; + + public static final int NUM_KEYS_TO_PUBLISH = 10; private final Account account; private final XmppConnectionService mXmppConnectionService; private final SQLiteAxolotlStore axolotlStore; private final SessionMap sessions; private final BundleMap bundleCache; + private final Map> deviceIds; private int ownDeviceId; public static class SQLiteAxolotlStore implements AxolotlStore { @@ -565,6 +568,7 @@ public class AxolotlService { this.mXmppConnectionService = connectionService; this.account = account; this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); + this.deviceIds = new HashMap<>(); this.sessions = new SessionMap(axolotlStore, account); this.bundleCache = new BundleMap(); this.ownDeviceId = axolotlStore.getLocalRegistrationId(); @@ -607,80 +611,11 @@ public class AxolotlService { return ownDeviceId; } - public void fetchBundleIfNeeded(final Contact contact, final Integer deviceId) { - final AxolotlAddress address = new AxolotlAddress(contact.getJid().toString(), deviceId); - if (sessions.get(address) != null) { - return; - } - - synchronized (bundleCache) { - PreKeyBundle bundle = bundleCache.get(address); - if (bundle == null) { - bundle = new PreKeyBundle(0, deviceId, 0, null, 0, null, null, null); - bundleCache.put(address, bundle); - } - - if(bundle.getPreKey() == null) { - Log.d(Config.LOGTAG, "No preKey in cache, fetching..."); - IqPacket prekeysPacket = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(contact.getJid(), deviceId); - mXmppConnectionService.sendIqPacket(account, prekeysPacket, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized (bundleCache) { - Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final PreKeyBundle bundle = bundleCache.get(address); - final List preKeyBundleList = parser.preKeys(packet); - if (preKeyBundleList.isEmpty()) { - Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); - return; - } - Random random = new Random(); - final PreKeyBundle newBundle = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); - if (bundle == null || newBundle == null) { - //should never happen - return; - } - - final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), - bundle.getDeviceId(), newBundle.getPreKeyId(), newBundle.getPreKey(), - bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), - bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); - - bundleCache.put(address, mergedBundle); - } - } - }); - } - if(bundle.getIdentityKey() == null) { - Log.d(Config.LOGTAG, "No bundle in cache, fetching..."); - IqPacket bundlePacket = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(contact.getJid(), deviceId); - mXmppConnectionService.sendIqPacket(account, bundlePacket, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized (bundleCache) { - Log.d(Config.LOGTAG, "Received bundle IQ packet, processing..."); - final IqParser parser = mXmppConnectionService.getIqParser(); - final PreKeyBundle bundle = bundleCache.get(address); - final PreKeyBundle newBundle = parser.bundle(packet); - if( bundle == null || newBundle == null ) { - Log.d(Config.LOGTAG, "bundle IQ packet invalid: " + packet); - //should never happen - return; - } - - final PreKeyBundle mergedBundle = new PreKeyBundle(bundle.getRegistrationId(), - bundle.getDeviceId(), bundle.getPreKeyId(), bundle.getPreKey(), - newBundle.getSignedPreKeyId(), newBundle.getSignedPreKey(), - newBundle.getSignedPreKeySignature(), newBundle.getIdentityKey()); - - axolotlStore.saveIdentity(contact.getJid().toBareJid().toString(), newBundle.getIdentityKey()); - bundleCache.put(address, mergedBundle); - } - } - }); - } + public void registerDevices(final Jid jid, final Set deviceIds) { + for(Integer i:deviceIds) { + Log.d(Config.LOGTAG, "Adding Device ID:"+ jid + ":"+i); } + this.deviceIds.put(jid, deviceIds); } public void publishOwnDeviceIdIfNeeded() { @@ -689,14 +624,14 @@ public class AxolotlService { @Override public void onIqPacketReceived(Account account, IqPacket packet) { Element item = mXmppConnectionService.getIqParser().getItem(packet); - List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); - if(deviceIds == null) { - deviceIds = new ArrayList<>(); + Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + if (deviceIds == null) { + deviceIds = new HashSet(); } - if(!deviceIds.contains(getOwnDeviceId())) { - Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing..."); + if (!deviceIds.contains(getOwnDeviceId())) { deviceIds.add(getOwnDeviceId()); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -708,22 +643,68 @@ public class AxolotlService { }); } - public void publishBundleIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundleForDevice(account.getJid().toBareJid(), ownDeviceId); + private boolean validateBundle(PreKeyBundle bundle) { + if (bundle == null || bundle.getIdentityKey() == null + || bundle.getSignedPreKey() == null || bundle.getSignedPreKeySignature() == null) { + return false; + } + + try { + SignedPreKeyRecord signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); + IdentityKey identityKey = axolotlStore.getIdentityKeyPair().getPublicKey(); + Log.d(Config.LOGTAG,"own identity key:"+identityKey.getFingerprint()+", foreign: "+bundle.getIdentityKey().getFingerprint()); + Log.d(Config.LOGTAG,"bundle: "+Boolean.toString(bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())) + +" " + Boolean.toString(Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) + +" " + Boolean.toString( bundle.getIdentityKey().equals(identityKey))); + return bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + && Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature()) + && bundle.getIdentityKey().equals(identityKey); + } catch (InvalidKeyIdException ignored) { + return false; + } + } + + private boolean validatePreKeys(Map keys) { + if(keys == null) { return false; } + for(Integer id:keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if(!preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { + return false; + } + } catch (InvalidKeyIdException ignored) { + return false; + } + } + return true; + } + + public void publishBundlesIfNeeded() { + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), ownDeviceId); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); - if(bundle == null) { - Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing..."); + Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); + SignedPreKeyRecord signedPreKeyRecord; + List preKeyRecords; + if (!validateBundle(bundle) || keys.isEmpty() || !validatePreKeys(keys)) { int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); try { - SignedPreKeyRecord signedPreKeyRecord = KeyHelper.generateSignedPreKey( + signedPreKeyRecord = KeyHelper.generateSignedPreKey( axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); - IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundle( + + preKeyRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId(), NUM_KEYS_TO_PUBLISH); + for (PreKeyRecord record : preKeyRecords) { + axolotlStore.storePreKey(record.getId(), record); + } + + IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - ownDeviceId); + preKeyRecords, ownDeviceId); + Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -733,48 +714,83 @@ public class AxolotlService { }); } catch (InvalidKeyException e) { Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + return; } } } }); } - public void publishPreKeysIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrievePreKeysForDevice(account.getJid().toBareJid(), ownDeviceId); - mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - if(keys == null || keys.isEmpty()) { - Log.d(Config.LOGTAG, "Prekeys " + getOwnDeviceId() + " not in PEP. Publishing..."); - List preKeyRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId(), 100); - for(PreKeyRecord record : preKeyRecords) { - axolotlStore.storePreKey(record.getId(), record); - } - IqPacket publish = mXmppConnectionService.getIqGenerator().publishPreKeys( - preKeyRecords, ownDeviceId); - - mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d(Config.LOGTAG, "Published prekeys, got: " + packet); - // TODO: implement this! - } - }); - } - } - }); - } - - public boolean isContactAxolotlCapable(Contact contact) { - AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); - return sessions.hasAny(address) || bundleCache.hasAny(address); + Jid jid = contact.getJid().toBareJid(); + AxolotlAddress address = new AxolotlAddress(jid.toString(), 0); + return sessions.hasAny(address) || + ( deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); } - public void initiateSynchronousSession(Contact contact) { + private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) { + Log.d(Config.LOGTAG, "Building new sesstion for " + address.getDeviceId()); + try { + IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( + Jid.fromString(address.getName()), address.getDeviceId()); + Log.d(Config.LOGTAG, "Retrieving bundle: " + bundlesPacket); + mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); + final IqParser parser = mXmppConnectionService.getIqParser(); + final List preKeyBundleList = parser.preKeys(packet); + final PreKeyBundle bundle = parser.bundle(packet); + if (preKeyBundleList.isEmpty() || bundle == null) { + Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); + fetchStatusMap.put(address, FetchStatus.ERROR); + return; + } + Random random = new Random(); + final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size())); + if (preKey == null) { + //should never happen + fetchStatusMap.put(address, FetchStatus.ERROR); + return; + } + + final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(), + preKey.getPreKeyId(), preKey.getPreKey(), + bundle.getSignedPreKeyId(), bundle.getSignedPreKey(), + bundle.getSignedPreKeySignature(), bundle.getIdentityKey()); + + axolotlStore.saveIdentity(address.getName(), bundle.getIdentityKey()); + + try { + SessionBuilder builder = new SessionBuilder(axolotlStore, address); + builder.process(preKeyBundle); + XmppAxolotlSession session = new XmppAxolotlSession(axolotlStore, address); + sessions.put(address, session); + fetchStatusMap.put(address, FetchStatus.SUCCESS); + } catch (UntrustedIdentityException|InvalidKeyException e) { + Log.d(Config.LOGTAG, "Error building session for " + address + ": " + + e.getClass().getName() + ", " + e.getMessage()); + fetchStatusMap.put(address, FetchStatus.ERROR); + } + + AxolotlAddress ownAddress = new AxolotlAddress(conversation.getAccount().getJid().toBareJid().toString(),0); + AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0); + if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + && !fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) { + conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL, + new Conversation.OnMessageFound() { + @Override + public void onMessageFound(Message message) { + processSending(message); + } + }); + } + } + }); + } catch (InvalidJidException e) { + Log.e(Config.LOGTAG,"Got address with invalid jid: " + address.getName()); + } } private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 5b3bde6a5..0bef88532 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -10,6 +10,7 @@ import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import java.util.ArrayList; import java.util.List; +import java.util.Set; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; @@ -131,23 +132,15 @@ public class IqGenerator extends AbstractGenerator { return packet; } - public IqPacket retrieveBundleForDevice(final Jid to, final int deviceid) { - final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLE+":"+deviceid, null); + public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) { + final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null); if(to != null) { packet.setTo(to); } return packet; } - public IqPacket retrievePreKeysForDevice(final Jid to, final int deviceId) { - final IqPacket packet = retrieve(AxolotlService.PEP_PREKEYS+":"+deviceId, null); - if(to != null) { - packet.setTo(to); - } - return packet; - } - - public IqPacket publishDeviceIds(final List ids) { + public IqPacket publishDeviceIds(final Set ids) { final Element item = new Element("item"); final Element list = item.addChild("list", AxolotlService.PEP_PREFIX); for(Integer id:ids) { @@ -158,7 +151,8 @@ public class IqGenerator extends AbstractGenerator { return publish(AxolotlService.PEP_DEVICE_LIST, item); } - public IqPacket publishBundle(final SignedPreKeyRecord signedPreKeyRecord, IdentityKey identityKey, final int deviceId) { + public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, + final List preKeyRecords, final int deviceId) { final Element item = new Element("item"); final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); @@ -170,19 +164,14 @@ public class IqGenerator extends AbstractGenerator { final Element identityKeyElement = bundle.addChild("identityKey"); identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); - return publish(AxolotlService.PEP_BUNDLE+":"+deviceId, item); - } - - public IqPacket publishPreKeys(final List prekeyList, final int deviceId) { - final Element item = new Element("item"); - final Element prekeys = item.addChild("prekeys", AxolotlService.PEP_PREFIX); - for(PreKeyRecord preKeyRecord:prekeyList) { + final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX); + for(PreKeyRecord preKeyRecord:preKeyRecords) { final Element prekey = prekeys.addChild("preKeyPublic"); prekey.setAttribute("preKeyId", preKeyRecord.getId()); prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT)); } - return publish(AxolotlService.PEP_PREKEYS+":"+deviceId, item); + return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item); } public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index df143a411..935517877 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -12,8 +12,10 @@ import org.whispersystems.libaxolotl.state.PreKeyBundle; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; @@ -94,8 +96,8 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return items.findChild("item"); } - public List deviceIds(final Element item) { - List deviceIds = new ArrayList<>(); + public Set deviceIds(final Element item) { + Set deviceIds = new HashSet<>(); if (item == null) { return null; } @@ -165,14 +167,18 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { public Map preKeyPublics(final IqPacket packet) { Map preKeyRecords = new HashMap<>(); - Element prekeysItem = getItem(packet); - if (prekeysItem == null) { - Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + Element item = getItem(packet); + if (item == null) { + Log.d(Config.LOGTAG, "Couldn't find in bundle IQ packet: " + packet); return null; } - final Element prekeysElement = prekeysItem.findChild("prekeys"); + final Element bundleElement = item.findChild("bundle"); + if(bundleElement == null) { + return null; + } + final Element prekeysElement = bundleElement.findChild("prekeys"); if(prekeysElement == null) { - Log.d(Config.LOGTAG, "Couldn't find in preKeyPublic IQ packet: " + packet); + Log.d(Config.LOGTAG, "Couldn't find in bundle IQ packet: " + packet); return null; } for(Element preKeyPublicElement : prekeysElement.getChildren()) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 9a4cc276f..f96e5d7eb 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -275,8 +275,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } syncDirtyContacts(account); account.getAxolotlService().publishOwnDeviceIdIfNeeded(); - account.getAxolotlService().publishBundleIfNeeded(); - account.getAxolotlService().publishPreKeysIfNeeded(); + account.getAxolotlService().publishBundlesIfNeeded(); scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); } else if (account.getStatus() == Account.State.OFFLINE) { From cb7980c65ed5d91296e3ad571298dbed434707c0 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:19:17 +0200 Subject: [PATCH 016/166] Use bareJid for own session retrieval --- .../eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 22e959eb8..e4c0480ae 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -591,7 +591,7 @@ public class AxolotlService { } private Set findOwnSessions() { - AxolotlAddress ownAddress = getAddressForJid(account.getJid()); + AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid()); Set ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values()); return ownDeviceSessions; } From 3815d4efa378846c8aef840ad659268a0bef1536 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:22:26 +0200 Subject: [PATCH 017/166] Fetch bundles on-demand, encrypt in background Bundles are now fetched on demand when a session needs to be established. This should lessen the chance of changes to the bundles occuring before they're used, as well as lessen the load of fetching bundles. Also, the message encryption is now done in a background thread, as this can be somewhat costly if many sessions are present. This is probably not going to be an issue in real use, but it's good practice anyway. --- .../crypto/axolotl/AxolotlService.java | 133 ++++++++++++------ .../conversations/entities/Conversation.java | 6 +- .../generator/MessageGenerator.java | 10 +- .../conversations/parser/MessageParser.java | 12 +- .../services/XmppConnectionService.java | 13 +- 5 files changed, 109 insertions(+), 65 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index e4c0480ae..54394dd28 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.crypto.axolotl; +import android.support.annotation.Nullable; import android.util.Base64; import android.util.Log; @@ -42,13 +43,16 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.SerialSingleThreadExecutor; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public class AxolotlService { @@ -62,8 +66,9 @@ public class AxolotlService { private final XmppConnectionService mXmppConnectionService; private final SQLiteAxolotlStore axolotlStore; private final SessionMap sessions; - private final BundleMap bundleCache; private final Map> deviceIds; + private final FetchStatusMap fetchStatusMap; + private final SerialSingleThreadExecutor executor; private int ownDeviceId; public static class SQLiteAxolotlStore implements AxolotlStore { @@ -560,7 +565,13 @@ public class AxolotlService { } - private static class BundleMap extends AxolotlAddressMap { + private static enum FetchStatus { + PENDING, + SUCCESS, + ERROR + } + + private static class FetchStatusMap extends AxolotlAddressMap { } @@ -570,7 +581,8 @@ public class AxolotlService { this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); this.deviceIds = new HashMap<>(); this.sessions = new SessionMap(axolotlStore, account); - this.bundleCache = new BundleMap(); + this.fetchStatusMap = new FetchStatusMap(); + this.executor = new SerialSingleThreadExecutor(); this.ownDeviceId = axolotlStore.getLocalRegistrationId(); } @@ -793,56 +805,93 @@ public class AxolotlService { } } - private void createSessionsIfNeeded(Contact contact) throws NoSessionsCreatedException { + private boolean createSessionsIfNeeded(Conversation conversation) { + boolean newSessions = false; Log.d(Config.LOGTAG, "Creating axolotl sessions if needed..."); - AxolotlAddress address = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0); - for(Integer deviceId: bundleCache.getAll(address).keySet()) { - Log.d(Config.LOGTAG, "Processing device ID: " + deviceId); - AxolotlAddress remoteAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), deviceId); - if(sessions.get(remoteAddress) == null) { - Log.d(Config.LOGTAG, "Building new sesstion for " + deviceId); - SessionBuilder builder = new SessionBuilder(this.axolotlStore, remoteAddress); - try { - builder.process(bundleCache.get(remoteAddress)); - XmppAxolotlSession session = new XmppAxolotlSession(this.axolotlStore, remoteAddress); - sessions.put(remoteAddress, session); - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": InvalidKeyException, " +e.getMessage()); - } catch (UntrustedIdentityException e) { - Log.d(Config.LOGTAG, "Error building session for " + deviceId+ ": UntrustedIdentityException, " +e.getMessage()); - } - } else { - Log.d(Config.LOGTAG, "Already have session for " + deviceId); + Jid contactJid = conversation.getContact().getJid().toBareJid(); + Set addresses = new HashSet<>(); + if(deviceIds.get(contactJid) != null) { + for(Integer foreignId:this.deviceIds.get(contactJid)) { + Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+foreignId); + addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); + } + } else { + Log.e(Config.LOGTAG, "Have no target devices in PEP!"); + } + Log.d(Config.LOGTAG, "Checking own account "+account.getJid().toBareJid()); + if(deviceIds.get(account.getJid().toBareJid()) != null) { + for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) { + Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+ownId); + addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId)); } } - if(!this.hasAny(contact)) { - Log.e(Config.LOGTAG, "No Axolotl sessions available!"); - throw new NoSessionsCreatedException(); // FIXME: proper error handling + for (AxolotlAddress address : addresses) { + Log.d(Config.LOGTAG, "Processing device: " + address.toString()); + FetchStatus status = fetchStatusMap.get(address); + XmppAxolotlSession session = sessions.get(address); + if ( session == null && ( status == null || status == FetchStatus.ERROR) ) { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(conversation, address); + newSessions = true; + } else { + Log.d(Config.LOGTAG, "Already have session for " + address.toString()); + } } + return newSessions; } - public XmppAxolotlMessage processSending(Contact contact, String outgoingMessage) throws NoSessionsCreatedException { - XmppAxolotlMessage message = new XmppAxolotlMessage(contact, ownDeviceId, outgoingMessage); - createSessionsIfNeeded(contact); - Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); + @Nullable + public XmppAxolotlMessage encrypt(Message message ){ + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact(), + ownDeviceId, message.getBody()); - for(XmppAxolotlSession session : findSessionsforContact(contact)) { -// if(!session.isTrusted()) { - // TODO: handle this properly - // continue; - // } - message.addHeader(session.processSending(message.getInnerKey())); + if(findSessionsforContact(axolotlMessage.getContact()).isEmpty()) { + return null; + } + Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); + for (XmppAxolotlSession session : findSessionsforContact(axolotlMessage.getContact())) { + Log.d(Config.LOGTAG, session.remoteAddress.toString()); + //if(!session.isTrusted()) { + // TODO: handle this properly + // continue; + // } + axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey())); } Log.d(Config.LOGTAG, "Building axolotl own headers..."); - for(XmppAxolotlSession session : findOwnSessions()) { - // if(!session.isTrusted()) { - // TODO: handle this properly - // continue; - // } - message.addHeader(session.processSending(message.getInnerKey())); + for (XmppAxolotlSession session : findOwnSessions()) { + Log.d(Config.LOGTAG, session.remoteAddress.toString()); + // if(!session.isTrusted()) { + // TODO: handle this properly + // continue; + // } + axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey())); } - return message; + return axolotlMessage; + } + + private void processSending(final Message message) { + executor.execute(new Runnable() { + @Override + public void run() { + MessagePacket packet = mXmppConnectionService.getMessageGenerator() + .generateAxolotlChat(message); + if (packet == null) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + } else { + mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); + mXmppConnectionService.sendMessagePacket(account, packet); + } + } + }); + } + + public void sendMessage(Message message) { + boolean newSessions = createSessionsIfNeeded(message.getConversation()); + + if (!newSessions) { + this.processSending(message); + } } public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 289ed4ea5..2efd8a290 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -179,13 +179,13 @@ public class Conversation extends AbstractEntity implements Blockable { } } - public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) { + public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) { synchronized (this.messages) { for (Message message : this.messages) { if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) - && (message.getEncryption() == Message.ENCRYPTION_OTR)) { + && (message.getEncryption() == encryptionType)) { onMessageFound.onMessageFound(message); - } + } } } } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 0b6a7c614..b0727690b 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -66,16 +66,18 @@ public class MessageGenerator extends AbstractGenerator { delay.setAttribute("stamp", mDateFormat.format(date)); } - public MessagePacket generateAxolotlChat(Message message) throws NoSessionsCreatedException{ + public MessagePacket generateAxolotlChat(Message message) { return generateAxolotlChat(message, false); } - public MessagePacket generateAxolotlChat(Message message, boolean addDelay) throws NoSessionsCreatedException{ + public MessagePacket generateAxolotlChat(Message message, boolean addDelay) { MessagePacket packet = preparePacket(message, addDelay); AxolotlService service = message.getConversation().getAccount().getAxolotlService(); Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing..."); - XmppAxolotlMessage axolotlMessage = service.processSending(message.getContact(), - message.getBody()); + XmppAxolotlMessage axolotlMessage = service.encrypt(message); + if (axolotlMessage == null) { + return null; + } packet.setAxolotlMessage(axolotlMessage.toXml()); return packet; } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index de7057308..31f70b97b 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -7,6 +7,7 @@ import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; import java.util.List; +import java.util.Set; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -105,6 +106,7 @@ public class MessageParser extends AbstractParser implements XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); if(plaintextMessage != null) { finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED); + finishedMessage.setAxolotlSession(plaintextMessage.getSession()); } return finishedMessage; @@ -189,15 +191,9 @@ public class MessageParser extends AbstractParser implements } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing..."); Element item = items.findChild("item"); - List deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); + Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); AxolotlService axolotlService = account.getAxolotlService(); - if(account.getJid().toBareJid().equals(from)) { - } else { - Contact contact = account.getRoster().getContact(from); - for (Integer deviceId : deviceIds) { - axolotlService.fetchBundleIfNeeded(contact, deviceId); - } - } + axolotlService.registerDevices(from, deviceIds); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index f96e5d7eb..8b7161210 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -703,7 +703,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) { message.getConversation().endOtrIfNeeded(); - message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, + new Conversation.OnMessageFound() { @Override public void onMessageFound(Message message) { markMessage(message,Message.STATUS_SEND_FAILED); @@ -758,12 +759,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } break; case Message.ENCRYPTION_AXOLOTL: - try { - packet = mMessageGenerator.generateAxolotlChat(message); - Log.d(Config.LOGTAG, "Succeeded generating axolotl chat message!"); - } catch (NoSessionsCreatedException e) { - message.setStatus(Message.STATUS_WAITING); - } + message.setStatus(Message.STATUS_WAITING); + account.getAxolotlService().sendMessage(message); break; } @@ -1797,7 +1794,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa account.getJid().toBareJid() + " otr session established with " + conversation.getJid() + "/" + otrSession.getSessionID().getUserID()); - conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() { @Override public void onMessageFound(Message message) { From 1b0596d57473f7aafa633ed2d5f3b3610a653a51 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:25:23 +0200 Subject: [PATCH 018/166] Tag messages with originating session This can be used later in order to display trust status of messages, as well as for potential resending of messages in case of preKey conflicts. --- .../conversations/crypto/axolotl/XmppAxolotlMessage.java | 5 +++++ src/main/java/eu/siacs/conversations/entities/Message.java | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index e1b956507..01b06c226 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -75,6 +75,11 @@ public class XmppAxolotlMessage { public String getPlaintext() { return plaintext; } + + public AxolotlService.XmppAxolotlSession getSession() { + return session; + } + } public XmppAxolotlMessage(Contact contact, Element axolotlMessage) { diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 336af9721..b1dffc820 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -670,4 +670,8 @@ public class Message extends AbstractEntity { public boolean isTrusted() { return this.axolotlSession != null && this.axolotlSession.isTrusted(); } + + public void setAxolotlSession(AxolotlService.XmppAxolotlSession session) { + this.axolotlSession = session; + } } From 992cf5652e37f075bfcd9ed4f862fe407f3404dc Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:30:23 +0200 Subject: [PATCH 019/166] When receiving, add mock session if none exists We need a session object in order to build a session from a PreKeyWhisperMessage, so add an empty one when none exists on receiving a message. Warning: this will break right now if the session can not be constructed from the received message.There will be an invalid session which will break if we try to send using it. --- .../eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 54394dd28..6a493e548 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -904,7 +904,7 @@ public class AxolotlService { Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message); // TODO: handle this properly session = new XmppAxolotlSession(axolotlStore, senderAddress); - + sessions.put(senderAddress,session); } for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { From 9a0232f7e7271209afb04395091c6ca7016fed09 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Mon, 29 Jun 2015 14:33:43 +0200 Subject: [PATCH 020/166] Formatting fixes --- .../eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 6a493e548..1b591f781 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -901,13 +901,13 @@ public class AxolotlService { XmppAxolotlSession session = sessions.get(senderAddress); if (session == null) { - Log.d(Config.LOGTAG, "No axolotl session found while parsing received message " + message); + Log.d(Config.LOGTAG, "Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); // TODO: handle this properly session = new XmppAxolotlSession(axolotlStore, senderAddress); sessions.put(senderAddress,session); } - for(XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { + for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { if (header.getRecipientDeviceId() == ownDeviceId) { Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing..."); byte[] payloadKey = session.processReceiving(header); From 18c1e15d002f415c4449afe06e6dc80aef5aeade Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 3 Jul 2015 13:20:27 +0200 Subject: [PATCH 021/166] Rework PEP content verification Now checks which part(s) are out of sync w/ local storage, and updates only those, rather than assuming the entire node corrupt and overwriting it all (especially relevant for preKey list) --- .../crypto/axolotl/AxolotlService.java | 108 ++++++++++-------- .../conversations/generator/IqGenerator.java | 2 +- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 1b591f781..470c52067 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -655,42 +655,6 @@ public class AxolotlService { }); } - private boolean validateBundle(PreKeyBundle bundle) { - if (bundle == null || bundle.getIdentityKey() == null - || bundle.getSignedPreKey() == null || bundle.getSignedPreKeySignature() == null) { - return false; - } - - try { - SignedPreKeyRecord signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); - IdentityKey identityKey = axolotlStore.getIdentityKeyPair().getPublicKey(); - Log.d(Config.LOGTAG,"own identity key:"+identityKey.getFingerprint()+", foreign: "+bundle.getIdentityKey().getFingerprint()); - Log.d(Config.LOGTAG,"bundle: "+Boolean.toString(bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())) - +" " + Boolean.toString(Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) - +" " + Boolean.toString( bundle.getIdentityKey().equals(identityKey))); - return bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) - && Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature()) - && bundle.getIdentityKey().equals(identityKey); - } catch (InvalidKeyIdException ignored) { - return false; - } - } - - private boolean validatePreKeys(Map keys) { - if(keys == null) { return false; } - for(Integer id:keys.keySet()) { - try { - PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); - if(!preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { - return false; - } - } catch (InvalidKeyIdException ignored) { - return false; - } - } - return true; - } - public void publishBundlesIfNeeded() { IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), ownDeviceId); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @@ -698,25 +662,75 @@ public class AxolotlService { public void onIqPacketReceived(Account account, IqPacket packet) { PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet); Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); - SignedPreKeyRecord signedPreKeyRecord; - List preKeyRecords; - if (!validateBundle(bundle) || keys.isEmpty() || !validatePreKeys(keys)) { + boolean flush = false; + if (bundle == null) { + Log.e(Config.LOGTAG, "Received invalid bundle:" + packet); + bundle = new PreKeyBundle(-1, -1, -1 , null, -1, null, null, null); + flush = true; + } + if (keys == null) { + Log.e(Config.LOGTAG, "Received invalid prekeys:" + packet); + } + try { + boolean changed = false; + // Validate IdentityKey + IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); + if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { + Log.d(Config.LOGTAG, "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + changed = true; + } + + // Validate signedPreKeyRecord + ID + SignedPreKeyRecord signedPreKeyRecord; int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size(); try { - signedPreKeyRecord = KeyHelper.generateSignedPreKey( - axolotlStore.getIdentityKeyPair(), numSignedPreKeys + 1); + signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId()); + if ( flush + ||!bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) + || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { + Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); + axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } + } catch (InvalidKeyIdException e) { + Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); + changed = true; + } - preKeyRecords = KeyHelper.generatePreKeys( - axolotlStore.getCurrentPreKeyId(), NUM_KEYS_TO_PUBLISH); - for (PreKeyRecord record : preKeyRecords) { + // Validate PreKeys + Set preKeyRecords = new HashSet<>(); + if (keys != null) { + for (Integer id : keys.keySet()) { + try { + PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id); + if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) { + preKeyRecords.add(preKeyRecord); + } + } catch (InvalidKeyIdException ignored) { + } + } + } + int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size(); + if (newKeys > 0) { + List newRecords = KeyHelper.generatePreKeys( + axolotlStore.getCurrentPreKeyId()+1, newKeys); + preKeyRecords.addAll(newRecords); + for (PreKeyRecord record : newRecords) { axolotlStore.storePreKey(record.getId(), record); } + changed = true; + Log.d(Config.LOGTAG, "Adding " + newKeys + " new preKeys to PEP."); + } + + if(changed) { IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), preKeyRecords, ownDeviceId); - Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " not in PEP. Publishing: " + publish); + Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -724,10 +738,10 @@ public class AxolotlService { Log.d(Config.LOGTAG, "Published bundle, got: " + packet); } }); - } catch (InvalidKeyException e) { + } + } catch (InvalidKeyException e) { Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); return; - } } } }); diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 0bef88532..19c1d4f73 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -152,7 +152,7 @@ public class IqGenerator extends AbstractGenerator { } public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey, - final List preKeyRecords, final int deviceId) { + final Set preKeyRecords, final int deviceId) { final Element item = new Element("item"); final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX); final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic"); From ec6870307e0ecee8184ddfef73444290e9d15828 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 3 Jul 2015 13:27:35 +0200 Subject: [PATCH 022/166] Properly track message sender Previously, the sender was assumed to be the conversation counterpart. This broke carboned own-device messages. We now track the sender properly, and also set the status (sent by one of the own devices vs received from the counterpart) accordingly. --- .../crypto/axolotl/AxolotlService.java | 8 ++++---- .../crypto/axolotl/XmppAxolotlMessage.java | 15 ++++++++------- .../siacs/conversations/parser/MessageParser.java | 8 ++++---- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 470c52067..d4089b202 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -856,14 +856,14 @@ public class AxolotlService { @Nullable public XmppAxolotlMessage encrypt(Message message ){ - final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact(), + final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(), ownDeviceId, message.getBody()); - if(findSessionsforContact(axolotlMessage.getContact()).isEmpty()) { + if(findSessionsforContact(message.getContact()).isEmpty()) { return null; } Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); - for (XmppAxolotlSession session : findSessionsforContact(axolotlMessage.getContact())) { + for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) { Log.d(Config.LOGTAG, session.remoteAddress.toString()); //if(!session.isTrusted()) { // TODO: handle this properly @@ -910,7 +910,7 @@ public class AxolotlService { public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; - AxolotlAddress senderAddress = new AxolotlAddress(message.getContact().getJid().toBareJid().toString(), + AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), message.getSenderDeviceId()); XmppAxolotlSession session = sessions.get(senderAddress); diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index 01b06c226..06dd2cdaf 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -19,13 +19,14 @@ import javax.crypto.spec.SecretKeySpec; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.jid.Jid; public class XmppAxolotlMessage { private byte[] innerKey; private byte[] ciphertext; private byte[] iv; private final Set headers; - private final Contact contact; + private final Jid from; private final int sourceDeviceId; public static class XmppAxolotlMessageHeader { @@ -82,8 +83,8 @@ public class XmppAxolotlMessage { } - public XmppAxolotlMessage(Contact contact, Element axolotlMessage) { - this.contact = contact; + public XmppAxolotlMessage(Jid from, Element axolotlMessage) { + this.from = from; this.sourceDeviceId = Integer.parseInt(axolotlMessage.getAttribute("id")); this.headers = new HashSet<>(); for(Element child:axolotlMessage.getChildren()) { @@ -101,8 +102,8 @@ public class XmppAxolotlMessage { } } - public XmppAxolotlMessage(Contact contact, int sourceDeviceId, String plaintext) { - this.contact = contact; + public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) { + this.from = from; this.sourceDeviceId = sourceDeviceId; this.headers = new HashSet<>(); this.encrypt(plaintext); @@ -124,8 +125,8 @@ public class XmppAxolotlMessage { } } - public Contact getContact() { - return this.contact; + public Jid getFrom() { + return this.from; } public int getSenderDeviceId() { diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 31f70b97b..cc878e7fb 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -99,13 +99,13 @@ public class MessageParser extends AbstractParser implements } } - private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation) { + private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) { Message finishedMessage = null; AxolotlService service = conversation.getAccount().getAxolotlService(); - XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(conversation.getContact(), axolotlMessage); + XmppAxolotlMessage xmppAxolotlMessage = new XmppAxolotlMessage(from.toBareJid(), axolotlMessage); XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); if(plaintextMessage != null) { - finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, Message.STATUS_RECEIVED); + finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); finishedMessage.setAxolotlSession(plaintextMessage.getSession()); } @@ -322,7 +322,7 @@ public class MessageParser extends AbstractParser implements } else if (pgpEncrypted != null) { message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); } else if (axolotlEncrypted != null) { - message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation); + message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status); if (message == null) { return; } From 69600502d217334031732cef2e80ebd4461e522d Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 3 Jul 2015 13:31:14 +0200 Subject: [PATCH 023/166] Fix asynchronous axolotl message sending XmppConnectionService.sendMessage() now dispatches messages to the AxolotlService, where they only are prepared for sending and cached. AxolotlService now triggers a XmppConnectionService.resendMessage(), which then handles sending the cached message packet. This transparently fixes, e.g., handling of messages sent while we are offline. --- .../crypto/axolotl/AxolotlService.java | 29 +++++++++++++++---- .../services/XmppConnectionService.java | 6 ++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index d4089b202..d79fa1bf8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -67,6 +67,7 @@ public class AxolotlService { private final SQLiteAxolotlStore axolotlStore; private final SessionMap sessions; private final Map> deviceIds; + private final Map messageCache; private final FetchStatusMap fetchStatusMap; private final SerialSingleThreadExecutor executor; private int ownDeviceId; @@ -580,6 +581,7 @@ public class AxolotlService { this.account = account; this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); this.deviceIds = new HashMap<>(); + this.messageCache = new HashMap<>(); this.sessions = new SessionMap(axolotlStore, account); this.fetchStatusMap = new FetchStatusMap(); this.executor = new SerialSingleThreadExecutor(); @@ -892,22 +894,37 @@ public class AxolotlService { .generateAxolotlChat(message); if (packet == null) { mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); + //mXmppConnectionService.updateConversationUi(); } else { - mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); - mXmppConnectionService.sendMessagePacket(account, packet); + Log.d(Config.LOGTAG, "Generated message, caching: " + message.getUuid()); + messageCache.put(message.getUuid(), packet); + mXmppConnectionService.resendMessage(message); } } }); } - public void sendMessage(Message message) { - boolean newSessions = createSessionsIfNeeded(message.getConversation()); + public void prepareMessage(Message message) { + if (!messageCache.containsKey(message.getUuid())) { + boolean newSessions = createSessionsIfNeeded(message.getConversation()); - if (!newSessions) { - this.processSending(message); + if (!newSessions) { + this.processSending(message); + } } } + public MessagePacket fetchPacketFromCache(Message message) { + MessagePacket packet = messageCache.get(message.getUuid()); + if (packet != null) { + Log.d(Config.LOGTAG, "Cache hit: " + message.getUuid()); + messageCache.remove(message.getUuid()); + } else { + Log.d(Config.LOGTAG, "Cache miss: " + message.getUuid()); + } + return packet; + } + public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceiving(XmppAxolotlMessage message) { XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null; AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 8b7161210..3e443d75f 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -52,7 +52,6 @@ import de.duenndns.ssl.MemorizingTrustManager; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.crypto.axolotl.NoSessionsCreatedException; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Bookmark; @@ -760,7 +759,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa break; case Message.ENCRYPTION_AXOLOTL: message.setStatus(Message.STATUS_WAITING); - account.getAxolotlService().sendMessage(message); + packet = account.getAxolotlService().fetchPacketFromCache(message); + if (packet == null && account.isOnlineAndConnected()) { + account.getAxolotlService().prepareMessage(message); + } break; } From bf4185ac08a42e9d16bf1e2fc0126bff467a55be Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 3 Jul 2015 13:34:34 +0200 Subject: [PATCH 024/166] Refresh PEP on session establish We now track preKeys used to establish incoming sessions with us. On each new established session, we remove the used prekey from PEP. We have to do this because libaxolotl-java internally clears the used preKey from its storage, so we will not be able to establish any future sessions using that key. --- .../crypto/axolotl/AxolotlService.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index d79fa1bf8..420c75b5b 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -444,6 +444,7 @@ public class AxolotlService { public static class XmppAxolotlSession { private SessionCipher cipher; private boolean isTrusted = false; + private Integer preKeyId = null; private SQLiteAxolotlStore sqLiteAxolotlStore; private AxolotlAddress remoteAddress; @@ -463,6 +464,14 @@ public class AxolotlService { return this.isTrusted; } + public Integer getPreKeyId() { + return preKeyId; + } + + public void resetPreKeyId() { + preKeyId = null; + } + public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { byte[] plaintext = null; try { @@ -470,6 +479,9 @@ public class AxolotlService { PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); Log.d(Config.LOGTAG, "PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); plaintext = cipher.decrypt(message); + if (message.getPreKeyId().isPresent()) { + preKeyId = message.getPreKeyId().get(); + } } catch (InvalidMessageException | InvalidVersionException e) { WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); plaintext = cipher.decrypt(message); @@ -946,6 +958,12 @@ public class AxolotlService { Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message..."); plaintextMessage = message.decrypt(session, payloadKey); } + Integer preKeyId = session.getPreKeyId(); + if (preKeyId != null) { + publishBundlesIfNeeded(); + session.resetPreKeyId(); + } + break; } } From 25450bf6d365e4bb71addd38275296575f3a5658 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 5 Jul 2015 22:10:43 +0200 Subject: [PATCH 025/166] Trust all IdentityKeys The trust-on-first-use policy leads to problems when receiving messages from two different devices of a contact before sending a message to them (as their IdentityKeys will not have been added yet). Since session trust will be managed externally anyway, this change is not a security problem, and will allow us to decrypt messages from yet-untrusted sessions. --- .../siacs/conversations/crypto/axolotl/AxolotlService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 420c75b5b..cdd8d85d9 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -233,8 +233,9 @@ public class AxolotlService { */ @Override public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - Set trustedKeys = mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name); - return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); + //Set trustedKeys = mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name); + //return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); + return true; } // -------------------------------------- From 6867b5c3abeeb5116a2542c56a706b733fd9cbf0 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 5 Jul 2015 22:53:34 +0200 Subject: [PATCH 026/166] Return empty set on invalid PEP devicelist --- .../crypto/axolotl/AxolotlService.java | 3 +- .../siacs/conversations/parser/IqParser.java | 36 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index cdd8d85d9..d37879c3d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.crypto.axolotl; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Base64; import android.util.Log; @@ -638,7 +639,7 @@ public class AxolotlService { return ownDeviceId; } - public void registerDevices(final Jid jid, final Set deviceIds) { + public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { for(Integer i:deviceIds) { Log.d(Config.LOGTAG, "Adding Device ID:"+ jid + ":"+i); } diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 935517877..c147978e0 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.parser; +import android.support.annotation.NonNull; import android.util.Base64; import android.util.Log; @@ -96,26 +97,25 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { return items.findChild("item"); } + @NonNull public Set deviceIds(final Element item) { Set deviceIds = new HashSet<>(); - if (item == null) { - return null; - } - final Element list = item.findChild("list"); - if(list == null) { - return null; - } - for(Element device : list.getChildren()) { - if(!device.getName().equals("device")) { - continue; - } - try { - Integer id = Integer.valueOf(device.getAttribute("id")); - deviceIds.add(id); - } catch (NumberFormatException e) { - Log.e(Config.LOGTAG, "Encountered nvalid node in PEP:" + device.toString() - + ", skipping..."); - continue; + if (item != null) { + final Element list = item.findChild("list"); + if (list != null) { + for (Element device : list.getChildren()) { + if (!device.getName().equals("device")) { + continue; + } + try { + Integer id = Integer.valueOf(device.getAttribute("id")); + deviceIds.add(id); + } catch (NumberFormatException e) { + Log.e(Config.LOGTAG, "Encountered nvalid node in PEP:" + device.toString() + + ", skipping..."); + continue; + } + } } } return deviceIds; From 0cf64857cfa8d42b8759ca2934af91d6060c55a5 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 5 Jul 2015 22:54:28 +0200 Subject: [PATCH 027/166] Only cache session if successfully established When receiving a message, only remember the XmppAxolotlSession wrapper if the prospective session was actually established. This prevents us from erroneously adding empty sessions that are never established using received PreKeyWhisperMessages, which would lead to errors if we try to use them for sending. --- .../siacs/conversations/crypto/axolotl/AxolotlService.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index d37879c3d..faa0e5add 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -944,12 +944,13 @@ public class AxolotlService { AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(), message.getSenderDeviceId()); + boolean newSession = false; XmppAxolotlSession session = sessions.get(senderAddress); if (session == null) { Log.d(Config.LOGTAG, "Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); // TODO: handle this properly session = new XmppAxolotlSession(axolotlStore, senderAddress); - sessions.put(senderAddress,session); + newSession = true; } for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { @@ -969,6 +970,10 @@ public class AxolotlService { } } + if (newSession && plaintextMessage != null) { + sessions.put(senderAddress,session); + } + return plaintextMessage; } } From 491f623708437f418497ecace2876e9c81708e72 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Tue, 7 Jul 2015 19:27:12 +0200 Subject: [PATCH 028/166] Fix displaying Contact IdentityKeys Migrate ContactDetailsActivity to use new SQL IdentityKeys storage, remove dead code from Contact class. --- .../siacs/conversations/entities/Contact.java | 64 ------------------- .../ui/ContactDetailsActivity.java | 3 +- 2 files changed, 2 insertions(+), 65 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 240d5223c..fdb5f932a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -350,70 +350,6 @@ public class Contact implements ListItem, Blockable { } } - public List getAxolotlIdentityKeys() { - synchronized (this.keys) { - JSONArray serializedKeyItems = this.keys.optJSONArray("axolotl_identity_key"); - List identityKeys = new ArrayList<>(); - List toDelete = new ArrayList<>(); - if(serializedKeyItems != null) { - for(int i = 0; i Date: Tue, 7 Jul 2015 19:28:35 +0200 Subject: [PATCH 029/166] Adapt prettifyFingerprint() to axolotl FP sizes --- .../siacs/conversations/ui/ContactDetailsActivity.java | 2 +- .../java/eu/siacs/conversations/utils/CryptoHelper.java | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 2777b8148..33861f828 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -387,7 +387,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd .findViewById(R.id.button_remove); remove.setVisibility(View.VISIBLE); keyType.setText("Axolotl Fingerprint"); - key.setText(identityKey.getFingerprint()); + key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); keys.addView(view); remove.setOnClickListener(new OnClickListener() { diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index 2dec203d9..c7c9ac423 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -96,11 +96,10 @@ public final class CryptoHelper { } else if (fingerprint.length() < 40) { return fingerprint; } - StringBuilder builder = new StringBuilder(fingerprint); - builder.insert(8, " "); - builder.insert(17, " "); - builder.insert(26, " "); - builder.insert(35, " "); + StringBuilder builder = new StringBuilder(fingerprint.replaceAll("\\s","")); + for(int i=8;i Date: Tue, 7 Jul 2015 19:30:08 +0200 Subject: [PATCH 030/166] Refactor axolotl database recreation --- .../persistance/DatabaseBackend.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index c70ffad2c..a3868a1d9 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -283,14 +283,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { cursor.close(); } if (oldVersion < 15 && newVersion >= 15) { - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME); - db.execSQL(CREATE_SESSIONS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME); - db.execSQL(CREATE_PREKEYS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); - db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); - db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME); - db.execSQL(CREATE_IDENTITIES_STATEMENT); + recreateAxolotlDb(); } } @@ -868,4 +861,17 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { storeIdentityKey(account, name, true, Base64.encodeToString(identityKeyPair.serialize(),Base64.DEFAULT)); } + + public void recreateAxolotlDb() { + Log.d(Config.LOGTAG, ">>> (RE)CREATING AXOLOTL DATABASE <<<"); + SQLiteDatabase db = this.getWritableDatabase(); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME); + db.execSQL(CREATE_SESSIONS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME); + db.execSQL(CREATE_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME); + db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); + db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME); + db.execSQL(CREATE_IDENTITIES_STATEMENT); + } } From 968410ae33f7ed341868847f3fedbd03ebd54f2b Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Tue, 7 Jul 2015 19:32:52 +0200 Subject: [PATCH 031/166] Fix devicelist update handling No longer store own device ID (so that we don't encrypt messages for ourselves), verify that own device ID is present in update list (otherwise republish), reflect update in UI. --- .../siacs/conversations/crypto/axolotl/AxolotlService.java | 5 +++++ .../java/eu/siacs/conversations/parser/MessageParser.java | 1 + 2 files changed, 6 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index faa0e5add..2b0954c6d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -640,10 +640,15 @@ public class AxolotlService { } public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { + if(deviceIds.contains(getOwnDeviceId())) { + Log.d(Config.LOGTAG, "Skipping own Device ID:"+ jid + ":"+getOwnDeviceId()); + deviceIds.remove(getOwnDeviceId()); + } for(Integer i:deviceIds) { Log.d(Config.LOGTAG, "Adding Device ID:"+ jid + ":"+i); } this.deviceIds.put(jid, deviceIds); + publishOwnDeviceIdIfNeeded(); } public void publishOwnDeviceIdIfNeeded() { diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index cc878e7fb..0dc8d5215 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -194,6 +194,7 @@ public class MessageParser extends AbstractParser implements Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); AxolotlService axolotlService = account.getAxolotlService(); axolotlService.registerDevices(from, deviceIds); + mXmppConnectionService.updateAccountUi(); } } From 37b214a8a853dfdc1760bc69d9213484e519832f Mon Sep 17 00:00:00 2001 From: Christian S Date: Sun, 19 Jul 2015 14:03:46 +0200 Subject: [PATCH 032/166] show contact details in conference details ... --- .../siacs/conversations/ui/ConferenceDetailsActivity.java | 5 +++++ src/main/res/menu/muc_details_context.xml | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 07b8819d9..614a6648e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -274,6 +274,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers name = user.getJid().toBareJid().toString(); } menu.setHeaderTitle(name); + MenuItem showContactDetails = menu.findItem(R.id.action_contact_details); MenuItem startConversation = menu.findItem(R.id.start_conversation); MenuItem giveMembership = menu.findItem(R.id.give_membership); MenuItem removeMembership = menu.findItem(R.id.remove_membership); @@ -282,6 +283,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room); MenuItem banFromConference = menu.findItem(R.id.ban_from_conference); startConversation.setVisible(true); + showContactDetails.setVisible(true); if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) && self.getAffiliation().outranks(user.getAffiliation())) { if (mAdvancedMode) { @@ -309,6 +311,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { + case R.id.action_contact_details: + switchToContactDetails(mSelectedUser.getContact()); + return true; case R.id.start_conversation: startConversation(mSelectedUser); return true; diff --git a/src/main/res/menu/muc_details_context.xml b/src/main/res/menu/muc_details_context.xml index dc0f5d3eb..592e44af1 100644 --- a/src/main/res/menu/muc_details_context.xml +++ b/src/main/res/menu/muc_details_context.xml @@ -1,5 +1,9 @@ + - \ No newline at end of file + From 7049904c326e6dabc02138fa436d209bf724e0bc Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Tue, 7 Jul 2015 19:36:22 +0200 Subject: [PATCH 033/166] Add basic PEP managemend UI to EditAccountActivity EditAccountActivity now show own fingerprint, and gives an option to regenerate local keying material (and wipe all sessions associated with the old keys in the process). It also now displays a list of other own devices, and gives an option to remove all but the current device. --- .../crypto/axolotl/AxolotlService.java | 33 ++++++ .../persistance/DatabaseBackend.java | 21 ++++ .../conversations/ui/EditAccountActivity.java | 109 +++++++++++++++++- src/main/res/layout/activity_edit_account.xml | 101 ++++++++++++++++ src/main/res/values/strings.xml | 6 + 5 files changed, 266 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 2b0954c6d..ec4eb7c57 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -176,6 +176,13 @@ public class AxolotlService { return reg_id; } + public void regenerate() { + mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); + account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); + identityKeyPair = loadIdentityKeyPair(); + currentPreKeyId = 0; + mXmppConnectionService.updateAccountUi(); + } /** * Get the local client's identity key pair. @@ -602,6 +609,10 @@ public class AxolotlService { this.ownDeviceId = axolotlStore.getLocalRegistrationId(); } + public IdentityKey getOwnPublicKey() { + return axolotlStore.getIdentityKeyPair().getPublicKey(); + } + public void trustSession(AxolotlAddress counterpart) { XmppAxolotlSession session = sessions.get(counterpart); if (session != null) { @@ -635,10 +646,19 @@ public class AxolotlService { return sessions.hasAny(contactAddress); } + public void regenerateKeys() { + axolotlStore.regenerate(); + publishBundlesIfNeeded(); + } + public int getOwnDeviceId() { return ownDeviceId; } + public Set getOwnDeviceIds() { + return this.deviceIds.get(account.getJid().toBareJid()); + } + public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { if(deviceIds.contains(getOwnDeviceId())) { Log.d(Config.LOGTAG, "Skipping own Device ID:"+ jid + ":"+getOwnDeviceId()); @@ -651,6 +671,19 @@ public class AxolotlService { publishOwnDeviceIdIfNeeded(); } + public void wipeOtherPepDevices() { + Set deviceIds = new HashSet<>(); + deviceIds.add(getOwnDeviceId()); + IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); + Log.d(Config.LOGTAG, "Wiping all other devices from Pep:" + publish); + mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + // TODO: implement this! + } + }); + } + public void publishOwnDeviceIdIfNeeded() { IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid()); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index a3868a1d9..1aa2bade9 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -874,4 +874,25 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME); db.execSQL(CREATE_IDENTITIES_STATEMENT); } + + public void wipeAxolotlDb(Account account) { + String accountName = account.getUuid(); + Log.d(Config.LOGTAG, ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); + SQLiteDatabase db = this.getWritableDatabase(); + String[] deleteArgs= { + accountName + }; + db.delete(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(AxolotlService.SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + db.delete(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?", + deleteArgs); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 908c29d21..ab4dc0592 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1,9 +1,13 @@ package eu.siacs.conversations.ui; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; import android.app.PendingIntent; +import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.text.Editable; +import android.text.TextUtils; import android.text.TextWatcher; import android.view.Menu; import android.view.MenuItem; @@ -23,6 +27,8 @@ import android.widget.TableLayout; import android.widget.TextView; import android.widget.Toast; +import java.util.Set; + import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; @@ -54,9 +60,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private TextView mServerInfoPep; private TextView mSessionEst; private TextView mOtrFingerprint; + private TextView mAxolotlFingerprint; + private TextView mAxolotlDevicelist; private ImageView mAvatar; private RelativeLayout mOtrFingerprintBox; + private RelativeLayout mAxolotlFingerprintBox; + private RelativeLayout mAxolotlDevicelistBox; private ImageButton mOtrFingerprintToClipboardButton; + private ImageButton mAxolotlFingerprintToClipboardButton; + private ImageButton mWipeAxolotlPepButton; + private ImageButton mRegenerateAxolotlKeyButton; private Jid jidToEdit; private Account mAccount; @@ -310,6 +323,13 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box); this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); + this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint); + this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box); + this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); + this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key); + this.mAxolotlDevicelist = (TextView) findViewById(R.id.axolotl_devicelist); + this.mAxolotlDevicelistBox = (RelativeLayout) findViewById(R.id.axolotl_devices_box); + this.mWipeAxolotlPepButton = (ImageButton) findViewById(R.id.action_wipe_axolotl_pep); this.mSaveButton = (Button) findViewById(R.id.save_button); this.mCancelButton = (Button) findViewById(R.id.cancel_button); this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener); @@ -477,10 +497,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else { this.mServerInfoPep.setText(R.string.server_info_unavailable); } - final String fingerprint = this.mAccount.getOtrFingerprint(); - if (fingerprint != null) { + final String otrFingerprint = this.mAccount.getOtrFingerprint(); + if (otrFingerprint != null) { this.mOtrFingerprintBox.setVisibility(View.VISIBLE); - this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint)); + this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint)); this.mOtrFingerprintToClipboardButton .setVisibility(View.VISIBLE); this.mOtrFingerprintToClipboardButton @@ -489,7 +509,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate @Override public void onClick(final View v) { - if (copyTextToClipboard(fingerprint, R.string.otr_fingerprint)) { + if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) { Toast.makeText( EditAccountActivity.this, R.string.toast_message_otr_fingerprint, @@ -500,6 +520,55 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else { this.mOtrFingerprintBox.setVisibility(View.GONE); } + final Set ownDevices = this.mAccount.getAxolotlService().getOwnDeviceIds(); + if (ownDevices != null && !ownDevices.isEmpty()) { + this.mAxolotlDevicelistBox.setVisibility(View.VISIBLE); + this.mAxolotlDevicelist.setText(TextUtils.join(", ", ownDevices)); + this.mWipeAxolotlPepButton + .setVisibility(View.VISIBLE); + this.mWipeAxolotlPepButton + .setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + showWipePepDialog(); + } + }); + } else { + this.mAxolotlDevicelistBox.setVisibility(View.GONE); + } + final String axolotlFingerprint = this.mAccount.getAxolotlService().getOwnPublicKey().getFingerprint(); + if (axolotlFingerprint != null) { + this.mAxolotlFingerprintBox.setVisibility(View.VISIBLE); + this.mAxolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(axolotlFingerprint)); + this.mAxolotlFingerprintToClipboardButton + .setVisibility(View.VISIBLE); + this.mAxolotlFingerprintToClipboardButton + .setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(final View v) { + + if (copyTextToClipboard(axolotlFingerprint, R.string.axolotl_fingerprint)) { + Toast.makeText( + EditAccountActivity.this, + R.string.toast_message_axolotl_fingerprint, + Toast.LENGTH_SHORT).show(); + } + } + }); + this.mRegenerateAxolotlKeyButton + .setVisibility(View.VISIBLE); + this.mRegenerateAxolotlKeyButton + .setOnClickListener(new View.OnClickListener() { + + @Override + public void onClick(final View v) { + showRegenerateAxolotlKeyDialog(); + } + }); + } else { + this.mAxolotlFingerprintBox.setVisibility(View.GONE); + } } else { if (this.mAccount.errorStatus()) { this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId())); @@ -512,4 +581,36 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mStats.setVisibility(View.GONE); } } + + public void showRegenerateAxolotlKeyDialog() { + Builder builder = new Builder(this); + builder.setTitle("Regenerate Key"); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage("Are you sure you want to regenerate your Identity Key? (This will also wipe all established sessions and contact Identity Keys)"); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton("Yes", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mAccount.getAxolotlService().regenerateKeys(); + } + }); + builder.create().show(); + } + + public void showWipePepDialog() { + Builder builder = new Builder(this); + builder.setTitle("Wipe PEP"); + builder.setIconAttribute(android.R.attr.alertDialogIcon); + builder.setMessage("Are you sure you want to wipe all other devices from the PEP device ID list?"); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton("Yes", + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mAccount.getAxolotlService().wipeOtherPepDevices(); + } + }); + builder.create().show(); + } } diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index 98de84f56..df20e6f2d 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -342,6 +342,107 @@ android:visibility="visible" android:contentDescription="@string/copy_otr_clipboard_description"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 30a3e4d28..60ca5613e 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -207,6 +207,8 @@ Reception failed Your fingerprint OTR fingerprint + Axolotl fingerprint + Other own Axolotl Devices Verify Decrypt Conferences @@ -313,6 +315,7 @@ Conference name Use room’s subject instead of JID to identify conferences OTR fingerprint copied to clipboard! + Axolotl fingerprint copied to clipboard! You are banned from this conference This conference is members only You have been kicked from this conference @@ -379,6 +382,9 @@ Reset Account avatar Copy OTR fingerprint to clipboard + Copy Axolotl fingerprint to clipboard + Copy Axolotl fingerprint to clipboard + Wipe other devices from PEP Fetching history from server No more history on server Updating… From 3458f5bb9182ca99dd800e4cde21168323260da4 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 8 Jul 2015 17:44:24 +0200 Subject: [PATCH 034/166] Clean up logging Add a fixed prefix to axolotl-related log messages, set log levels sensibly. --- .../crypto/axolotl/AxolotlService.java | 115 ++++++++++-------- .../generator/MessageGenerator.java | 2 +- .../siacs/conversations/parser/IqParser.java | 15 +-- .../conversations/parser/MessageParser.java | 2 +- .../persistance/DatabaseBackend.java | 8 +- 5 files changed, 75 insertions(+), 67 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index ec4eb7c57..8ac69d37a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -61,6 +61,8 @@ public class AxolotlService { public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist"; public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles"; + public static final String LOGPREFIX = "AxolotlService"; + public static final int NUM_KEYS_TO_PUBLISH = 10; private final Account account; @@ -100,7 +102,7 @@ public class AxolotlService { private static IdentityKeyPair generateIdentityKeyPair() { - Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair..."); + Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl IdentityKeyPair..."); ECKeyPair identityKeyPairKeys = Curve.generateKeyPair(); IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()), identityKeyPairKeys.getPrivateKey()); @@ -108,7 +110,7 @@ public class AxolotlService { } private static int generateRegistrationId() { - Log.d(Config.LOGTAG, "Generating axolotl registration ID..."); + Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl registration ID..."); int reg_id = KeyHelper.generateRegistrationId(false); return reg_id; } @@ -119,7 +121,7 @@ public class AxolotlService { this.localRegistrationId = loadRegistrationId(); this.currentPreKeyId = loadCurrentPreKeyId(); for (SignedPreKeyRecord record : loadSignedPreKeys()) { - Log.d(Config.LOGTAG, "Got Axolotl signed prekey record:" + record.getId()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got Axolotl signed prekey record:" + record.getId()); } } @@ -139,7 +141,7 @@ public class AxolotlService { if (ownKey != null) { return ownKey; } else { - Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + ownName); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl key for account " + ownName); ownKey = generateIdentityKeyPair(); mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey); } @@ -152,13 +154,13 @@ public class AxolotlService { if (regIdString != null) { reg_id = Integer.valueOf(regIdString); } else { - Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid()); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl registration id for account " + account.getJid()); reg_id = generateRegistrationId(); boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id)); if (success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { - Log.e(Config.LOGTAG, "Failed to write new key to the database!"); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to write new key to the database!"); } } return reg_id; @@ -170,7 +172,7 @@ public class AxolotlService { if (regIdString != null) { reg_id = Integer.valueOf(regIdString); } else { - Log.d(Config.LOGTAG, "Could not retrieve current prekey id for account " + account.getJid()); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve current prekey id for account " + account.getJid()); reg_id = 0; } return reg_id; @@ -366,7 +368,7 @@ public class AxolotlService { if (success) { mXmppConnectionService.databaseBackend.updateAccount(account); } else { - Log.e(Config.LOGTAG, "Failed to write new prekey id to the database!"); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to write new prekey id to the database!"); } } @@ -456,11 +458,13 @@ public class AxolotlService { private Integer preKeyId = null; private SQLiteAxolotlStore sqLiteAxolotlStore; private AxolotlAddress remoteAddress; + private final Account account; - public XmppAxolotlSession(SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { + public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { this.cipher = new SessionCipher(store, remoteAddress); this.remoteAddress = remoteAddress; this.sqLiteAxolotlStore = store; + this.account = account; this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress); } @@ -486,21 +490,20 @@ public class AxolotlService { try { try { PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); - Log.d(Config.LOGTAG, "PreKeyWhisperMessage ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); plaintext = cipher.decrypt(message); if (message.getPreKeyId().isPresent()) { preKeyId = message.getPreKeyId().get(); } } catch (InvalidMessageException | InvalidVersionException e) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"WhisperMessage received"); WhisperMessage message = new WhisperMessage(incomingHeader.getContents()); plaintext = cipher.decrypt(message); } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); } - } catch (LegacyMessageException | InvalidMessageException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); - } catch (DuplicateMessageException | NoSessionException e) { - Log.d(Config.LOGTAG, "Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); + } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) { + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error decrypting axolotl header, "+e.getClass().getName()+": " + e.getMessage()); } return plaintext; } @@ -580,7 +583,7 @@ public class AxolotlService { List deviceIDs = store.getSubDeviceSessions(address); for (Integer deviceId : deviceIDs) { AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); - this.put(axolotlAddress, new XmppAxolotlSession(store, axolotlAddress)); + this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress)); } } } @@ -596,6 +599,10 @@ public class AxolotlService { private static class FetchStatusMap extends AxolotlAddressMap { } + + public static String getLogprefix(Account account) { + return LOGPREFIX+" ("+account.getJid().toBareJid().toString()+"): "; + } public AxolotlService(Account account, XmppConnectionService connectionService) { this.mXmppConnectionService = connectionService; @@ -661,11 +668,11 @@ public class AxolotlService { public void registerDevices(final Jid jid, @NonNull final Set deviceIds) { if(deviceIds.contains(getOwnDeviceId())) { - Log.d(Config.LOGTAG, "Skipping own Device ID:"+ jid + ":"+getOwnDeviceId()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Skipping own Device ID:"+ jid + ":"+getOwnDeviceId()); deviceIds.remove(getOwnDeviceId()); } for(Integer i:deviceIds) { - Log.d(Config.LOGTAG, "Adding Device ID:"+ jid + ":"+i); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding Device ID:"+ jid + ":"+i); } this.deviceIds.put(jid, deviceIds); publishOwnDeviceIdIfNeeded(); @@ -675,7 +682,7 @@ public class AxolotlService { Set deviceIds = new HashSet<>(); deviceIds.add(getOwnDeviceId()); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); - Log.d(Config.LOGTAG, "Wiping all other devices from Pep:" + publish); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Wiping all other devices from Pep:" + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -697,7 +704,7 @@ public class AxolotlService { if (!deviceIds.contains(getOwnDeviceId())) { deviceIds.add(getOwnDeviceId()); IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds); - Log.d(Config.LOGTAG, "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -718,19 +725,19 @@ public class AxolotlService { Map keys = mXmppConnectionService.getIqParser().preKeyPublics(packet); boolean flush = false; if (bundle == null) { - Log.e(Config.LOGTAG, "Received invalid bundle:" + packet); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received invalid bundle:" + packet); bundle = new PreKeyBundle(-1, -1, -1 , null, -1, null, null, null); flush = true; } if (keys == null) { - Log.e(Config.LOGTAG, "Received invalid prekeys:" + packet); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received invalid prekeys:" + packet); } try { boolean changed = false; // Validate IdentityKey IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair(); if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) { - Log.d(Config.LOGTAG, "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP."); changed = true; } @@ -742,13 +749,13 @@ public class AxolotlService { if ( flush ||!bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey()) || !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) { - Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); changed = true; } } catch (InvalidKeyIdException e) { - Log.d(Config.LOGTAG, "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP."); signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1); axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord); changed = true; @@ -776,7 +783,7 @@ public class AxolotlService { axolotlStore.storePreKey(record.getId(), record); } changed = true; - Log.d(Config.LOGTAG, "Adding " + newKeys + " new preKeys to PEP."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Adding " + newKeys + " new preKeys to PEP."); } @@ -784,17 +791,17 @@ public class AxolotlService { IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), preKeyRecords, ownDeviceId); - Log.d(Config.LOGTAG, "Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+ ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { // TODO: implement this! - Log.d(Config.LOGTAG, "Published bundle, got: " + packet); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Published bundle, got: " + packet); } }); } } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage()); return; } } @@ -809,21 +816,21 @@ public class AxolotlService { } private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) { - Log.d(Config.LOGTAG, "Building new sesstion for " + address.getDeviceId()); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building new sesstion for " + address.getDeviceId()); try { IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( Jid.fromString(address.getName()), address.getDeviceId()); - Log.d(Config.LOGTAG, "Retrieving bundle: " + bundlesPacket); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Retrieving bundle: " + bundlesPacket); mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - Log.d(Config.LOGTAG, "Received preKey IQ packet, processing..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received preKey IQ packet, processing..."); final IqParser parser = mXmppConnectionService.getIqParser(); final List preKeyBundleList = parser.preKeys(packet); final PreKeyBundle bundle = parser.bundle(packet); if (preKeyBundleList.isEmpty() || bundle == null) { - Log.d(Config.LOGTAG, "preKey IQ packet invalid: " + packet); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"preKey IQ packet invalid: " + packet); fetchStatusMap.put(address, FetchStatus.ERROR); return; } @@ -845,11 +852,11 @@ public class AxolotlService { try { SessionBuilder builder = new SessionBuilder(axolotlStore, address); builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(axolotlStore, address); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address); sessions.put(address, session); fetchStatusMap.put(address, FetchStatus.SUCCESS); } catch (UntrustedIdentityException|InvalidKeyException e) { - Log.d(Config.LOGTAG, "Error building session for " + address + ": " + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Error building session for " + address + ": " + e.getClass().getName() + ", " + e.getMessage()); fetchStatusMap.put(address, FetchStatus.ERROR); } @@ -869,32 +876,32 @@ public class AxolotlService { } }); } catch (InvalidJidException e) { - Log.e(Config.LOGTAG,"Got address with invalid jid: " + address.getName()); + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got address with invalid jid: " + address.getName()); } } private boolean createSessionsIfNeeded(Conversation conversation) { boolean newSessions = false; - Log.d(Config.LOGTAG, "Creating axolotl sessions if needed..."); + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Creating axolotl sessions if needed..."); Jid contactJid = conversation.getContact().getJid().toBareJid(); Set addresses = new HashSet<>(); if(deviceIds.get(contactJid) != null) { for(Integer foreignId:this.deviceIds.get(contactJid)) { - Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+foreignId); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found device "+account.getJid().toBareJid()+":"+foreignId); addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); } } else { - Log.e(Config.LOGTAG, "Have no target devices in PEP!"); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Have no target devices in PEP!"); } - Log.d(Config.LOGTAG, "Checking own account "+account.getJid().toBareJid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Checking own account "+account.getJid().toBareJid()); if(deviceIds.get(account.getJid().toBareJid()) != null) { for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) { - Log.d(Config.LOGTAG, "Found device "+account.getJid().toBareJid()+":"+ownId); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found device "+account.getJid().toBareJid()+":"+ownId); addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId)); } } for (AxolotlAddress address : addresses) { - Log.d(Config.LOGTAG, "Processing device: " + address.toString()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Processing device: " + address.toString()); FetchStatus status = fetchStatusMap.get(address); XmppAxolotlSession session = sessions.get(address); if ( session == null && ( status == null || status == FetchStatus.ERROR) ) { @@ -902,7 +909,7 @@ public class AxolotlService { this.buildSessionFromPEP(conversation, address); newSessions = true; } else { - Log.d(Config.LOGTAG, "Already have session for " + address.toString()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString()); } } return newSessions; @@ -916,18 +923,18 @@ public class AxolotlService { if(findSessionsforContact(message.getContact()).isEmpty()) { return null; } - Log.d(Config.LOGTAG, "Building axolotl foreign headers..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl foreign headers..."); for (XmppAxolotlSession session : findSessionsforContact(message.getContact())) { - Log.d(Config.LOGTAG, session.remoteAddress.toString()); + Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString()); //if(!session.isTrusted()) { // TODO: handle this properly // continue; // } axolotlMessage.addHeader(session.processSending(axolotlMessage.getInnerKey())); } - Log.d(Config.LOGTAG, "Building axolotl own headers..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building axolotl own headers..."); for (XmppAxolotlSession session : findOwnSessions()) { - Log.d(Config.LOGTAG, session.remoteAddress.toString()); + Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account)+session.remoteAddress.toString()); // if(!session.isTrusted()) { // TODO: handle this properly // continue; @@ -948,7 +955,7 @@ public class AxolotlService { mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED); //mXmppConnectionService.updateConversationUi(); } else { - Log.d(Config.LOGTAG, "Generated message, caching: " + message.getUuid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Generated message, caching: " + message.getUuid()); messageCache.put(message.getUuid(), packet); mXmppConnectionService.resendMessage(message); } @@ -969,10 +976,10 @@ public class AxolotlService { public MessagePacket fetchPacketFromCache(Message message) { MessagePacket packet = messageCache.get(message.getUuid()); if (packet != null) { - Log.d(Config.LOGTAG, "Cache hit: " + message.getUuid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Cache hit: " + message.getUuid()); messageCache.remove(message.getUuid()); } else { - Log.d(Config.LOGTAG, "Cache miss: " + message.getUuid()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Cache miss: " + message.getUuid()); } return packet; } @@ -985,18 +992,18 @@ public class AxolotlService { boolean newSession = false; XmppAxolotlSession session = sessions.get(senderAddress); if (session == null) { - Log.d(Config.LOGTAG, "Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); // TODO: handle this properly - session = new XmppAxolotlSession(axolotlStore, senderAddress); + session = new XmppAxolotlSession(account, axolotlStore, senderAddress); newSession = true; } for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { if (header.getRecipientDeviceId() == ownDeviceId) { - Log.d(Config.LOGTAG, "Found axolotl header matching own device ID, processing..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found axolotl header matching own device ID, processing..."); byte[] payloadKey = session.processReceiving(header); if (payloadKey != null) { - Log.d(Config.LOGTAG, "Got payload key from axolotl header. Decrypting message..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl header. Decrypting message..."); plaintextMessage = message.decrypt(session, payloadKey); } Integer preKeyId = session.getPreKeyId(); diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index b0727690b..1dd21541b 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -73,7 +73,7 @@ public class MessageGenerator extends AbstractGenerator { public MessagePacket generateAxolotlChat(Message message, boolean addDelay) { MessagePacket packet = preparePacket(message, addDelay); AxolotlService service = message.getConversation().getAccount().getAxolotlService(); - Log.d(Config.LOGTAG, "Submitting message to axolotl service for send processing..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(message.getConversation().getAccount())+"Submitting message to axolotl service for send processing..."); XmppAxolotlMessage axolotlMessage = service.encrypt(message); if (axolotlMessage == null) { return null; diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index c147978e0..e74cb65c0 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Set; import eu.siacs.conversations.Config; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.XmppConnectionService; @@ -111,7 +112,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { Integer id = Integer.valueOf(device.getAttribute("id")); deviceIds.add(id); } catch (NumberFormatException e) { - Log.e(Config.LOGTAG, "Encountered nvalid node in PEP:" + device.toString() + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered nvalid node in PEP:" + device.toString() + ", skipping..."); continue; } @@ -138,7 +139,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { try { publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0); } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, "Invalid signedPreKeyPublic in PEP: " + e.getMessage()); + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage()); } return publicKey; } @@ -160,7 +161,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { try { identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0); } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG,"Invalid identityKey in PEP: "+e.getMessage()); + Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage()); } return identityKey; } @@ -169,7 +170,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { Map preKeyRecords = new HashMap<>(); Element item = getItem(packet); if (item == null) { - Log.d(Config.LOGTAG, "Couldn't find in bundle IQ packet: " + packet); + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find in bundle IQ packet: " + packet); return null; } final Element bundleElement = item.findChild("bundle"); @@ -178,12 +179,12 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } final Element prekeysElement = bundleElement.findChild("prekeys"); if(prekeysElement == null) { - Log.d(Config.LOGTAG, "Couldn't find in bundle IQ packet: " + packet); + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find in bundle IQ packet: " + packet); return null; } for(Element preKeyPublicElement : prekeysElement.getChildren()) { if(!preKeyPublicElement.getName().equals("preKeyPublic")){ - Log.d(Config.LOGTAG, "Encountered unexpected tag in prekeys list: " + preKeyPublicElement); + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement); continue; } Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId")); @@ -191,7 +192,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0); preKeyRecords.put(preKeyId, preKeyPublic); } catch (InvalidKeyException e) { - Log.e(Config.LOGTAG, "Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping..."); + Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping..."); continue; } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 0dc8d5215..8b5f97af6 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -189,7 +189,7 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.updateAccountUi(); } } else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) { - Log.d(Config.LOGTAG, "Received PEP device list update from "+ from + ", processing..."); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing..."); Element item = items.findChild("item"); Set deviceIds = mXmppConnectionService.getIqParser().deviceIds(item); AxolotlService axolotlService = account.getAxolotlService(); diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 1aa2bade9..e408fa7ff 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -820,7 +820,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT)); } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name); } } cursor.close(); @@ -836,7 +836,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { try { identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0)); } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name); } } cursor.close(); @@ -863,7 +863,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public void recreateAxolotlDb() { - Log.d(Config.LOGTAG, ">>> (RE)CREATING AXOLOTL DATABASE <<<"); + Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<"); SQLiteDatabase db = this.getWritableDatabase(); db.execSQL("DROP TABLE IF EXISTS " + AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME); db.execSQL(CREATE_SESSIONS_STATEMENT); @@ -877,7 +877,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { public void wipeAxolotlDb(Account account) { String accountName = account.getUuid(); - Log.d(Config.LOGTAG, ">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<"); SQLiteDatabase db = this.getWritableDatabase(); String[] deleteArgs= { accountName From bd29653a20768039778f3cbf75609772fb16de4d Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 8 Jul 2015 17:45:37 +0200 Subject: [PATCH 035/166] Make some fields final --- .../siacs/conversations/crypto/axolotl/AxolotlService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 8ac69d37a..664a248f8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -453,11 +453,11 @@ public class AxolotlService { } public static class XmppAxolotlSession { - private SessionCipher cipher; + private final SessionCipher cipher; private boolean isTrusted = false; private Integer preKeyId = null; - private SQLiteAxolotlStore sqLiteAxolotlStore; - private AxolotlAddress remoteAddress; + private final SQLiteAxolotlStore sqLiteAxolotlStore; + private final AxolotlAddress remoteAddress; private final Account account; public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { From 540faeb54b1fb230e624573840284daabaf4919b Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 8 Jul 2015 17:46:03 +0200 Subject: [PATCH 036/166] Clean up unused constant --- .../eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 664a248f8..3c9584d3d 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -89,7 +89,6 @@ public class AxolotlService { public static final String TRUSTED = "trusted"; public static final String OWN = "ownkey"; - public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key"; public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; From f1d73b9d4e64150cda347223afdeaee2ddf595eb Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 8 Jul 2015 18:13:49 +0200 Subject: [PATCH 037/166] Use full int range for device IDs --- .../eu/siacs/conversations/crypto/axolotl/AxolotlService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 3c9584d3d..ef7c523df 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -110,7 +110,7 @@ public class AxolotlService { private static int generateRegistrationId() { Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl registration ID..."); - int reg_id = KeyHelper.generateRegistrationId(false); + int reg_id = KeyHelper.generateRegistrationId(true); return reg_id; } From 2628662a7f374dc7cbb01869afb6f5f8fdf5f619 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 8 Jul 2015 18:14:28 +0200 Subject: [PATCH 038/166] Display axolotl chat message hint --- .../java/eu/siacs/conversations/ui/ConversationFragment.java | 3 +++ src/main/res/values/strings.xml | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index ec50ea548..026c74adf 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -325,6 +325,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa case Message.ENCRYPTION_OTR: mEditMessage.setHint(getString(R.string.send_otr_message)); break; + case Message.ENCRYPTION_AXOLOTL: + mEditMessage.setHint(getString(R.string.send_axolotl_message)); + break; case Message.ENCRYPTION_PGP: mEditMessage.setHint(getString(R.string.send_pgp_message)); break; diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 60ca5613e..6808f1bb5 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -80,6 +80,7 @@ Choose presence to contact Send plain text message Send OTR encrypted message + Send Axolotl encrypted message Send OpenPGP encrypted message Your nickname has been changed Send unencrypted From 03614a0262639d1f16b0fd13ec2f4ee0767205c9 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 9 Jul 2015 14:15:59 +0200 Subject: [PATCH 039/166] Fix getSubDeviceSessions SQL query --- .../eu/siacs/conversations/persistance/DatabaseBackend.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index e408fa7ff..4fb019423 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -594,7 +594,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, columns, AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND ", + + AxolotlService.SQLiteAxolotlStore.NAME + " = ?", selectionArgs, null, null, null); From 7f918542c845fc1746676143334a5c0eca0a1e76 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 9 Jul 2015 14:18:54 +0200 Subject: [PATCH 040/166] Postpone initAccountService until roster loaded The AxolotlService depends on the roster being loaded when it is initialized so that it can fill its in-memory SessionMap. --- .../siacs/conversations/services/XmppConnectionService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 3e443d75f..ca62b7db6 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -594,9 +594,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); this.accounts = databaseBackend.getAccounts(); - for (final Account account : this.accounts) { - account.initAccountServices(this); - } restoreFromDatabase(); getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver); @@ -955,6 +952,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Log.d(Config.LOGTAG,"restoring roster"); for(Account account : accounts) { databaseBackend.readRoster(account.getRoster()); + account.initAccountServices(XmppConnectionService.this); } getBitmapCache().evictAll(); Looper.prepare(); From d173913ebabbf8de2725dd296ae6428defd4b3b3 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 9 Jul 2015 14:23:17 +0200 Subject: [PATCH 041/166] Overhauled Message tagging Messages are now tagged with the IdentityKey fingerprint of the originating session. IdentityKeys have one of three trust states: undecided (default), trusted, and untrusted/not yet trusted. --- .../crypto/axolotl/AxolotlService.java | 100 +++++++++++------- .../crypto/axolotl/XmppAxolotlMessage.java | 11 +- .../siacs/conversations/entities/Message.java | 19 ++-- .../conversations/parser/MessageParser.java | 3 +- .../persistance/DatabaseBackend.java | 93 ++++++++++------ 5 files changed, 138 insertions(+), 88 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index ef7c523df..9c5c82ffa 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -85,6 +85,7 @@ public class AxolotlService { public static final String DEVICE_ID = "device_id"; public static final String ID = "id"; public static final String KEY = "key"; + public static final String FINGERPRINT = "fingerprint"; public static final String NAME = "name"; public static final String TRUSTED = "trusted"; public static final String OWN = "ownkey"; @@ -99,6 +100,23 @@ public class AxolotlService { private final int localRegistrationId; private int currentPreKeyId = 0; + public enum Trust { + UNDECIDED, // 0 + TRUSTED, + UNTRUSTED; + + public String toString() { + switch(this){ + case UNDECIDED: + return "Trust undecided"; + case TRUSTED: + return "Trusted"; + case UNTRUSTED: + default: + return "Untrusted"; + } + } + }; private static IdentityKeyPair generateIdentityKeyPair() { Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Generating axolotl IdentityKeyPair..."); @@ -242,11 +260,17 @@ public class AxolotlService { */ @Override public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - //Set trustedKeys = mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name); - //return trustedKeys.isEmpty() || trustedKeys.contains(identityKey); return true; } + public Trust getFingerprintTrust(String name, String fingerprint) { + return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, name, fingerprint); + } + + public void setFingerprintTrust(String name, String fingerprint, Trust trust) { + mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, name, fingerprint, trust); + } + // -------------------------------------- // SessionStore // -------------------------------------- @@ -325,14 +349,6 @@ public class AxolotlService { new AxolotlAddress(name, 0)); } - public boolean isTrustedSession(AxolotlAddress address) { - return mXmppConnectionService.databaseBackend.isTrustedSession(this.account, address); - } - - public void setTrustedSession(AxolotlAddress address, boolean trusted) { - mXmppConnectionService.databaseBackend.setTrustedSession(this.account, address, trusted); - } - // -------------------------------------- // PreKeyStore // -------------------------------------- @@ -453,27 +469,22 @@ public class AxolotlService { public static class XmppAxolotlSession { private final SessionCipher cipher; - private boolean isTrusted = false; private Integer preKeyId = null; private final SQLiteAxolotlStore sqLiteAxolotlStore; private final AxolotlAddress remoteAddress; private final Account account; + private String fingerprint = null; + + public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) { + this(account, store, remoteAddress); + this.fingerprint = fingerprint; + } public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { this.cipher = new SessionCipher(store, remoteAddress); this.remoteAddress = remoteAddress; this.sqLiteAxolotlStore = store; this.account = account; - this.isTrusted = sqLiteAxolotlStore.isTrustedSession(remoteAddress); - } - - public void trust() { - sqLiteAxolotlStore.setTrustedSession(remoteAddress, true); - this.isTrusted = true; - } - - public boolean isTrusted() { - return this.isTrusted; } public Integer getPreKeyId() { @@ -481,18 +492,29 @@ public class AxolotlService { } public void resetPreKeyId() { + preKeyId = null; } + public String getFingerprint() { + return fingerprint; + } + public byte[] processReceiving(XmppAxolotlMessage.XmppAxolotlMessageHeader incomingHeader) { byte[] plaintext = null; try { try { PreKeyWhisperMessage message = new PreKeyWhisperMessage(incomingHeader.getContents()); Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); - plaintext = cipher.decrypt(message); - if (message.getPreKeyId().isPresent()) { - preKeyId = message.getPreKeyId().get(); + String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", ""); + if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Had session with fingerprint "+ this.fingerprint+", received message with fingerprint "+fingerprint); + } else { + this.fingerprint = fingerprint; + plaintext = cipher.decrypt(message); + if (message.getPreKeyId().isPresent()) { + preKeyId = message.getPreKeyId().get(); + } } } catch (InvalidMessageException | InvalidVersionException e) { Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"WhisperMessage received"); @@ -582,7 +604,9 @@ public class AxolotlService { List deviceIDs = store.getSubDeviceSessions(address); for (Integer deviceId : deviceIDs) { AxolotlAddress axolotlAddress = new AxolotlAddress(address, deviceId); - this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress)); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building session for remote address: "+axolotlAddress.toString()); + String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", ""); + this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint)); } } } @@ -619,18 +643,6 @@ public class AxolotlService { return axolotlStore.getIdentityKeyPair().getPublicKey(); } - public void trustSession(AxolotlAddress counterpart) { - XmppAxolotlSession session = sessions.get(counterpart); - if (session != null) { - session.trust(); - } - } - - public boolean isTrustedSession(AxolotlAddress counterpart) { - XmppAxolotlSession session = sessions.get(counterpart); - return session != null && session.isTrusted(); - } - private AxolotlAddress getAddressForJid(Jid jid) { return new AxolotlAddress(jid.toString(), 0); } @@ -808,11 +820,19 @@ public class AxolotlService { } public boolean isContactAxolotlCapable(Contact contact) { + Jid jid = contact.getJid().toBareJid(); AxolotlAddress address = new AxolotlAddress(jid.toString(), 0); return sessions.hasAny(address) || ( deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); } + public SQLiteAxolotlStore.Trust getFingerprintTrust(String name, String fingerprint) { + return axolotlStore.getFingerprintTrust(name, fingerprint); + } + + public void setFingerprintTrust(String name, String fingerprint, SQLiteAxolotlStore.Trust trust) { + axolotlStore.setFingerprintTrust(name, fingerprint, trust); + } private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) { Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building new sesstion for " + address.getDeviceId()); @@ -851,7 +871,7 @@ public class AxolotlService { try { SessionBuilder builder = new SessionBuilder(axolotlStore, address); builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", "")); sessions.put(address, session); fetchStatusMap.put(address, FetchStatus.SUCCESS); } catch (UntrustedIdentityException|InvalidKeyException e) { @@ -890,7 +910,7 @@ public class AxolotlService { addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); } } else { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Have no target devices in PEP!"); + Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); } Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Checking own account "+account.getJid().toBareJid()); if(deviceIds.get(account.getJid().toBareJid()) != null) { @@ -1003,7 +1023,7 @@ public class AxolotlService { byte[] payloadKey = session.processReceiving(header); if (payloadKey != null) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl header. Decrypting message..."); - plaintextMessage = message.decrypt(session, payloadKey); + plaintextMessage = message.decrypt(session, payloadKey, session.getFingerprint()); } Integer preKeyId = session.getPreKeyId(); if (preKeyId != null) { diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index 06dd2cdaf..459952281 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -67,10 +67,12 @@ public class XmppAxolotlMessage { public static class XmppAxolotlPlaintextMessage { private final AxolotlService.XmppAxolotlSession session; private final String plaintext; + private final String fingerprint; - public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext) { + public XmppAxolotlPlaintextMessage(AxolotlService.XmppAxolotlSession session, String plaintext, String fingerprint) { this.session = session; this.plaintext = plaintext; + this.fingerprint = fingerprint; } public String getPlaintext() { @@ -81,6 +83,9 @@ public class XmppAxolotlMessage { return session; } + public String getFingerprint() { + return fingerprint; + } } public XmppAxolotlMessage(Jid from, Element axolotlMessage) { @@ -167,7 +172,7 @@ public class XmppAxolotlMessage { } - public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key) { + public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) { XmppAxolotlPlaintextMessage plaintextMessage = null; try { @@ -178,7 +183,7 @@ public class XmppAxolotlMessage { cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); String plaintext = new String(cipher.doFinal(ciphertext)); - plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext); + plaintextMessage = new XmppAxolotlPlaintextMessage(session, plaintext, fingerprint); } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index b1dffc820..9695e6fa9 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -54,6 +54,7 @@ public class Message extends AbstractEntity { public static final String REMOTE_MSG_ID = "remoteMsgId"; public static final String SERVER_MSG_ID = "serverMsgId"; public static final String RELATIVE_FILE_PATH = "relativeFilePath"; + public static final String FINGERPRINT = "axolotl_fingerprint"; public static final String ME_COMMAND = "/me "; @@ -67,7 +68,6 @@ public class Message extends AbstractEntity { protected int encryption; protected int status; protected int type; - private AxolotlService.XmppAxolotlSession axolotlSession = null; protected String relativeFilePath; protected boolean read = true; protected String remoteMsgId = null; @@ -76,6 +76,7 @@ public class Message extends AbstractEntity { protected Transferable transferable = null; private Message mNextMessage = null; private Message mPreviousMessage = null; + private String axolotlFingerprint = null; private Message() { @@ -97,6 +98,7 @@ public class Message extends AbstractEntity { TYPE_TEXT, null, null, + null, null); this.conversation = conversation; } @@ -104,7 +106,7 @@ public class Message extends AbstractEntity { private Message(final String uuid, final String conversationUUid, final Jid counterpart, final Jid trueCounterpart, final String body, final long timeSent, final int encryption, final int status, final int type, final String remoteMsgId, - final String relativeFilePath, final String serverMsgId) { + final String relativeFilePath, final String serverMsgId, final String fingerprint) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -117,6 +119,7 @@ public class Message extends AbstractEntity { this.remoteMsgId = remoteMsgId; this.relativeFilePath = relativeFilePath; this.serverMsgId = serverMsgId; + this.axolotlFingerprint = fingerprint; } public static Message fromCursor(Cursor cursor) { @@ -153,7 +156,8 @@ public class Message extends AbstractEntity { cursor.getInt(cursor.getColumnIndex(TYPE)), cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), - cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID))); + cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), + cursor.getString(cursor.getColumnIndex(FINGERPRINT))); } public static Message createStatusMessage(Conversation conversation, String body) { @@ -187,6 +191,7 @@ public class Message extends AbstractEntity { values.put(REMOTE_MSG_ID, remoteMsgId); values.put(RELATIVE_FILE_PATH, relativeFilePath); values.put(SERVER_MSG_ID, serverMsgId); + values.put(FINGERPRINT, axolotlFingerprint); return values; } @@ -667,11 +672,7 @@ public class Message extends AbstractEntity { public int height = 0; } - public boolean isTrusted() { - return this.axolotlSession != null && this.axolotlSession.isTrusted(); - } - - public void setAxolotlSession(AxolotlService.XmppAxolotlSession session) { - this.axolotlSession = session; + public void setAxolotlFingerprint(String fingerprint) { + this.axolotlFingerprint = fingerprint; } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 8b5f97af6..9cbe5b460 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -106,7 +106,8 @@ public class MessageParser extends AbstractParser implements XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceiving(xmppAxolotlMessage); if(plaintextMessage != null) { finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status); - finishedMessage.setAxolotlSession(plaintextMessage.getSession()); + finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint()); + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint()); } return finishedMessage; diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 4fb019423..0da52010b 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -82,7 +82,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + AxolotlService.SQLiteAxolotlStore.DEVICE_ID + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, " + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " @@ -97,6 +96,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + AxolotlService.SQLiteAxolotlStore.OWN + " INTEGER, " + + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " TEXT PRIMARY KEY ON CONFLICT IGNORE, " + + AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, " + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE " @@ -132,6 +133,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " + Message.RELATIVE_FILE_PATH + " TEXT, " + Message.SERVER_MSG_ID + " TEXT, " + + Message.FINGERPRINT + " TEXT, " + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID @@ -284,6 +286,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { } if (oldVersion < 15 && newVersion >= 15) { recreateAxolotlDb(); + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.FINGERPRINT + " TEXT"); } } @@ -645,28 +649,6 @@ public class DatabaseBackend extends SQLiteOpenHelper { args); } - public boolean isTrustedSession(Account account, AxolotlAddress contact) { - boolean trusted = false; - Cursor cursor = getCursorForSession(account, contact); - if(cursor.getCount() != 0) { - cursor.moveToFirst(); - trusted = cursor.getInt(cursor.getColumnIndex( - AxolotlService.SQLiteAxolotlStore.TRUSTED)) > 0; - } - cursor.close(); - return trusted; - } - - public void setTrustedSession(Account account, AxolotlAddress contact, boolean trusted) { - SQLiteDatabase db = this.getWritableDatabase(); - ContentValues values = new ContentValues(); - values.put(AxolotlService.SQLiteAxolotlStore.NAME, contact.getName()); - values.put(AxolotlService.SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId()); - values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); - values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trusted?1:0); - db.insert(AxolotlService.SQLiteAxolotlStore.SESSION_TABLENAME, null, values); - } - private Cursor getCursorForPreKey(Account account, int preKeyId) { SQLiteDatabase db = this.getReadableDatabase(); String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; @@ -796,17 +778,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { } private Cursor getIdentityKeyCursor(Account account, String name, boolean own) { + return getIdentityKeyCursor(account, name, own, null); + } + + private Cursor getIdentityKeyCursor(Account account, String name, boolean own, String fingerprint) { final SQLiteDatabase db = this.getReadableDatabase(); - String[] columns = {AxolotlService.SQLiteAxolotlStore.KEY}; - String[] selectionArgs = {account.getUuid(), - name, - own?"1":"0"}; + String[] columns = {AxolotlService.SQLiteAxolotlStore.TRUSTED, + AxolotlService.SQLiteAxolotlStore.KEY}; + ArrayList selectionArgs = new ArrayList<>(4); + selectionArgs.add(account.getUuid()); + selectionArgs.add(name); + selectionArgs.add(own?"1":"0"); + String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.OWN + " = ? "; + if (fingerprint != null){ + selectionArgs.add(fingerprint); + selectionString += "AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? "; + } Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, columns, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.OWN + " = ? ", - selectionArgs, + selectionString, + selectionArgs.toArray(new String[selectionArgs.size()]), null, null, null); return cursor; @@ -844,22 +837,52 @@ public class DatabaseBackend extends SQLiteOpenHelper { return identityKeys; } - private void storeIdentityKey(Account account, String name, boolean own, String base64Serialized) { + private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); values.put(AxolotlService.SQLiteAxolotlStore.NAME, name); - values.put(AxolotlService.SQLiteAxolotlStore.OWN, own?1:0); + values.put(AxolotlService.SQLiteAxolotlStore.OWN, own ? 1 : 0); + values.put(AxolotlService.SQLiteAxolotlStore.FINGERPRINT, fingerprint); values.put(AxolotlService.SQLiteAxolotlStore.KEY, base64Serialized); db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); } + public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String name, String fingerprint) { + Cursor cursor = getIdentityKeyCursor(account, name, false, fingerprint); + AxolotlService.SQLiteAxolotlStore.Trust trust = null; + if (cursor.getCount() > 0) { + cursor.moveToFirst(); + int trustValue = cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED)); + trust = AxolotlService.SQLiteAxolotlStore.Trust.values()[trustValue]; + } + cursor.close(); + return trust; + } + + public boolean setIdentityKeyTrust(Account account, String name, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] selectionArgs = { + account.getUuid(), + name, + fingerprint + }; + ContentValues values = new ContentValues(); + values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.ordinal()); + int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? " + + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ", + selectionArgs); + return rows == 1; + } + public void storeIdentityKey(Account account, String name, IdentityKey identityKey) { - storeIdentityKey(account, name, false, Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); + storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); } public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { - storeIdentityKey(account, name, true, Base64.encodeToString(identityKeyPair.serialize(),Base64.DEFAULT)); + storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT)); } public void recreateAxolotlDb() { From 23a4e1e6fada4eb5309db35efcf80d6f1f6f4d34 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Thu, 9 Jul 2015 14:26:19 +0200 Subject: [PATCH 042/166] Display trust status in ContactDetailsActivity --- .../ui/ContactDetailsActivity.java | 3 +++ src/main/res/layout/contact_key.xml | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 33861f828..4ff47f9ec 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -383,11 +383,14 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd View view = inflater.inflate(R.layout.contact_key, keys, false); TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); + TextView keyTrust = (TextView) view.findViewById(R.id.key_trust); ImageButton remove = (ImageButton) view .findViewById(R.id.button_remove); remove.setVisibility(View.VISIBLE); + keyTrust.setVisibility(View.VISIBLE); keyType.setText("Axolotl Fingerprint"); key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); + keyTrust.setText(contact.getAccount().getAxolotlService().getFingerprintTrust(contact.getJid().toBareJid().toString(), identityKey.getFingerprint().replaceAll("\\s","")).toString()); keys.addView(view); remove.setOnClickListener(new OnClickListener() { diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml index 933b72b45..79b9af620 100644 --- a/src/main/res/layout/contact_key.xml +++ b/src/main/res/layout/contact_key.xml @@ -3,12 +3,11 @@ android:layout_width="wrap_content" android:layout_height="match_parent" > - @@ -24,8 +24,20 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/black54" + android:layout_alignParentLeft="true" + android:layout_below="@+id/key" android:textSize="?attr/TextSizeInfo"/> - + + + Date: Fri, 10 Jul 2015 02:18:01 +0200 Subject: [PATCH 043/166] Fix and expand key regeneration function Wipe session cache to prevent stale sessions being used. Wipe fetch status cache to enable recreation of sessions. Regenerate deviceId, so that foreign devices will talk to us again. --- .../crypto/axolotl/AxolotlService.java | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 9c5c82ffa..6b6c37505 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -73,7 +73,6 @@ public class AxolotlService { private final Map messageCache; private final FetchStatusMap fetchStatusMap; private final SerialSingleThreadExecutor executor; - private int ownDeviceId; public static class SQLiteAxolotlStore implements AxolotlStore { @@ -97,7 +96,7 @@ public class AxolotlService { private final XmppConnectionService mXmppConnectionService; private IdentityKeyPair identityKeyPair; - private final int localRegistrationId; + private int localRegistrationId; private int currentPreKeyId = 0; public enum Trust { @@ -166,9 +165,13 @@ public class AxolotlService { } private int loadRegistrationId() { + return loadRegistrationId(false); + } + + private int loadRegistrationId(boolean regenerate) { String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID); int reg_id; - if (regIdString != null) { + if (!regenerate && regIdString != null) { reg_id = Integer.valueOf(regIdString); } else { Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Could not retrieve axolotl registration id for account " + account.getJid()); @@ -199,6 +202,7 @@ public class AxolotlService { mXmppConnectionService.databaseBackend.wipeAxolotlDb(account); account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0)); identityKeyPair = loadIdentityKeyPair(); + localRegistrationId = loadRegistrationId(true); currentPreKeyId = 0; mXmppConnectionService.updateAccountUi(); } @@ -584,6 +588,9 @@ public class AxolotlService { } } + public void clear() { + map.clear(); + } } @@ -636,7 +643,6 @@ public class AxolotlService { this.sessions = new SessionMap(axolotlStore, account); this.fetchStatusMap = new FetchStatusMap(); this.executor = new SerialSingleThreadExecutor(); - this.ownDeviceId = axolotlStore.getLocalRegistrationId(); } public IdentityKey getOwnPublicKey() { @@ -666,11 +672,14 @@ public class AxolotlService { public void regenerateKeys() { axolotlStore.regenerate(); + sessions.clear(); + fetchStatusMap.clear(); publishBundlesIfNeeded(); + publishOwnDeviceIdIfNeeded(); } public int getOwnDeviceId() { - return ownDeviceId; + return axolotlStore.loadRegistrationId(); } public Set getOwnDeviceIds() { @@ -728,7 +737,7 @@ public class AxolotlService { } public void publishBundlesIfNeeded() { - IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), ownDeviceId); + IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId()); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -801,7 +810,7 @@ public class AxolotlService { if(changed) { IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles( signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(), - preKeyRecords, ownDeviceId); + preKeyRecords, getOwnDeviceId()); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+ ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish); mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() { @Override @@ -937,7 +946,7 @@ public class AxolotlService { @Nullable public XmppAxolotlMessage encrypt(Message message ){ final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(), - ownDeviceId, message.getBody()); + getOwnDeviceId(), message.getBody()); if(findSessionsforContact(message.getContact()).isEmpty()) { return null; @@ -1018,7 +1027,7 @@ public class AxolotlService { } for (XmppAxolotlMessage.XmppAxolotlMessageHeader header : message.getHeaders()) { - if (header.getRecipientDeviceId() == ownDeviceId) { + if (header.getRecipientDeviceId() == getOwnDeviceId()) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found axolotl header matching own device ID, processing..."); byte[] payloadKey = session.processReceiving(header); if (payloadKey != null) { From 160e4017dfbd458325b8b63cf527f190b3f5bc2d Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 02:24:33 +0200 Subject: [PATCH 044/166] Fix IdentityKey storage model Added proper UNIQUE statement --- .../siacs/conversations/persistance/DatabaseBackend.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 0da52010b..0ba2876af 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -96,11 +96,15 @@ public class DatabaseBackend extends SQLiteOpenHelper { + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " TEXT, " + AxolotlService.SQLiteAxolotlStore.NAME + " TEXT, " + AxolotlService.SQLiteAxolotlStore.OWN + " INTEGER, " - + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " TEXT PRIMARY KEY ON CONFLICT IGNORE, " + + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " TEXT, " + AxolotlService.SQLiteAxolotlStore.TRUSTED + " INTEGER, " + AxolotlService.SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + AxolotlService.SQLiteAxolotlStore.ACCOUNT - + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE " + + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, " + + "UNIQUE( " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + ", " + + AxolotlService.SQLiteAxolotlStore.NAME + ", " + + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + + ") ON CONFLICT IGNORE" +");"; private DatabaseBackend(Context context) { From 31d375c2c374dc8a36539a78582f44accc13592e Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 02:25:28 +0200 Subject: [PATCH 045/166] Fix setIdentityKeyTrust update statement --- .../eu/siacs/conversations/persistance/DatabaseBackend.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 0ba2876af..4c6bf2215 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -874,8 +874,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.ordinal()); int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, - AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? " + AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ", selectionArgs); return rows == 1; From 35714d3d08d287c5ded125c356835fb70ab342b7 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 02:36:29 +0200 Subject: [PATCH 046/166] Ensure that available sessions are always used Any time a new session is established, call syncRosterToDisk() to ensure that on subsequent restoreFromDatabase() calls, the roster is actually available. This is important so that initAccountServices() can properly initialize the SessionMap. This prevents a race condition where after adding a new account and initiating sessions with it, if the app is killed (e.g. by reinstall) before triggering a syncRosterToDisk(), subsequent restores will not have the roster available, leading to missing XmppAxolotlSessions in the SessionMap cache. As a result of this, a new session was initiated when sending a new message, and received messages could not be tagged with the originating session's fingerprint. As an added sanity check, go to the database to confirm no records are present before creating fresh XmppAxolotlSession objects (both in the sending and receiving case). --- .../crypto/axolotl/AxolotlService.java | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 6b6c37505..23adbad84 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -595,13 +595,17 @@ public class AxolotlService { } private static class SessionMap extends AxolotlAddressMap { + private final XmppConnectionService xmppConnectionService; + private final Account account; - public SessionMap(SQLiteAxolotlStore store, Account account) { + public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) { super(); - this.fillMap(store, account); + this.xmppConnectionService = service; + this.account = account; + this.fillMap(store); } - private void fillMap(SQLiteAxolotlStore store, Account account) { + private void fillMap(SQLiteAxolotlStore store) { for (Contact contact : account.getRoster().getContacts()) { Jid bareJid = contact.getJid().toBareJid(); if (bareJid == null) { @@ -618,6 +622,11 @@ public class AxolotlService { } } + @Override + public void put(AxolotlAddress address, XmppAxolotlSession value) { + super.put(address, value); + xmppConnectionService.syncRosterToDisk(account); + } } private static enum FetchStatus { @@ -640,7 +649,7 @@ public class AxolotlService { this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService); this.deviceIds = new HashMap<>(); this.messageCache = new HashMap<>(); - this.sessions = new SessionMap(axolotlStore, account); + this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account); this.fetchStatusMap = new FetchStatusMap(); this.executor = new SerialSingleThreadExecutor(); } @@ -933,9 +942,16 @@ public class AxolotlService { FetchStatus status = fetchStatusMap.get(address); XmppAxolotlSession session = sessions.get(address); if ( session == null && ( status == null || status == FetchStatus.ERROR) ) { - fetchStatusMap.put(address, FetchStatus.PENDING); - this.buildSessionFromPEP(conversation, address); - newSessions = true; + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if ( identityKey != null ) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString() + ", adding to cache..."); + session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); + sessions.put(address, session); + } else { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(conversation, address); + newSessions = true; + } } else { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString()); } @@ -1022,7 +1038,12 @@ public class AxolotlService { if (session == null) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Account: "+account.getJid()+" No axolotl session found while parsing received message " + message); // TODO: handle this properly - session = new XmppAxolotlSession(account, axolotlStore, senderAddress); + IdentityKey identityKey = axolotlStore.loadSession(senderAddress).getSessionState().getRemoteIdentityKey(); + if ( identityKey != null ) { + session = new XmppAxolotlSession(account, axolotlStore, senderAddress, identityKey.getFingerprint().replaceAll("\\s", "")); + } else { + session = new XmppAxolotlSession(account, axolotlStore, senderAddress); + } newSession = true; } @@ -1044,7 +1065,7 @@ public class AxolotlService { } if (newSession && plaintextMessage != null) { - sessions.put(senderAddress,session); + sessions.put(senderAddress, session); } return plaintextMessage; From 3d339460889644c859d932eb3f2e324bd5696707 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 02:56:44 +0200 Subject: [PATCH 047/166] Add key trust toggle to ContactDetailsActivity Can now toggle IdentityKey trust --- .../ui/ContactDetailsActivity.java | 49 ++++++++++++++---- src/main/res/drawable-hdpi/ic_action_done.png | Bin 0 -> 1320 bytes .../res/drawable-hdpi/ic_done_black_24dp.png | Bin 0 -> 177 bytes src/main/res/drawable-mdpi/ic_action_done.png | Bin 0 -> 1197 bytes .../res/drawable-mdpi/ic_done_black_24dp.png | Bin 0 -> 130 bytes .../res/drawable-xhdpi/ic_action_done.png | Bin 0 -> 1546 bytes .../res/drawable-xhdpi/ic_done_black_24dp.png | Bin 0 -> 188 bytes .../drawable-xxhdpi/ic_done_black_24dp.png | Bin 0 -> 227 bytes .../drawable-xxxhdpi/ic_done_black_24dp.png | Bin 0 -> 277 bytes src/main/res/layout/contact_key.xml | 10 ++++ src/main/res/values-v21/themes.xml | 1 + src/main/res/values/attrs.xml | 1 + src/main/res/values/themes.xml | 1 + 13 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 src/main/res/drawable-hdpi/ic_action_done.png create mode 100644 src/main/res/drawable-hdpi/ic_done_black_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_action_done.png create mode 100644 src/main/res/drawable-mdpi/ic_done_black_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_action_done.png create mode 100644 src/main/res/drawable-xhdpi/ic_done_black_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_done_black_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 4ff47f9ec..ebc1ae839 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -35,6 +35,7 @@ import java.util.List; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; @@ -363,13 +364,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd View view = inflater.inflate(R.layout.contact_key, keys, false); TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); - ImageButton remove = (ImageButton) view + ImageButton removeButton = (ImageButton) view .findViewById(R.id.button_remove); - remove.setVisibility(View.VISIBLE); + removeButton.setVisibility(View.VISIBLE); keyType.setText("OTR Fingerprint"); key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint)); keys.addView(view); - remove.setOnClickListener(new OnClickListener() { + removeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { @@ -384,19 +385,47 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); TextView keyTrust = (TextView) view.findViewById(R.id.key_trust); - ImageButton remove = (ImageButton) view + ImageButton removeButton = (ImageButton) view .findViewById(R.id.button_remove); - remove.setVisibility(View.VISIBLE); - keyTrust.setVisibility(View.VISIBLE); + ImageButton trustButton = (ImageButton) view + .findViewById(R.id.button_trust); + final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); + final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); + final Jid bareJid = contactJid.toBareJid(); + AxolotlService.SQLiteAxolotlStore.Trust trust = contact.getAccount().getAxolotlService() + .getFingerprintTrust(bareJid.toString(), fingerprint); + switch (trust) { + case TRUSTED: + removeButton.setVisibility(View.VISIBLE); + //Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting remove button visible!"); + break; + case UNDECIDED: + case UNTRUSTED: + //Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting trust button visible!"); + trustButton.setVisibility(View.VISIBLE); + break; + } keyType.setText("Axolotl Fingerprint"); key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); - keyTrust.setText(contact.getAccount().getAxolotlService().getFingerprintTrust(contact.getJid().toBareJid().toString(), identityKey.getFingerprint().replaceAll("\\s","")).toString()); + keyTrust.setText(trust.toString()); + keyTrust.setVisibility(View.VISIBLE); keys.addView(view); - remove.setOnClickListener(new OnClickListener() { - + removeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - //confirmToDeleteFingerprint(otrFingerprint); + axolotlService.setFingerprintTrust(bareJid.toString(), fingerprint, + AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); + refreshUi(); + xmppConnectionService.updateConversationUi(); + } + }); + trustButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + axolotlService.setFingerprintTrust(bareJid.toString(), fingerprint, + AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED); + refreshUi(); + xmppConnectionService.updateConversationUi(); } }); } diff --git a/src/main/res/drawable-hdpi/ic_action_done.png b/src/main/res/drawable-hdpi/ic_action_done.png new file mode 100644 index 0000000000000000000000000000000000000000..58bf972171f199c359dcdb3bc6233ac84c7853d3 GIT binary patch literal 1320 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%qp275hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s00+w{G(#^lGsVi#&Ct}+$imdr$-u?X(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19yq1OzjUQlAlEdbi=l3J8mmYU*Ll%J~r_Ow+d7PlC? zn3-5OIXVK(v%u~ah~5<3ZZW~>7JZ;&^g&UJ6xA>xVEO?u;RzSWfhYacJYae+0w(OB zPunsW7#L@Hx;TbZ+F}XRH$BVJ_~tbB>ux&w{i&Jpl6k3-y`NvKHk21q zV1%GG?pIzqOj6l*_t-(U9qjp;vo`HwN=}ep)5vUa_N7?Rr}hN4ng+Fwoh&_BpW+wD z&uL8OV?3O|erD1ih8>LjGWVRk>cn-L{3q>ay)F~Cc;}xn`8m#J@(k z@^-_wj*fK>KI!`&dhL6Y6 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-hdpi/ic_done_black_24dp.png b/src/main/res/drawable-hdpi/ic_done_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..d4c06072b56dace7da1ab5fcfc5cb0a494adc5cf GIT binary patch literal 177 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8g{O;SNCo5DGlpDEh5{}R!&SuQ zX|-?oV07r-F(+5sYxQj19uA4F0*7@nHs;@YsQf$gW>+px@zP|&GZEd1Z}?}-I>Y|V z=y1-BV5J$-bD3R}6Ta_|*W@zZWWK6sUb;=`BCRvpTf-#$b)_C_PW(7UG2P#7{m0nI aGWJPksgEnI&wc~Cfx*+&&t;ucLK6Uc3_uP5 literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-mdpi/ic_action_done.png b/src/main/res/drawable-mdpi/ic_action_done.png new file mode 100644 index 0000000000000000000000000000000000000000..cf5fab3adf243318d11ec95448c6deae3f52d9b1 GIT binary patch literal 1197 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%qp275hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s00+w{G(#^lGsVi#&Ct}+$imXp$-u?X(ACh=#L&{!#L3yw z&D7A`#K{n**Cju>G&eP`1g19yq1O?oUQlAlEdbi=l3J8mmYU*Ll%J~r_Ow+dZnv1@ zG!Lpb1-Dx)aO%|uIz}H9wMbD769T3m5EGtofgE_!Pt60S_ab1z*4|&C&cMJZ@9E+g zQgQ3ebbs!H2{Omc#n}0tv`#21C~#0@f8W4;a{i~dB^meHw?8lo^y+Np_VX($n(_YC z@+E&a+Mh40ocC+XEx!5ZZ+*78?c#g6#lYQ#qw)iToIq8>mujbJsbce3PBczkwdxup zkDJUF2f-x=jZU}=Fgph@h!N>j@?Pr!beU|A1vJvL35&6;ySFy-J4wL_Ed=se(2D>)PC;-R9y zdup`rq1*C;xs&W09=YXh=ju@sDp||AN+rHQcJ=j-`9~Vw?O~j!62JMM+>gE!%~B^t z*B7ZvUsQj@6<2p>=j`nZeM}Q~7(NOZ>jx?|Mx7)_1qW{Q?n+DPg;FVn(PM_rcwjwgk7z zaExq;FcCM$1T(WNW-4=WI(1VrnIo~NbIM>M*htY0#}vP^FxeFW{X=%i-TV8!d7tO~ zdUEwS*{>~}w{ji;01FLSdXr$+1>d=Igzo{O;<{i~@EK-4moDL57>fgGHrj%N1`;d9 zO*m%rRCM7f01z5Ogb|R~a#dE8&^Q=`4tJ)^jrh+MMR3IR69s}KEImMxFjdYS170y97Bn2l! zc!x&%v?#MN2TZ3~98}8S1S_0`fJq7&qJY&3^{b#9Mqmh$L$E3Vfl;Ljg%NP-kqX&Z zTM25?XHMl3ZW^hb=NS}&TrQW)l_;axQV3D2)etO)%@DX$DNsRmu?1h*=3L=$S+q zji`a*c#N{*2E9fqaL5S4h9>Hh6v?`@WTg&HMvx3dtw7Wn3SD|yrV>#pbjUPTPg|WN zPVv)N+cT_sR%}oVBqLvOnw4(~Yn#0m)vQ=ZC?~O*G{jdJEH8vU^=Avr+i|X?`*2LY9 z7jHvHE29PrBH!kINN(*uSt$Oev6h7iu1N7t^Bcq8%eH4kerw;l_*m?|A2AK`y6?34 zP`@{J#lYB+FlJ}1sp{bG{!_iCO_Q1c_7CzZkyMzhv`>@qo7TfY@Gmt3uSG*tgxNWQc=v|JYQSD*fn7li5 z#PB$#i7UEVf00h?$hp1uLQj2zQtZFGsWX7jD_#?wJrFj3Z4#6Vd{fyGdK_MQaQVc! gtQRZb>M z)eh4Z*LH7YPT^1c;c>xMY3CR5UF8g~O~MqEfWRtq)wx@t#f5EcS$cj=DHe>A9+n8m zSQI=s(8$bw&b3_ExG+wt$|R_#_-}!m@==T1(=U4%_LXEm_Sb!xepO%dfZ-C;lZ8td zj!$Hgapx-N^Vofj<@f|98RxE|K99&g#i>rA^B#LHEf9Sw5tKPe>yY6RAwfaGi}hdF Y%FU*83Z;v716|GF>FVdQ&MBb@0A~SHegFUf literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_done_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..2f6d6386de9510fa6dd8c83cbb61a6f2e0fab9b2 GIT binary patch literal 277 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg?s&R5hEy=VJ?|UH<|x2&QEUS@ zUqF*g%NA{xn*DVG1~S@B-uDxXPUtYNKXnEQ>b^wD=lt6oW;%QS1o1=daUAyI8~(&3 zd|u96v#;i-{{JTZcp&}h>FMeJ8GvN{`tw#N{;ZEGUD<;p)-Lh7=#>`kZ6pLOOe=>Nw L`njxgN@xNAv*3Ko literal 0 HcmV?d00001 diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml index 79b9af620..a43f8083b 100644 --- a/src/main/res/layout/contact_key.xml +++ b/src/main/res/layout/contact_key.xml @@ -50,4 +50,14 @@ android:src="?attr/icon_remove" android:visibility="invisible" /> + \ No newline at end of file diff --git a/src/main/res/values-v21/themes.xml b/src/main/res/values-v21/themes.xml index d1679f928..78deb772f 100644 --- a/src/main/res/values-v21/themes.xml +++ b/src/main/res/values-v21/themes.xml @@ -18,6 +18,7 @@ @drawable/ic_file_download_white_24dp @drawable/ic_edit_white_24dp @drawable/ic_edit_grey600_24dp + @drawable/ic_done_black_24dp @drawable/ic_group_white_24dp @drawable/ic_add_white_24dp @drawable/ic_attach_file_white_24dp diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml index e314f752f..d471e54ac 100644 --- a/src/main/res/values/attrs.xml +++ b/src/main/res/values/attrs.xml @@ -14,6 +14,7 @@ + diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml index 5c67203be..afdc3e809 100644 --- a/src/main/res/values/themes.xml +++ b/src/main/res/values/themes.xml @@ -18,6 +18,7 @@ @drawable/ic_action_download @drawable/ic_action_edit @drawable/ic_action_edit_dark + @drawable/ic_action_done @drawable/ic_action_group @drawable/ic_action_new From 9e8d9a64012531be8f6f72835c6f879d2a4451a1 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 03:00:40 +0200 Subject: [PATCH 048/166] Show trust status of messages' originating session Shade lock icon red if message was received in a session that has not been marked trusted by the user or fingerprint is unknown --- .../eu/siacs/conversations/entities/Message.java | 4 ++++ .../conversations/ui/adapter/MessageAdapter.java | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 9695e6fa9..ac6c66250 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -675,4 +675,8 @@ public class Message extends AbstractEntity { public void setAxolotlFingerprint(String fingerprint) { this.axolotlFingerprint = fingerprint; } + + public String getAxolotlFingerprint() { + return axolotlFingerprint; + } } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 167f3f02f..d2c75a5ee 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.ui.adapter; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.graphics.Color; import android.graphics.Typeface; import android.net.Uri; import android.text.Spannable; @@ -26,6 +27,7 @@ import android.widget.Toast; import java.util.List; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -154,6 +156,17 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.indicator.setVisibility(View.GONE); } else { viewHolder.indicator.setVisibility(View.VISIBLE); + if (message.getMergedStatus() == Message.STATUS_RECEIVED + && message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + AxolotlService.SQLiteAxolotlStore.Trust trust = message.getConversation() + .getAccount().getAxolotlService().getFingerprintTrust( + message.getContact().getJid().toBareJid().toString(), + message.getAxolotlFingerprint()); + + if (trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) { + viewHolder.indicator.setColorFilter(Color.RED); + } + } } String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(), From 6c38e531284b76ee71ef33e7f76ba1d619b25cc2 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 10 Jul 2015 03:02:49 +0200 Subject: [PATCH 049/166] Disable Axolotl option if not usable In MUCs or if contact is not axolotl capable, disable axolotl menu option --- .../ui/ConversationActivity.java | 20 ++++++++-------- .../siacs/conversations/ui/XmppActivity.java | 23 ------------------- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index e4ce4a0f1..2e50af3bf 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -37,6 +37,7 @@ import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Contact; @@ -752,15 +753,10 @@ public class ConversationActivity extends XmppActivity } break; case R.id.encryption_choice_axolotl: - Log.d(Config.LOGTAG, "Trying to enable axolotl..."); - if(conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { - Log.d(Config.LOGTAG, "Enabled axolotl for Contact " + conversation.getContact().getJid() ); - conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); - item.setChecked(true); - } else { - Log.d(Config.LOGTAG, "Contact " + conversation.getContact().getJid() + " not axolotl capable!"); - showAxolotlNoSessionsDialog(); - } + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount()) + + "Enabled axolotl for Contact " + conversation.getContact().getJid()); + conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL); + item.setChecked(true); break; default: conversation.setNextEncryption(Message.ENCRYPTION_NONE); @@ -776,13 +772,18 @@ public class ConversationActivity extends XmppActivity MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr); MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none); MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp); + MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl); if (conversation.getMode() == Conversation.MODE_MULTI) { otr.setEnabled(false); + axolotl.setEnabled(false); } else { if (forceEncryption()) { none.setVisible(false); } } + if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) { + axolotl.setEnabled(false); + } switch (conversation.getNextEncryption(forceEncryption())) { case Message.ENCRYPTION_NONE: none.setChecked(true); @@ -794,7 +795,6 @@ public class ConversationActivity extends XmppActivity pgp.setChecked(true); break; case Message.ENCRYPTION_AXOLOTL: - Log.d(Config.LOGTAG, "Axolotl confirmed. Setting menu item checked!"); popup.getMenu().findItem(R.id.encryption_choice_axolotl) .setChecked(true); break; diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index eebeb040a..7c994c31a 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -266,29 +266,6 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } - public void showAxolotlNoSessionsDialog() { - Builder builder = new AlertDialog.Builder(this); - builder.setTitle("No Sessions"); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage("Your contact is not Axolotl-capable!"); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setNeutralButton("Foo", - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - } - }); - builder.setPositiveButton("Bar", - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - } - }); - builder.create().show(); - } - abstract void onBackendConnected(); protected void registerListeners() { From 3ab59c93a6d26362ab8ade6b88dadc099cc6cb07 Mon Sep 17 00:00:00 2001 From: Sam Whited Date: Thu, 9 Jul 2015 22:37:40 -0500 Subject: [PATCH 050/166] Add refresh icon to v21 theme --- .../conversations/ui/EditAccountActivity.java | 1 - .../drawable-hdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 508 bytes .../drawable-mdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 356 bytes .../drawable-xhdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 644 bytes .../drawable-xxhdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 882 bytes .../drawable-xxxhdpi/ic_refresh_grey600_24dp.png | Bin 0 -> 1161 bytes src/main/res/values-v21/themes.xml | 1 + 7 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png create mode 100644 src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png create mode 100644 src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png create mode 100644 src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png create mode 100644 src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index ab4dc0592..b33e909f8 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.ui; -import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.PendingIntent; import android.content.DialogInterface; diff --git a/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..51cc4dbd1568c1f132dafa2b73d46c5e66f85bd0 GIT binary patch literal 508 zcmV004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00DGKL_t(Y$L-azPQpMO$MFt=1BS#&6QfXg24jrAgM*V+ zh@*p}-w>HZUcF`RKH0IFzj33D4hAuMFy5xwc9_NTRl;G2be77ch7-SJ|D5Zw< z$YWs=V2&sj#Q}D&o~}?rb$Vjm_HLWn(Cl0wQx#PvV1mQtL zg?U5}-dOgET1)NP3Y!i^RaH{%_{r>(!4bG4Nt}6t#7PcO=~G=WkKx z$J50z#Nzbb%l2N(i2`jObA8JElN6u*zgov0@H)eZBX8p5h|cL8%Y-fpN2G)sBn!8s z?B|Hqx?F3+Z+v)9yjphn&3J(}FX^rR(n}Bi`>LfY68308nb*$ji9%fdhpt`m2tR%! zMt4ugmJLF2u`*v|e%$z;#o?S--IAVH*Otw()WEh=y`|T{3P?u1*tXL-`(dQScENY8 zyH&Ob_fug%JP% literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..7891efffa64deb1140c0c039b32bf4a0493bf96c GIT binary patch literal 644 zcmV-~0(004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00I0-L_t(o!|j(%Pg6k@hD-Udlp=8_LI|lPVMY8Apc|IZ z2yxLr@vJRh(gF(-7R3r1q6-sO{0Ipo8Ut&wd`bz#g#^<|a@owC+=gLh?$s_JH)qqP z?|Glj?Kx*oBSh*Okq{69?hBxuG2XGo8C9-0=L@Th^H8D8`Umu}$|a5>E?FT}qn*I< z2WVx1ze;mAs>~4+>PZ}bfD}7gb2qk05@AZbvj#k)>@oSy41;viLL-kEBF};6(=tW* z+7giR*zYnz^oF8|3_lg;u>!QREfdy=-+IbNtOo<+r95Y-u_)jC15#Aww>K5>RFD%C z<+G3bwzmKzxDe7&gZ-qkXpMlhEX=6Ze&Uv{FaiofI1|X;0`OS~2Q~ItOEl8Q)pZ(-lSC!zjBx9wguD;s~_Xul-2uk9DFnCG7QKti*S zeU!Q_n*D5cNjZ}o_IRnNs779~FEvN1$D?0c20RFd5;>mJ%>$b0;stq+xx+F2L3Y#R zjSF2JU+D?D{Km-11wb7YruAddT!v36%GRrm-xTQmKkIvfG|OyIrb3ldHYqTwsjl!Z eR0s$GHE|8G+Mu|4IRdW$0000004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00QbsL_t(&-tE~>NK`=_!0}zl^;E>Jl$#zps2fzE*ujz@ zJVf0JgIX;}q9oDu&=jfT1HzTfYC-g`6i=KaQntFW5OfDFih49I{CX!AgRnh7z^3=vjH5M`DLLY$@C zGPX}IsFW_INN&)k=*F8hzMrH%Pz52DHpHyA7(f>G+T9)FletXbSO2cV|3yA!6;GOC8>`2ht|`c91293QirtE}nDQU93`GR;$> zT0y%d^`i^QpiU{zHT9C-E`A7!mZ($W93n1ciLlK{jj>0%wl4$}de6L+oXEAT2 zbYD4Oi&M{>6#opj(<*FrO0i*?OF47eSx&i}a`jio-*z&s9P^YtmQj19k@b5zY}8e! zZp%=xdO$UeJ+! z&6BU~sU4i=CSj(TBhCunc*hunoMNZhz%w8NG9Uvop#K;23vmac8o+8Me*gdg07*qo IM6N<$f->2T>i_@% literal 0 HcmV?d00001 diff --git a/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png b/src/main/res/drawable-xxxhdpi/ic_refresh_grey600_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..e44a6d2823f87827cc85b22153932381831463fc GIT binary patch literal 1161 zcmV;41a|w0P)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0RM-N%)bBt010qN zS#tmY3ljhU3ljkVnw%H_00aR^L_t(|+U=TMXjDfShIcnHAu)+*DphC^Y5H?jEk+_m zysAIsB1E+9m8Au5yy#jJc}O8xO@fI{r6nO2QE6z6KZRnT(n2o^S}F#i*n*)+f8s?* zH0DRsYQSKO7jdi3%x-qi`Od@x!*{prdFOq0=giFc&Kn^T?us}72jBo4fCF#<4!{98 z00-azo*sZ@yhAfxTxN(#rkQ4vAuiKJ6L0f;hHCrl0Yq6xD}OUHNA>X;Z=`L1#ty(U z)bh`q)}O?vp=b`(dS+|@l<*0+Gcup32^vyB)Ia=Ipa3!AjC(Sls2gl2YEW+Y{~G}U zyh^t(^NBjg@}%|y1bB}-BKBt|(_2aG2LzBso0$1T?SD9x8kNrj0LY~)rIjI05N9K$ zl#oLfg_QCpyEx5o%IiAG*6arWki!p2EsW4gS<1a)ErGgjYJ`)CraVE*&1H{Qo zOQV=NZl*o8gaJM`4sw?!@-xzTmLuG^5Ad#Wh+bBBS_;-PWEbFN+}rxd}sRr>+}>VbD-*qXcj3P5St0MyP~ zN!S3?_Ek>U0R9a-z+l*?P;c08;$IYiO<@E0Qurus9pEDc;AGeUDiwfhVFM^;T0vMH zw)e!pDgbSP**U6=bY2FaS&<#}{YY=FMOyE5w+z5*+7b7|Kmh8raY|(XBK)QZ53*3) zzTUh%FLe(@*s1+C2gCvN*M{5V0P-18#HUy(YJaV^)(xb0Z1g@q`vjW1kFpke(&7Wz zxJ?n)wIR+3wAW|fI{&C?UnN&MFSO0@zWu8zkeZ{4uk61U=mx) zjc<3mDD<&)eQ!`4W=T+yv(3iN&7ZvP$^L5oGN|3BBFN0#0VBc`Eg5cYg|u+bs8t(m z=r77wNh2OLX`Yb^s7@ODI2L?rF`Ms_wgE?Jr`(vpO5m5%o+9Jbnfa37ZRI$gvYR=~ zH|(NLANjZZt}Zc;M+{+rihZ+!8ZC&X)N|?!L&+BKCjc zh53peM|qzczRX`w@6a4%@U@t|jCnFYMxBpcTr_%IKn+(iGJl1*fAewxv$cwY{61%p ztcPaG?Y31itHo?#51m}1pAqgd#W(|WbBabPd2T-XLQi!$00-az9DoCG01m(bH~Usni literal 0 HcmV?d00001 diff --git a/src/main/res/values-v21/themes.xml b/src/main/res/values-v21/themes.xml index 78deb772f..9e8775f90 100644 --- a/src/main/res/values-v21/themes.xml +++ b/src/main/res/values-v21/themes.xml @@ -21,6 +21,7 @@ @drawable/ic_done_black_24dp @drawable/ic_group_white_24dp @drawable/ic_add_white_24dp + @drawable/ic_refresh_grey600_24dp @drawable/ic_attach_file_white_24dp @drawable/ic_lock_open_white_24dp @drawable/ic_delete_grey600_24dp From fca0c367576e4cd1f218c663ad2da6a1d4bfb392 Mon Sep 17 00:00:00 2001 From: Sam Whited Date: Thu, 9 Jul 2015 23:08:44 -0500 Subject: [PATCH 051/166] Fix copying of axolotl keys to clipboard --- .../java/eu/siacs/conversations/ui/EditAccountActivity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index b33e909f8..e3a43b8b3 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -324,7 +324,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint); this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box); - this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); + this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard); this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key); this.mAxolotlDevicelist = (TextView) findViewById(R.id.axolotl_devicelist); this.mAxolotlDevicelistBox = (RelativeLayout) findViewById(R.id.axolotl_devices_box); From e8ec2ee628076aa43132c3214b786ddde64f93a6 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 15 Jul 2015 15:45:12 +0200 Subject: [PATCH 052/166] Don't merge messages with different trust statuses --- .../java/eu/siacs/conversations/entities/Message.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index ac6c66250..698775c8a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -399,7 +399,8 @@ public class Message extends AbstractEntity { !message.getBody().startsWith(ME_COMMAND) && !this.getBody().startsWith(ME_COMMAND) && !this.bodyIsHeart() && - !message.bodyIsHeart() + !message.bodyIsHeart() && + this.isTrusted() == message.isTrusted() ); } @@ -679,4 +680,9 @@ public class Message extends AbstractEntity { public String getAxolotlFingerprint() { return axolotlFingerprint; } + + public boolean isTrusted() { + return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint) + == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED; + } } From 4038af2f4764f1d35d82822514d65d17ab45b302 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Wed, 15 Jul 2015 16:32:42 +0200 Subject: [PATCH 053/166] Fix trust status for outgoing messages Tag sent messages with own fingerprint, set own fingerprint as always trusted, include own fingerprint in database trust search, explicitly reset trust colorfilter --- .../crypto/axolotl/AxolotlService.java | 16 ++++---- .../persistance/DatabaseBackend.java | 37 ++++++++++++------- .../services/XmppConnectionService.java | 4 ++ .../ui/ContactDetailsActivity.java | 6 +-- .../ui/adapter/MessageAdapter.java | 8 ++-- 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 23adbad84..57e57f7f3 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -267,12 +267,12 @@ public class AxolotlService { return true; } - public Trust getFingerprintTrust(String name, String fingerprint) { - return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, name, fingerprint); + public Trust getFingerprintTrust(String fingerprint) { + return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint); } - public void setFingerprintTrust(String name, String fingerprint, Trust trust) { - mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, name, fingerprint, trust); + public void setFingerprintTrust(String fingerprint, Trust trust) { + mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust); } // -------------------------------------- @@ -844,12 +844,12 @@ public class AxolotlService { return sessions.hasAny(address) || ( deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty()); } - public SQLiteAxolotlStore.Trust getFingerprintTrust(String name, String fingerprint) { - return axolotlStore.getFingerprintTrust(name, fingerprint); + public SQLiteAxolotlStore.Trust getFingerprintTrust(String fingerprint) { + return axolotlStore.getFingerprintTrust(fingerprint); } - public void setFingerprintTrust(String name, String fingerprint, SQLiteAxolotlStore.Trust trust) { - axolotlStore.setFingerprintTrust(name, fingerprint, trust); + public void setFingerprintTrust(String fingerprint, SQLiteAxolotlStore.Trust trust) { + axolotlStore.setFingerprintTrust(fingerprint, trust); } private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 4c6bf2215..39ef5d366 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -785,20 +785,28 @@ public class DatabaseBackend extends SQLiteOpenHelper { return getIdentityKeyCursor(account, name, own, null); } - private Cursor getIdentityKeyCursor(Account account, String name, boolean own, String fingerprint) { + private Cursor getIdentityKeyCursor(Account account, String fingerprint) { + return getIdentityKeyCursor(account, null, null, fingerprint); + } + + private Cursor getIdentityKeyCursor(Account account, String name, Boolean own, String fingerprint) { final SQLiteDatabase db = this.getReadableDatabase(); String[] columns = {AxolotlService.SQLiteAxolotlStore.TRUSTED, AxolotlService.SQLiteAxolotlStore.KEY}; ArrayList selectionArgs = new ArrayList<>(4); selectionArgs.add(account.getUuid()); - selectionArgs.add(name); - selectionArgs.add(own?"1":"0"); - String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.OWN + " = ? "; + String selectionString = AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ?"; + if (name != null){ + selectionArgs.add(name); + selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.NAME + " = ?"; + } if (fingerprint != null){ selectionArgs.add(fingerprint); - selectionString += "AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? "; + selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ?"; + } + if (own != null){ + selectionArgs.add(own?"1":"0"); + selectionString += " AND " +AxolotlService.SQLiteAxolotlStore.OWN + " = ?"; } Cursor cursor = db.query(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, columns, @@ -842,6 +850,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { } private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) { + storeIdentityKey(account, name, own, fingerprint, base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust.UNDECIDED); + } + + private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, AxolotlService.SQLiteAxolotlStore.Trust trusted) { SQLiteDatabase db = this.getWritableDatabase(); ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.ACCOUNT, account.getUuid()); @@ -849,11 +861,12 @@ public class DatabaseBackend extends SQLiteOpenHelper { values.put(AxolotlService.SQLiteAxolotlStore.OWN, own ? 1 : 0); values.put(AxolotlService.SQLiteAxolotlStore.FINGERPRINT, fingerprint); values.put(AxolotlService.SQLiteAxolotlStore.KEY, base64Serialized); + values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trusted.ordinal()); db.insert(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values); } - public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String name, String fingerprint) { - Cursor cursor = getIdentityKeyCursor(account, name, false, fingerprint); + public AxolotlService.SQLiteAxolotlStore.Trust isIdentityKeyTrusted(Account account, String fingerprint) { + Cursor cursor = getIdentityKeyCursor(account, fingerprint); AxolotlService.SQLiteAxolotlStore.Trust trust = null; if (cursor.getCount() > 0) { cursor.moveToFirst(); @@ -864,18 +877,16 @@ public class DatabaseBackend extends SQLiteOpenHelper { return trust; } - public boolean setIdentityKeyTrust(Account account, String name, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) { + public boolean setIdentityKeyTrust(Account account, String fingerprint, AxolotlService.SQLiteAxolotlStore.Trust trust) { SQLiteDatabase db = this.getWritableDatabase(); String[] selectionArgs = { account.getUuid(), - name, fingerprint }; ContentValues values = new ContentValues(); values.put(AxolotlService.SQLiteAxolotlStore.TRUSTED, trust.ordinal()); int rows = db.update(AxolotlService.SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, AxolotlService.SQLiteAxolotlStore.ACCOUNT + " = ? AND " - + AxolotlService.SQLiteAxolotlStore.NAME + " = ? AND " + AxolotlService.SQLiteAxolotlStore.FINGERPRINT + " = ? ", selectionArgs); return rows == 1; @@ -886,7 +897,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) { - storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT)); + storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED); } public void recreateAxolotlDb() { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ca62b7db6..92e4cf35e 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -759,6 +759,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa packet = account.getAxolotlService().fetchPacketFromCache(message); if (packet == null && account.isOnlineAndConnected()) { account.getAxolotlService().prepareMessage(message); + message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); } break; @@ -789,6 +790,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa conversation.startOtrSession(message.getCounterpart().getResourcepart(), false); } break; + case Message.ENCRYPTION_AXOLOTL: + message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); + break; } } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index ebc1ae839..ef99b4915 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -393,7 +393,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); final Jid bareJid = contactJid.toBareJid(); AxolotlService.SQLiteAxolotlStore.Trust trust = contact.getAccount().getAxolotlService() - .getFingerprintTrust(bareJid.toString(), fingerprint); + .getFingerprintTrust(fingerprint); switch (trust) { case TRUSTED: removeButton.setVisibility(View.VISIBLE); @@ -413,7 +413,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd removeButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - axolotlService.setFingerprintTrust(bareJid.toString(), fingerprint, + axolotlService.setFingerprintTrust(fingerprint, AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); refreshUi(); xmppConnectionService.updateConversationUi(); @@ -422,7 +422,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd trustButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { - axolotlService.setFingerprintTrust(bareJid.toString(), fingerprint, + axolotlService.setFingerprintTrust(fingerprint, AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED); refreshUi(); xmppConnectionService.updateConversationUi(); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index d2c75a5ee..df3a391b1 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -156,15 +156,15 @@ public class MessageAdapter extends ArrayAdapter { viewHolder.indicator.setVisibility(View.GONE); } else { viewHolder.indicator.setVisibility(View.VISIBLE); - if (message.getMergedStatus() == Message.STATUS_RECEIVED - && message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { + if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { AxolotlService.SQLiteAxolotlStore.Trust trust = message.getConversation() .getAccount().getAxolotlService().getFingerprintTrust( - message.getContact().getJid().toBareJid().toString(), message.getAxolotlFingerprint()); - if (trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) { + if(trust == null || trust != AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED) { viewHolder.indicator.setColorFilter(Color.RED); + } else { + viewHolder.indicator.clearColorFilter(); } } } From 43703870e87bfda94993439f57c7d2692aaf7783 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 17 Jul 2015 18:40:27 +0200 Subject: [PATCH 054/166] Remove unneccessary code --- .../eu/siacs/conversations/services/XmppConnectionService.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 92e4cf35e..60815f5e0 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -755,9 +755,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } break; case Message.ENCRYPTION_AXOLOTL: - message.setStatus(Message.STATUS_WAITING); packet = account.getAxolotlService().fetchPacketFromCache(message); - if (packet == null && account.isOnlineAndConnected()) { + if (packet == null) { account.getAxolotlService().prepareMessage(message); message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); } From 2045a7126242ef7656a38e445b636ae87d4b763e Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 17 Jul 2015 19:44:05 +0200 Subject: [PATCH 055/166] Handle file transmission properly in axolotl --- .../services/XmppConnectionService.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 60815f5e0..08c0b3fa5 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -755,10 +755,18 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } break; case Message.ENCRYPTION_AXOLOTL: - packet = account.getAxolotlService().fetchPacketFromCache(message); - if (packet == null) { - account.getAxolotlService().prepareMessage(message); - message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); + if (message.needsUploading()) { + if (account.httpUploadAvailable() || message.fixCounterpart()) { + this.sendFileMessage(message); + } else { + break; + } + } else { + packet = account.getAxolotlService().fetchPacketFromCache(message); + if (packet == null) { + account.getAxolotlService().prepareMessage(message); + message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", "")); + } } break; From 9c4d55f82ce50391ac09b4f7d7a0f3576c014e56 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Fri, 17 Jul 2015 19:44:45 +0200 Subject: [PATCH 056/166] Send correct body for HTTP files When using HTTP upload to send files, take care to transmit only the URL rather than the entire body, which contains metadata. --- .../conversations/crypto/axolotl/AxolotlService.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 57e57f7f3..8358125d2 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -961,8 +961,14 @@ public class AxolotlService { @Nullable public XmppAxolotlMessage encrypt(Message message ){ + final String content; + if (message.hasFileOnRemoteHost()) { + content = message.getFileParams().url.toString(); + } else { + content = message.getBody(); + } final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(), - getOwnDeviceId(), message.getBody()); + getOwnDeviceId(), content); if(findSessionsforContact(message.getContact()).isEmpty()) { return null; From 6f67469bda0ffe97cb8cd8d400affed5a17c34c5 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 19 Jul 2015 14:09:49 +0200 Subject: [PATCH 057/166] Refactor trust key ui and show in account details Refactored the trust key row UI element so it can be used in multiple places. It now also uses a slider to toggle the trust state, and the redundant trust state description was removed. EditAccountActivity now shows the keys of other devices associated with that account. --- build.gradle | 1 + .../ui/ContactDetailsActivity.java | 49 +----------- .../conversations/ui/EditAccountActivity.java | 22 ++++++ .../siacs/conversations/ui/XmppActivity.java | 79 +++++++++++++++++++ src/main/res/layout/activity_edit_account.xml | 33 +++++++- src/main/res/layout/contact_key.xml | 20 ++--- src/main/res/values/strings.xml | 2 + src/main/res/values/styles.xml | 6 ++ 8 files changed, 154 insertions(+), 58 deletions(-) diff --git a/build.gradle b/build.gradle index d16fd3b88..5031c01f6 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,7 @@ dependencies { compile 'me.leolin:ShortcutBadger:1.1.1@aar' compile 'com.kyleduo.switchbutton:library:1.2.8' compile 'org.whispersystems:axolotl-android:1.3.4' + compile 'com.kyleduo.switchbutton:library:1.2.8' } android { diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index ef99b4915..e7a8ffb75 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -35,7 +35,6 @@ import java.util.List; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; @@ -381,53 +380,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys( contact.getAccount(), contact.getJid().toBareJid().toString())) { hasKeys = true; - View view = inflater.inflate(R.layout.contact_key, keys, false); - TextView key = (TextView) view.findViewById(R.id.key); - TextView keyType = (TextView) view.findViewById(R.id.key_type); - TextView keyTrust = (TextView) view.findViewById(R.id.key_trust); - ImageButton removeButton = (ImageButton) view - .findViewById(R.id.button_remove); - ImageButton trustButton = (ImageButton) view - .findViewById(R.id.button_trust); - final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); - final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); - final Jid bareJid = contactJid.toBareJid(); - AxolotlService.SQLiteAxolotlStore.Trust trust = contact.getAccount().getAxolotlService() - .getFingerprintTrust(fingerprint); - switch (trust) { - case TRUSTED: - removeButton.setVisibility(View.VISIBLE); - //Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting remove button visible!"); - break; - case UNDECIDED: - case UNTRUSTED: - //Log.d(Config.LOGTAG, AxolotlService.getLogprefix(contact.getAccount()) + "Setting trust button visible!"); - trustButton.setVisibility(View.VISIBLE); - break; - } - keyType.setText("Axolotl Fingerprint"); - key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); - keyTrust.setText(trust.toString()); - keyTrust.setVisibility(View.VISIBLE); - keys.addView(view); - removeButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - axolotlService.setFingerprintTrust(fingerprint, - AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); - refreshUi(); - xmppConnectionService.updateConversationUi(); - } - }); - trustButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - axolotlService.setFingerprintTrust(fingerprint, - AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED); - refreshUi(); - xmppConnectionService.updateConversationUi(); - } - }); + addFingerprintRow(keys, contact.getAccount(), identityKey); } if (contact.getPgpKeyId() != 0) { hasKeys = true; diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index e3a43b8b3..77ca2a679 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -26,6 +26,8 @@ import android.widget.TableLayout; import android.widget.TextView; import android.widget.Toast; +import org.whispersystems.libaxolotl.IdentityKey; + import java.util.Set; import eu.siacs.conversations.R; @@ -69,6 +71,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private ImageButton mAxolotlFingerprintToClipboardButton; private ImageButton mWipeAxolotlPepButton; private ImageButton mRegenerateAxolotlKeyButton; + private LinearLayout keys; + private LinearLayout keysCard; private Jid jidToEdit; private Account mAccount; @@ -329,6 +333,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mAxolotlDevicelist = (TextView) findViewById(R.id.axolotl_devicelist); this.mAxolotlDevicelistBox = (RelativeLayout) findViewById(R.id.axolotl_devices_box); this.mWipeAxolotlPepButton = (ImageButton) findViewById(R.id.action_wipe_axolotl_pep); + this.keysCard = (LinearLayout) findViewById(R.id.other_device_keys_card); + this.keys = (LinearLayout) findViewById(R.id.other_device_keys); this.mSaveButton = (Button) findViewById(R.id.save_button); this.mCancelButton = (Button) findViewById(R.id.cancel_button); this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener); @@ -568,6 +574,22 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else { this.mAxolotlFingerprintBox.setVisibility(View.GONE); } + final IdentityKey ownKey = mAccount.getAxolotlService().getOwnPublicKey(); + boolean hasKeys = false; + keys.removeAllViews(); + for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys( + mAccount, mAccount.getJid().toBareJid().toString())) { + if(ownKey.equals(identityKey)) { + continue; + } + hasKeys = true; + addFingerprintRow(keys, mAccount, identityKey); + } + if (hasKeys) { + keysCard.setVisibility(View.VISIBLE); + } else { + keysCard.setVisibility(View.GONE); + } } else { if (this.mAccount.errorStatus()) { this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId())); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 7c994c31a..9dfece2f5 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -43,8 +43,11 @@ import android.util.Log; import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; +import android.widget.CompoundButton; import android.widget.EditText; import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; import android.widget.Toast; import com.google.zxing.BarcodeFormat; @@ -53,9 +56,12 @@ import com.google.zxing.WriterException; import com.google.zxing.common.BitMatrix; import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.kyleduo.switchbutton.SwitchButton; import net.java.otr4j.session.SessionID; +import org.whispersystems.libaxolotl.IdentityKey; + import java.io.FileNotFoundException; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -65,6 +71,7 @@ import java.util.concurrent.RejectedExecutionException; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -74,6 +81,8 @@ import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; +import eu.siacs.conversations.ui.widget.Switch; +import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -588,6 +597,76 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } + protected void addFingerprintRow(LinearLayout keys, final Account account, IdentityKey identityKey) { + final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); + final AxolotlService.SQLiteAxolotlStore.Trust trust = account.getAxolotlService() + .getFingerprintTrust(fingerprint); + addFingerprintRowWithListeners(keys, account, identityKey, trust, true, + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + if (isChecked != (trust == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED)) { + account.getAxolotlService().setFingerprintTrust(fingerprint, + (isChecked) ? AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED : + AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); + } + refreshUi(); + xmppConnectionService.updateAccountUi(); + xmppConnectionService.updateConversationUi(); + } + }, + new View.OnClickListener() { + @Override + public void onClick(View v) { + account.getAxolotlService().setFingerprintTrust(fingerprint, + AxolotlService.SQLiteAxolotlStore.Trust.UNTRUSTED); + refreshUi(); + xmppConnectionService.updateAccountUi(); + xmppConnectionService.updateConversationUi(); + } + } + + ); + } + + protected void addFingerprintRowWithListeners(LinearLayout keys, final Account account, + IdentityKey identityKey, + AxolotlService.SQLiteAxolotlStore.Trust trust, + boolean showTag, + CompoundButton.OnCheckedChangeListener + onCheckedChangeListener, + View.OnClickListener onClickListener) { + View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false); + TextView key = (TextView) view.findViewById(R.id.key); + TextView keyType = (TextView) view.findViewById(R.id.key_type); + Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust); + trustToggle.setVisibility(View.VISIBLE); + trustToggle.setOnCheckedChangeListener(onCheckedChangeListener); + trustToggle.setOnClickListener(onClickListener); + + switch (trust) { + case UNTRUSTED: + case TRUSTED: + trustToggle.setChecked(trust == AxolotlService.SQLiteAxolotlStore.Trust.TRUSTED, false); + trustToggle.setEnabled(true); + break; + case UNDECIDED: + trustToggle.setChecked(false, false); + trustToggle.setEnabled(false); + break; + } + + if (showTag) { + keyType.setText(getString(R.string.axolotl_fingerprint)); + } else { + keyType.setVisibility(View.GONE); + } + + key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())); + keys.addView(view); + + } + public void selectPresence(final Conversation conversation, final OnPresenceSelected listener) { final Contact contact = conversation.getContact(); diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index df20e6f2d..3d65365d6 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -368,7 +368,7 @@ android:layout_height="wrap_content" android:textColor="@color/black54" android:textSize="?attr/TextSizeInfo" - android:text="@string/axolotl_fingerprint"/> + android:text="@string/this_device_axolotl_fingerprint"/> + + + + + + + diff --git a/src/main/res/layout/contact_key.xml b/src/main/res/layout/contact_key.xml index a43f8083b..64f6075ca 100644 --- a/src/main/res/layout/contact_key.xml +++ b/src/main/res/layout/contact_key.xml @@ -4,10 +4,10 @@ android:layout_height="match_parent" > @@ -37,27 +38,28 @@ android:visibility="gone" android:textColor="@color/black54" android:textSize="?attr/TextSizeInfo"/> - + android:visibility="gone" /> - + style="@style/MaterialDesignButton"/> + + \ No newline at end of file diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6808f1bb5..f4c75ee1f 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -209,6 +209,8 @@ Your fingerprint OTR fingerprint Axolotl fingerprint + Own Axolotl fingerprint + Other devices Other own Axolotl Devices Verify Decrypt diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index e8572d9d4..d609b5fb3 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -18,4 +18,10 @@ 16dp + + \ No newline at end of file From ec0aff4ed7982cc6db43cb6337f828f732429fd2 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 19 Jul 2015 14:17:25 +0200 Subject: [PATCH 058/166] Encrypt files for HTTP upload in encrypted chats --- .../eu/siacs/conversations/http/HttpDownloadConnection.java | 6 ++++-- .../eu/siacs/conversations/http/HttpUploadConnection.java | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 62fe4191d..a9c106ab1 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -70,7 +70,8 @@ public class HttpDownloadConnection implements Transferable { String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null; if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) { this.message.setEncryption(Message.ENCRYPTION_PGP); - } else if (message.getEncryption() != Message.ENCRYPTION_OTR) { + } else if (message.getEncryption() != Message.ENCRYPTION_OTR + && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) { this.message.setEncryption(Message.ENCRYPTION_NONE); } String extension; @@ -86,7 +87,8 @@ public class HttpDownloadConnection implements Transferable { this.file.setKey(CryptoHelper.hexToBytes(reference)); } - if (this.message.getEncryption() == Message.ENCRYPTION_OTR + if ((this.message.getEncryption() == Message.ENCRYPTION_OTR + || this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL) && this.file.getKey() == null) { this.message.setEncryption(Message.ENCRYPTION_NONE); } diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index a3ab8daba..c0d4455a1 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -88,7 +88,9 @@ public class HttpUploadConnection implements Transferable { this.file = mXmppConnectionService.getFileBackend().getFile(message, false); this.file.setExpectedSize(this.file.getSize()); - if (Config.ENCRYPT_ON_HTTP_UPLOADED) { + if (Config.ENCRYPT_ON_HTTP_UPLOADED + || message.getEncryption() == Message.ENCRYPTION_AXOLOTL + || message.getEncryption() == Message.ENCRYPTION_OTR) { this.key = new byte[48]; mXmppConnectionService.getRNG().nextBytes(this.key); this.file.setKey(this.key); From 14010bf5a6198e4e53ba3f86328d061cf20b8da1 Mon Sep 17 00:00:00 2001 From: Andreas Straub Date: Sun, 19 Jul 2015 18:36:28 +0200 Subject: [PATCH 059/166] Ask for key trust when sending messages If the contact (or the own account) has keys that have UNDECIDED trust, we now drop the user into the new TrustKeysActivity, where they have to decide for each new key whether it should be TRUSTED or UNTRUSTED. --- src/main/AndroidManifest.xml | 4 + .../crypto/axolotl/AxolotlService.java | 137 ++++++---- .../persistance/DatabaseBackend.java | 9 + .../services/XmppConnectionService.java | 46 +++- .../ui/ConversationActivity.java | 56 ++++- .../ui/ConversationFragment.java | 23 +- .../conversations/ui/TrustKeysActivity.java | 237 ++++++++++++++++++ .../siacs/conversations/ui/XmppActivity.java | 9 +- .../xmpp/OnNewKeysAvailable.java | 5 + src/main/res/layout/activity_trust_keys.xml | 120 +++++++++ src/main/res/values/strings.xml | 3 + 11 files changed, 586 insertions(+), 63 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java create mode 100644 src/main/java/eu/siacs/conversations/xmpp/OnNewKeysAvailable.java create mode 100644 src/main/res/layout/activity_trust_keys.xml diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 9fe370171..b0611f848 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -130,6 +130,10 @@ + getContactUndecidedKeys(String bareJid) { + return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, Trust.UNDECIDED); + } + // -------------------------------------- // SessionStore // -------------------------------------- @@ -658,6 +666,14 @@ public class AxolotlService { return axolotlStore.getIdentityKeyPair().getPublicKey(); } + public Set getPendingKeys() { + return axolotlStore.getContactUndecidedKeys(account.getJid().toBareJid().toString()); + } + + public Set getPendingKeys(Contact contact) { + return axolotlStore.getContactUndecidedKeys(contact.getJid().toBareJid().toString()); + } + private AxolotlAddress getAddressForJid(Jid jid) { return new AxolotlAddress(jid.toString(), 0); } @@ -852,14 +868,32 @@ public class AxolotlService { axolotlStore.setFingerprintTrust(fingerprint, trust); } - private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Building new sesstion for " + address.getDeviceId()); + private void buildSessionFromPEP(final Conversation conversation, final AxolotlAddress address, final boolean flushWaitingQueueAfterFetch) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId()); try { IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice( Jid.fromString(address.getName()), address.getDeviceId()); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Retrieving bundle: " + bundlesPacket); mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() { + private void finish() { + AxolotlAddress ownAddress = new AxolotlAddress(conversation.getAccount().getJid().toBareJid().toString(),0); + AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0); + if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + && !fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) { + if (flushWaitingQueueAfterFetch) { + conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL, + new Conversation.OnMessageFound() { + @Override + public void onMessageFound(Message message) { + processSending(message); + } + }); + } + mXmppConnectionService.newKeysAvailable(); + } + } + @Override public void onIqPacketReceived(Account account, IqPacket packet) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received preKey IQ packet, processing..."); @@ -869,6 +903,7 @@ public class AxolotlService { if (preKeyBundleList.isEmpty() || bundle == null) { Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account)+"preKey IQ packet invalid: " + packet); fetchStatusMap.put(address, FetchStatus.ERROR); + finish(); return; } Random random = new Random(); @@ -876,6 +911,7 @@ public class AxolotlService { if (preKey == null) { //should never happen fetchStatusMap.put(address, FetchStatus.ERROR); + finish(); return; } @@ -898,18 +934,7 @@ public class AxolotlService { fetchStatusMap.put(address, FetchStatus.ERROR); } - AxolotlAddress ownAddress = new AxolotlAddress(conversation.getAccount().getJid().toBareJid().toString(),0); - AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0); - if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) - && !fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) { - conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_AXOLOTL, - new Conversation.OnMessageFound() { - @Override - public void onMessageFound(Message message) { - processSending(message); - } - }); - } + finish(); } }); } catch (InvalidJidException e) { @@ -917,48 +942,75 @@ public class AxolotlService { } } - private boolean createSessionsIfNeeded(Conversation conversation) { - boolean newSessions = false; - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Creating axolotl sessions if needed..."); + public Set findDevicesWithoutSession(final Conversation conversation) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + conversation.getContact().getJid().toBareJid()); Jid contactJid = conversation.getContact().getJid().toBareJid(); Set addresses = new HashSet<>(); if(deviceIds.get(contactJid) != null) { for(Integer foreignId:this.deviceIds.get(contactJid)) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found device "+account.getJid().toBareJid()+":"+foreignId); - addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); + AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId); + if(sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if ( identityKey != null ) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId); + addresses.add(new AxolotlAddress(contactJid.toString(), foreignId)); + } + } } } else { Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!"); } - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Checking own account "+account.getJid().toBareJid()); if(deviceIds.get(account.getJid().toBareJid()) != null) { for(Integer ownId:this.deviceIds.get(account.getJid().toBareJid())) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Found device "+account.getJid().toBareJid()+":"+ownId); - addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId)); - } - } - for (AxolotlAddress address : addresses) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Processing device: " + address.toString()); - FetchStatus status = fetchStatusMap.get(address); - XmppAxolotlSession session = sessions.get(address); - if ( session == null && ( status == null || status == FetchStatus.ERROR) ) { - IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); - if ( identityKey != null ) { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString() + ", adding to cache..."); - session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); - sessions.put(address, session); - } else { - fetchStatusMap.put(address, FetchStatus.PENDING); - this.buildSessionFromPEP(conversation, address); - newSessions = true; + AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId); + if(sessions.get(address) == null) { + IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); + if ( identityKey != null ) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString() + ", adding to cache..."); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); + sessions.put(address, session); + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId); + addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId)); + } } - } else { - Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already have session for " + address.toString()); } } + + return addresses; + } + + public boolean createSessionsIfNeeded(final Conversation conversation, final boolean flushWaitingQueueAfterFetch) { + Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed..."); + boolean newSessions = false; + Set addresses = findDevicesWithoutSession(conversation); + for (AxolotlAddress address : addresses) { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString()); + FetchStatus status = fetchStatusMap.get(address); + if ( status == null || status == FetchStatus.ERROR ) { + fetchStatusMap.put(address, FetchStatus.PENDING); + this.buildSessionFromPEP(conversation, address, flushWaitingQueueAfterFetch); + newSessions = true; + } else { + Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Already fetching bundle for " + address.toString()); + } + } + return newSessions; } + public boolean hasPendingKeyFetches(Conversation conversation) { + AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(),0); + AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(),0); + return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING) + ||fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING); + + } + @Nullable public XmppAxolotlMessage encrypt(Message message ){ final String content; @@ -1013,10 +1065,9 @@ public class AxolotlService { }); } - public void prepareMessage(Message message) { + public void prepareMessage(final Message message) { if (!messageCache.containsKey(message.getUuid())) { - boolean newSessions = createSessionsIfNeeded(message.getConversation()); - + boolean newSessions = createSessionsIfNeeded(message.getConversation(), true); if (!newSessions) { this.processSending(message); } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 39ef5d366..a2c62a8c5 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -834,10 +834,19 @@ public class DatabaseBackend extends SQLiteOpenHelper { } public Set loadIdentityKeys(Account account, String name) { + return loadIdentityKeys(account, name, null); + } + + public Set loadIdentityKeys(Account account, String name, AxolotlService.SQLiteAxolotlStore.Trust trust) { Set identityKeys = new HashSet<>(); Cursor cursor = getIdentityKeyCursor(account, name, false); while(cursor.moveToNext()) { + if ( trust != null && + cursor.getInt(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.TRUSTED)) + != trust.ordinal()) { + continue; + } try { identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0)); } catch (InvalidKeyException e) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 08c0b3fa5..cc113cefa 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -85,6 +85,7 @@ import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnMessageAcknowledged; import eu.siacs.conversations.xmpp.OnMessagePacketReceived; +import eu.siacs.conversations.xmpp.OnNewKeysAvailable; import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; @@ -307,6 +308,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private int rosterChangedListenerCount = 0; private OnMucRosterUpdate mOnMucRosterUpdate = null; private int mucRosterChangedListenerCount = 0; + private OnNewKeysAvailable mOnNewKeysAvailable = null; + private int newKeysAvailableListenerCount = 0; private SecureRandom mRandom; private OpenPgpServiceConnection pgpServiceConnection; private PgpEngine mPgpEngine = null; @@ -1344,17 +1347,17 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa switchToForeground(); } this.mOnUpdateBlocklist = listener; - if (this.updateBlocklistListenerCount < 2) { - this.updateBlocklistListenerCount++; + if (this.newKeysAvailableListenerCount < 2) { + this.newKeysAvailableListenerCount++; } } } public void removeOnUpdateBlocklistListener() { synchronized (this) { - this.updateBlocklistListenerCount--; - if (this.updateBlocklistListenerCount <= 0) { - this.updateBlocklistListenerCount = 0; + this.newKeysAvailableListenerCount--; + if (this.newKeysAvailableListenerCount <= 0) { + this.newKeysAvailableListenerCount = 0; this.mOnUpdateBlocklist = null; if (checkListeners()) { switchToBackground(); @@ -1363,6 +1366,30 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void setOnNewKeysAvailableListener(final OnNewKeysAvailable listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnNewKeysAvailable = listener; + if (this.newKeysAvailableListenerCount < 2) { + this.newKeysAvailableListenerCount++; + } + } + } + + public void removeOnNewKeysAvailableListener() { + synchronized (this) { + this.newKeysAvailableListenerCount--; + if (this.newKeysAvailableListenerCount <= 0) { + this.newKeysAvailableListenerCount = 0; + this.mOnNewKeysAvailable = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { synchronized (this) { if (checkListeners()) { @@ -1393,7 +1420,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null && this.mOnUpdateBlocklist == null - && this.mOnShowErrorToast == null); + && this.mOnShowErrorToast == null + && this.mOnNewKeysAvailable == null); } private void switchToForeground() { @@ -2281,6 +2309,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + public void newKeysAvailable() { + if(mOnNewKeysAvailable != null) { + mOnNewKeysAvailable.onNewKeysAvailable(); + } + } + public Account findAccountByJid(final Jid accountJid) { for (Account account : this.accounts) { if (account.getJid().toBareJid().equals(accountJid.toBareJid())) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 2e50af3bf..a6cd0431f 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -65,11 +65,14 @@ public class ConversationActivity extends XmppActivity public static final int REQUEST_SEND_MESSAGE = 0x0201; public static final int REQUEST_DECRYPT_PGP = 0x0202; public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; + public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208; + public static final int REQUEST_TRUST_KEYS_MENU = 0x0209; public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; + public static final int ATTACHMENT_CHOICE_INVALID = 0x0306; private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; private static final String STATE_PANEL_OPEN = "state_panel_open"; private static final String STATE_PENDING_URI = "state_pending_uri"; @@ -79,6 +82,7 @@ public class ConversationActivity extends XmppActivity final private List mPendingImageUris = new ArrayList<>(); final private List mPendingFileUris = new ArrayList<>(); private Uri mPendingGeoUri = null; + private boolean forbidProcessingPendings = false; private View mContentView; @@ -401,7 +405,7 @@ public class ConversationActivity extends XmppActivity return true; } - private void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { + protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { final Conversation conversation = getSelectedConversation(); final Account account = conversation.getAccount(); final OnPresenceSelected callback = new OnPresenceSelected() { @@ -537,7 +541,9 @@ public class ConversationActivity extends XmppActivity showInstallPgpDialog(); } } else { - selectPresenceToAttachFile(attachmentChoice,encryption); + if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) { + selectPresenceToAttachFile(attachmentChoice, encryption); + } } } @@ -962,18 +968,23 @@ public class ConversationActivity extends XmppActivity this.mConversationFragment.reInit(getSelectedConversation()); } - for(Iterator i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) { - attachImageToConversation(getSelectedConversation(),i.next()); - } + if(!forbidProcessingPendings) { + for (Iterator i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) { + Uri foo = i.next(); + attachImageToConversation(getSelectedConversation(), foo); + } - for(Iterator i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) { - attachFileToConversation(getSelectedConversation(),i.next()); - } + for (Iterator i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) { + attachFileToConversation(getSelectedConversation(), i.next()); + } - if (mPendingGeoUri != null) { - attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); - mPendingGeoUri = null; + if (mPendingGeoUri != null) { + attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); + mPendingGeoUri = null; + } } + forbidProcessingPendings = false; + ExceptionHelper.checkForCrash(this, this.xmppConnectionService); setIntent(new Intent()); } @@ -1083,6 +1094,9 @@ public class ConversationActivity extends XmppActivity attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); this.mPendingGeoUri = null; } + } else if (requestCode == REQUEST_TRUST_KEYS_TEXT || requestCode == REQUEST_TRUST_KEYS_MENU) { + this.forbidProcessingPendings = !xmppConnectionServiceBound; + mConversationFragment.onActivityResult(requestCode, resultCode, data); } } else { mPendingImageUris.clear(); @@ -1235,6 +1249,26 @@ public class ConversationActivity extends XmppActivity return getPreferences().getBoolean("indicate_received", false); } + protected boolean trustKeysIfNeeded(int requestCode) { + return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID); + } + + protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) { + AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService(); + if(!axolotlService.getPendingKeys(mSelectedConversation.getContact()).isEmpty() + || !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty()) { + axolotlService.createSessionsIfNeeded(mSelectedConversation, false); + Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class); + intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString()); + intent.putExtra("account", mSelectedConversation.getAccount().getJid().toBareJid().toString()); + intent.putExtra("choice", attachmentChoice); + startActivityForResult(intent, requestCode); + return true; + } else { + return false; + } + } + @Override protected void refreshUiReal() { updateConversationList(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 026c74adf..15491deab 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.ui; +import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; import android.app.PendingIntent; @@ -11,6 +12,7 @@ import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.text.InputType; +import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; @@ -43,6 +45,7 @@ import java.util.concurrent.ConcurrentLinkedQueue; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -304,7 +307,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) { sendPgpMessage(message); } else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_AXOLOTL) { - sendAxolotlMessage(message); + if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) { + sendAxolotlMessage(message); + } } else { sendPlainTextMessage(message); } @@ -1128,7 +1133,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa protected void sendAxolotlMessage(final Message message) { final ConversationActivity activity = (ConversationActivity) getActivity(); final XmppConnectionService xmppService = activity.xmppConnectionService; - //message.setCounterpart(conversation.getNextCounterpart()); xmppService.sendMessage(message); messageSent(); } @@ -1195,4 +1199,19 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa updateSendButton(); } + @Override + public void onActivityResult(int requestCode, int resultCode, + final Intent data) { + if (resultCode == Activity.RESULT_OK) { + if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) { + final String body = mEditMessage.getText().toString(); + Message message = new Message(conversation, body, conversation.getNextEncryption(activity.forceEncryption())); + sendAxolotlMessage(message); + } else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) { + int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID); + activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption(activity.forceEncryption())); + } + } + } + } diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java new file mode 100644 index 000000000..4efa4f6cf --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java @@ -0,0 +1,237 @@ +package eu.siacs.conversations.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.CompoundButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.whispersystems.libaxolotl.IdentityKey; + + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.AxolotlService.SQLiteAxolotlStore.Trust; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.xmpp.OnNewKeysAvailable; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class TrustKeysActivity extends XmppActivity implements OnNewKeysAvailable { + private Jid accountJid; + private Jid contactJid; + + private Contact contact; + private TextView ownKeysTitle; + private LinearLayout ownKeys; + private LinearLayout ownKeysCard; + private TextView foreignKeysTitle; + private LinearLayout foreignKeys; + private LinearLayout foreignKeysCard; + private Button mSaveButton; + private Button mCancelButton; + + private final Map ownKeysToTrust = new HashMap<>(); + private final Map foreignKeysToTrust = new HashMap<>(); + + private final OnClickListener mSaveButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + commitTrusts(); + Intent data = new Intent(); + data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID)); + setResult(RESULT_OK, data); + finish(); + } + }; + + private final OnClickListener mCancelButtonListener = new OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }; + + @Override + protected void refreshUiReal() { + invalidateOptionsMenu(); + populateView(); + } + + @Override + protected String getShareableUri() { + if (contact != null) { + return contact.getShareableUri(); + } else { + return ""; + } + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_trust_keys); + try { + this.accountJid = Jid.fromString(getIntent().getExtras().getString("account")); + } catch (final InvalidJidException ignored) { + } + try { + this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact")); + } catch (final InvalidJidException ignored) { + } + + ownKeysTitle = (TextView) findViewById(R.id.own_keys_title); + ownKeys = (LinearLayout) findViewById(R.id.own_keys_details); + ownKeysCard = (LinearLayout) findViewById(R.id.own_keys_card); + foreignKeysTitle = (TextView) findViewById(R.id.foreign_keys_title); + foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys_details); + foreignKeysCard = (LinearLayout) findViewById(R.id.foreign_keys_card); + mCancelButton = (Button) findViewById(R.id.cancel_button); + mCancelButton.setOnClickListener(mCancelButtonListener); + mSaveButton = (Button) findViewById(R.id.save_button); + mSaveButton.setOnClickListener(mSaveButtonListener); + + + if (getActionBar() != null) { + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + } + } + + private void populateView() { + setTitle(getString(R.string.trust_keys)); + ownKeys.removeAllViews(); + foreignKeys.removeAllViews(); + boolean hasOwnKeys = false; + boolean hasForeignKeys = false; + for(final IdentityKey identityKey : ownKeysToTrust.keySet()) { + hasOwnKeys = true; + addFingerprintRowWithListeners(ownKeys, contact.getAccount(), identityKey, + Trust.fromBoolean(ownKeysToTrust.get(identityKey)), false, + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + ownKeysToTrust.put(identityKey, isChecked); + refreshUi(); + xmppConnectionService.updateAccountUi(); + xmppConnectionService.updateConversationUi(); + } + }, + null + ); + } + for(final IdentityKey identityKey : foreignKeysToTrust.keySet()) { + hasForeignKeys = true; + addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), identityKey, + Trust.fromBoolean(foreignKeysToTrust.get(identityKey)), false, + new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + foreignKeysToTrust.put(identityKey, isChecked); + refreshUi(); + xmppConnectionService.updateAccountUi(); + xmppConnectionService.updateConversationUi(); + } + }, + null + ); + } + + if(hasOwnKeys) { + ownKeysTitle.setText(accountJid.toString()); + ownKeysCard.setVisibility(View.VISIBLE); + } + if(hasForeignKeys) { + foreignKeysTitle.setText(contactJid.toString()); + foreignKeysCard.setVisibility(View.VISIBLE); + } + } + + private void getFingerprints(final Account account) { + Set ownKeysSet = account.getAxolotlService().getPendingKeys(); + for(final IdentityKey identityKey : ownKeysSet) { + if(!ownKeysToTrust.containsKey(identityKey)) { + ownKeysToTrust.put(identityKey, false); + } + } + Set foreignKeysSet = account.getAxolotlService().getPendingKeys(contact); + for(final IdentityKey identityKey : foreignKeysSet) { + if(!foreignKeysToTrust.containsKey(identityKey)) { + foreignKeysToTrust.put(identityKey, false); + } + } + } + + @Override + public void onBackendConnected() { + if ((accountJid != null) && (contactJid != null)) { + final Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + return; + } + this.contact = account.getRoster().getContact(contactJid); + ownKeysToTrust.clear(); + foreignKeysToTrust.clear(); + getFingerprints(account); + + Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, false); + if(account.getAxolotlService().hasPendingKeyFetches(conversation)) { + lock(); + } + + populateView(); + } + } + + @Override + public void onNewKeysAvailable() { + runOnUiThread(new Runnable() { + @Override + public void run() { + final Account account = xmppConnectionService + .findAccountByJid(accountJid); + unlock(); + getFingerprints(account); + refreshUi(); + } + }); + } + + private void commitTrusts() { + for(IdentityKey identityKey:ownKeysToTrust.keySet()) { + contact.getAccount().getAxolotlService().setFingerprintTrust( + identityKey.getFingerprint().replaceAll("\\s", ""), + Trust.fromBoolean(ownKeysToTrust.get(identityKey))); + } + for(IdentityKey identityKey:foreignKeysToTrust.keySet()) { + contact.getAccount().getAxolotlService().setFingerprintTrust( + identityKey.getFingerprint().replaceAll("\\s", ""), + Trust.fromBoolean(foreignKeysToTrust.get(identityKey))); + } + } + + private void unlock() { + mSaveButton.setEnabled(true); + mSaveButton.setText(getString(R.string.done)); + mSaveButton.setTextColor(getPrimaryTextColor()); + } + + private void lock() { + mSaveButton.setEnabled(false); + mSaveButton.setText(getString(R.string.fetching_keys)); + mSaveButton.setTextColor(getSecondaryTextColor()); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 9dfece2f5..00322452c 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -84,6 +84,7 @@ import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinde import eu.siacs.conversations.ui.widget.Switch; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.xmpp.OnNewKeysAvailable; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -296,6 +297,9 @@ public abstract class XmppActivity extends Activity { if (this instanceof XmppConnectionService.OnShowErrorToast) { this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this); } + if (this instanceof OnNewKeysAvailable) { + this.xmppConnectionService.setOnNewKeysAvailableListener((OnNewKeysAvailable) this); + } } protected void unregisterListeners() { @@ -317,6 +321,9 @@ public abstract class XmppActivity extends Activity { if (this instanceof XmppConnectionService.OnShowErrorToast) { this.xmppConnectionService.removeOnShowErrorToastListener(); } + if (this instanceof OnNewKeysAvailable) { + this.xmppConnectionService.removeOnNewKeysAvailableListener(); + } } @Override @@ -452,7 +459,7 @@ public abstract class XmppActivity extends Activity { @Override public void userInputRequried(PendingIntent pi, - Account account) { + Account account) { try { startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnNewKeysAvailable.java b/src/main/java/eu/siacs/conversations/xmpp/OnNewKeysAvailable.java new file mode 100644 index 000000000..59dc1c1ea --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnNewKeysAvailable.java @@ -0,0 +1,5 @@ +package eu.siacs.conversations.xmpp; + +public interface OnNewKeysAvailable { + public void onNewKeysAvailable(); +} diff --git a/src/main/res/layout/activity_trust_keys.xml b/src/main/res/layout/activity_trust_keys.xml new file mode 100644 index 000000000..c535d51df --- /dev/null +++ b/src/main/res/layout/activity_trust_keys.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +