From 1c4578ceac4b42152304d3292ffc0b09c15ac0d2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 8 Jul 2015 15:46:07 +0200 Subject: [PATCH 0001/2005] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index c2174be1..552eac41 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,9 @@ dependencies. ./gradlew distTar +## Troubleshooting +If you use a version of the Oracle JRE and get an InvalidKeyException you need to enable unlimited strength crypto. See https://stackoverflow.com/questions/6481627/java-security-illegal-key-size-or-default-parameters for instructions. + ## License This project uses libtextsecure-java from Open Whisper Systems: From 6c02004326e59ebe2f43fd327d45e79bfc21969f Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 8 Jul 2015 16:14:48 +0200 Subject: [PATCH 0002/2005] Extract code from main() to functions --- src/main/java/cli/Main.java | 227 +++++++++++++++++++-------------- src/main/java/cli/Manager.java | 10 +- 2 files changed, 136 insertions(+), 101 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 4cdbb806..2a0cfe46 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -20,7 +20,6 @@ import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; import org.apache.commons.io.IOUtils; -import org.whispersystems.textsecure.api.TextSecureMessageSender; import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException; import org.whispersystems.textsecure.api.messages.*; import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage; @@ -46,39 +45,8 @@ public class Main { // Workaround for BKS truststore Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1); - ArgumentParser parser = ArgumentParsers.newArgumentParser("textsecure-cli") - .defaultHelp(true) - .description("Commandline interface for TextSecure."); - Subparsers subparsers = parser.addSubparsers() - .title("subcommands") - .dest("command") - .description("valid subcommands") - .help("additional help"); - Subparser parserRegister = subparsers.addParser("register"); - parserRegister.addArgument("-v", "--voice") - .help("The verification should be done over voice, not sms.") - .action(Arguments.storeTrue()); - Subparser parserVerify = subparsers.addParser("verify"); - parserVerify.addArgument("verificationCode") - .help("The verification code you received via sms or voice call."); - Subparser parserSend = subparsers.addParser("send"); - parserSend.addArgument("recipient") - .help("Specify the recipients' phone number.") - .nargs("*"); - parserSend.addArgument("-m", "--message") - .help("Specify the message, if missing standard input is used."); - parserSend.addArgument("-a", "--attachment") - .nargs("*") - .help("Add file as attachment"); - Subparser parserReceive = subparsers.addParser("receive"); - parser.addArgument("-u", "--username") - .required(true) - .help("Specify your phone number, that will be used for verification."); - Namespace ns = null; - try { - ns = parser.parseArgs(args); - } catch (ArgumentParserException e) { - parser.handleError(e); + Namespace ns = parseArgs(args); + if (ns == null) { System.exit(1); } @@ -88,10 +56,11 @@ public class Main { try { m.load(); } catch (Exception e) { - System.out.println("Loading file error: " + e.getMessage()); + System.out.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage()); System.exit(2); } } + switch (ns.getString("command")) { case "register": if (!m.userHasKeys()) { @@ -125,7 +94,6 @@ public class Main { System.out.println("User is not registered."); System.exit(1); } - TextSecureMessageSender messageSender = m.getMessageSender(); String messageText = ns.getString("message"); if (messageText == null) { try { @@ -135,10 +103,11 @@ public class Main { System.exit(1); } } - final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); + final List attachments = ns.getList("attachment"); + List textSecureAttachments = null; if (attachments != null) { - List textSecureAttachments = new ArrayList(attachments.size()); + textSecureAttachments = new ArrayList(attachments.size()); for (String attachment : attachments) { try { File attachmentFile = new File(attachment); @@ -148,37 +117,23 @@ public class Main { textSecureAttachments.add(new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null)); } catch (IOException e) { System.out.println("Failed to add attachment \"" + attachment + "\": " + e.getMessage()); + System.out.println("Aborting sending."); System.exit(1); } } - messageBuilder.withAttachments(textSecureAttachments); } - TextSecureDataMessage message = messageBuilder.build(); List recipients = new ArrayList<>(ns.getList("recipient").size()); for (String recipient : ns.getList("recipient")) { try { recipients.add(m.getPushAddress(recipient)); } catch (InvalidNumberException e) { - System.out.println("Failed to send message to \"" + recipient + "\": " + e.getMessage()); - } - } - try { - messageSender.sendMessage(recipients, message); - } catch (IOException e) { - System.out.println("Failed to send message: " + e.getMessage()); - } catch (EncapsulatedExceptions e) { - System.out.println("Failed to send (some) messages:"); - for (NetworkFailureException n : e.getNetworkExceptions()) { - System.out.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage()); - } - for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) { - System.out.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage()); - } - for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) { - System.out.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage()); + System.out.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); + System.out.println("Aborting sending."); + System.exit(1); } } + sendMessage(m, messageText, textSecureAttachments, recipients); break; case "receive": if (!m.isRegistered()) { @@ -186,47 +141,7 @@ public class Main { System.exit(1); } try { - m.receiveMessages(5, true, new Manager.ReceiveMessageHandler() { - @Override - public void handleMessage(TextSecureEnvelope envelope) { - System.out.println("Envelope from: " + envelope.getSource()); - System.out.println("Timestamp: " + envelope.getTimestamp()); - - if (envelope.isReceipt()) { - System.out.println("Got receipt."); - } else if (envelope.isWhisperMessage() | envelope.isPreKeyWhisperMessage()) { - TextSecureContent content = m.decryptMessage(envelope); - - if (content == null) { - System.out.println("Failed to decrypt message."); - } else { - if (content.getDataMessage().isPresent()) { - TextSecureDataMessage message = content.getDataMessage().get(); - System.out.println("Body: " + message.getBody().get()); - - if (message.isEndSession()) { - m.handleEndSession(envelope.getSource()); - } else if (message.getAttachments().isPresent()) { - System.out.println("Attachments: "); - for (TextSecureAttachment attachment : message.getAttachments().get()) { - System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); - if (attachment.isPointer()) { - System.out.println(" Id: " + attachment.asPointer().getId() + " Key length: " + attachment.asPointer().getKey().length + (attachment.asPointer().getRelay().isPresent() ? " Relay: " + attachment.asPointer().getRelay().get() : "")); - } - } - } - } - if (content.getSyncMessage().isPresent()) { - TextSecureSyncMessage syncMessage = content.getSyncMessage().get(); - System.out.println("Received sync message"); - } - } - } else { - System.out.println("Unknown message received."); - } - System.out.println(); - } - }); + m.receiveMessages(5, true, new ReceiveMessageHandler(m)); } catch (IOException e) { System.out.println("Error while receiving message: " + e.getMessage()); } @@ -235,4 +150,120 @@ public class Main { m.save(); System.exit(0); } + + private static Namespace parseArgs(String[] args) { + ArgumentParser parser = ArgumentParsers.newArgumentParser("textsecure-cli") + .defaultHelp(true) + .description("Commandline interface for TextSecure."); + Subparsers subparsers = parser.addSubparsers() + .title("subcommands") + .dest("command") + .description("valid subcommands") + .help("additional help"); + + Subparser parserRegister = subparsers.addParser("register"); + parserRegister.addArgument("-v", "--voice") + .help("The verification should be done over voice, not sms.") + .action(Arguments.storeTrue()); + + Subparser parserVerify = subparsers.addParser("verify"); + parserVerify.addArgument("verificationCode") + .help("The verification code you received via sms or voice call."); + + Subparser parserSend = subparsers.addParser("send"); + parserSend.addArgument("recipient") + .help("Specify the recipients' phone number.") + .nargs("*"); + parserSend.addArgument("-m", "--message") + .help("Specify the message, if missing standard input is used."); + parserSend.addArgument("-a", "--attachment") + .nargs("*") + .help("Add file as attachment"); + + Subparser parserReceive = subparsers.addParser("receive"); + parser.addArgument("-u", "--username") + .required(true) + .help("Specify your phone number, that will be used for verification."); + + try { + return parser.parseArgs(args); + } catch (ArgumentParserException e) { + parser.handleError(e); + return null; + } + } + + private static void sendMessage(Manager m, String messageText, List textSecureAttachments, + List recipients) { + final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); + if (textSecureAttachments != null) { + messageBuilder.withAttachments(textSecureAttachments); + } + TextSecureDataMessage message = messageBuilder.build(); + + try { + m.sendMessage(recipients, message); + } catch (IOException e) { + System.out.println("Failed to send message: " + e.getMessage()); + } catch (EncapsulatedExceptions e) { + System.out.println("Failed to send (some) messages:"); + for (NetworkFailureException n : e.getNetworkExceptions()) { + System.out.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage()); + } + for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) { + System.out.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage()); + } + for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) { + System.out.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage()); + } + } + } + + private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { + Manager m; + + public ReceiveMessageHandler(Manager m) { + this.m = m; + } + + @Override + public void handleMessage(TextSecureEnvelope envelope) { + System.out.println("Envelope from: " + envelope.getSource()); + System.out.println("Timestamp: " + envelope.getTimestamp()); + + if (envelope.isReceipt()) { + System.out.println("Got receipt."); + } else if (envelope.isWhisperMessage() | envelope.isPreKeyWhisperMessage()) { + TextSecureContent content = m.decryptMessage(envelope); + + if (content == null) { + System.out.println("Failed to decrypt message."); + } else { + if (content.getDataMessage().isPresent()) { + TextSecureDataMessage message = content.getDataMessage().get(); + System.out.println("Body: " + message.getBody().get()); + + if (message.isEndSession()) { + m.handleEndSession(envelope.getSource()); + } else if (message.getAttachments().isPresent()) { + System.out.println("Attachments: "); + for (TextSecureAttachment attachment : message.getAttachments().get()) { + System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); + if (attachment.isPointer()) { + System.out.println(" Id: " + attachment.asPointer().getId() + " Key length: " + attachment.asPointer().getKey().length + (attachment.asPointer().getRelay().isPresent() ? " Relay: " + attachment.asPointer().getRelay().get() : "")); + } + } + } + } + if (content.getSyncMessage().isPresent()) { + TextSecureSyncMessage syncMessage = content.getSyncMessage().get(); + System.out.println("Received sync message"); + } + } + } else { + System.out.println("Unknown message received."); + } + System.out.println(); + } + } } diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 2398f3bc..77e89556 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -35,9 +35,11 @@ import org.whispersystems.textsecure.api.TextSecureMessageReceiver; import org.whispersystems.textsecure.api.TextSecureMessageSender; import org.whispersystems.textsecure.api.crypto.TextSecureCipher; import org.whispersystems.textsecure.api.messages.TextSecureContent; +import org.whispersystems.textsecure.api.messages.TextSecureDataMessage; import org.whispersystems.textsecure.api.messages.TextSecureEnvelope; import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.api.push.TrustStore; +import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.textsecure.api.util.InvalidNumberException; import org.whispersystems.textsecure.api.util.PhoneNumberFormatter; @@ -68,7 +70,7 @@ public class Manager { this.username = username; } - private String getFileName() { + public String getFileName() { String path = settingsPath + "/data"; new File(path).mkdirs(); return path + "/" + username; @@ -218,9 +220,11 @@ public class Manager { accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); } - public TextSecureMessageSender getMessageSender() { - return new TextSecureMessageSender(URL, TRUST_STORE, username, password, + public void sendMessage(List recipients, TextSecureDataMessage message) + throws IOException, EncapsulatedExceptions { + TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password, axolotlStore, Optional.absent()); + messageSender.sendMessage(recipients, message); } public TextSecureContent decryptMessage(TextSecureEnvelope envelope) { From dbaf1c693cebe46a2c332a2f018bea7d01cdca01 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 8 Jul 2015 16:22:04 +0200 Subject: [PATCH 0003/2005] Catch AssertionError Fixes #2 --- src/main/java/cli/Main.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 2a0cfe46..3259cfb5 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -144,6 +144,12 @@ public class Main { m.receiveMessages(5, true, new ReceiveMessageHandler(m)); } catch (IOException e) { System.out.println("Error while receiving message: " + e.getMessage()); + System.exit(3); + } catch (AssertionError e) { + System.out.println("Failed to receive message (Assertion): " + e.getMessage()); + System.out.println(e.getStackTrace()); + System.out.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); + System.exit(1); } break; } @@ -216,6 +222,11 @@ public class Main { for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) { System.out.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage()); } + } catch (AssertionError e) { + System.out.println("Failed to send message (Assertion): " + e.getMessage()); + System.out.println(e.getStackTrace()); + System.out.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); + System.exit(1); } } From 02b66bd69bc62420ca365f04b143f0f0ae91704a Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 31 Jul 2015 12:53:45 +0200 Subject: [PATCH 0004/2005] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 51018 -> 52271 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index c97a8bdb9088d370da7e88784a7a093b971aa23a..30d399d8d2bf522ff5de94bf434a7cc43a9a74b5 100644 GIT binary patch delta 20977 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 PLk7ag1cA^t9sc(}j}eS1 delta 19544 zcmZ6yb8s(hvo##sc6O2-Ysa>2+ctLmi*4JsZQHhOJMaBG_o@2M`Tm&dRqL9o2Gup) zy}G9+6?8lr6hU4J6buFk2nq`5R>e&u4uKT@Kh{$QAx;1Y2uL-KUkMZY4Ex^k&Jh^s z|95WbFR=ganI!+$zR>^ICQt$IBL8P0M(Z&e0|W@j9t;RbI3W%UBcZpGA;Ay?A22)^ zm`b)p7K~^Li_7lRMdBHe2q@CCE(qi6B{azdF9(xgxZ}N?oMxJtY-sT0^YQsY$c-E! zo2N*vmH$(l+%JQAu_|uB6l8L>%~%(-LAGxXXN}uB5bJdep1p1o$~6QUEKF++9bD9o z8M@L{AVjotE0ja*%46;L<0~b10~qNGJan&TxNj~OtV5JUPJAB?xs-h#%I_{y zEjXs%1p`84#T!E`ka_YX-@Mg)NUPWCJ=P)Yq8Iqpi|B6UTh z?`tqXxy*MPvk4#QXhyCAw`l@2_IG+ovHG-cF#0bRj1vm>7xesG9eFLtGs6g(h)#9{&?~x)o=QLB83Z5Yag#1cHF!@_WY~+p$MQ z7P#U>)|ZIu_dBP>&Ftoaygx5*99e5@f1Mm)1(j zF$v**N>)p_H*8uQ7VjsP`LAvr)D+6(Q( z1t8}+X~_cROhkLEZUpTtwW+Ret%F6{9*N}E=PR}zht=6ZGBfQje97)$Z*NU*VeYZp zD09wPWV=Pi-fXOOH&^Vk&?vJ&#uMKxi%ZVqW8<-7A@pNsDrdnkd^+;t{eXY-+4ItE z*K#ctr)<(6MrlxHTRho`OPiv@>ndm^Na*Ic=`o?N^g8cU-Tv$ zyPbanapw7`Cg2hKt(o4VaB3HzEC3+a`q9n1NtxrD<)eC(1AaFP`X&{RdaM!9l=&fh z_#w#siT&n7f3NvfRl*jaEb~J<^&|cOs_~&0xBmn3D3sP#{)ciBm<|A43yFFOKX3Xh zae!Fg@25z2l-qA2a9+p;TA6=>USFpzN*&M{PK{Qr+$!0u25G^z0%n0(quRZrd_2w)*dFRQa{3!< zcSQHLNm8%{b7KPY^mKDWK*;S}Z>N7b+vk^kCj?3|KrYsSh_7Fc$6b{*$7!f(*lsl0 z969(%P>kMXN^D^T{ZWe{bq3rWMTx{RLuOcvVWP=ruDO7}kPN_xI>R@kwJ{ZK;e>df zzi{*tZIE*zPw?;}F4mI9jljp6+6fS{dR!Y*r?SYLd%xM4vF?usdExw{(b>b8~%N$cz~RW{S)F#r9yr|c!zf?%PJ_K}vLd0A_UeV>*6 zxLy+Zdtp64a|lphhb0UrtJ!XP;Vn9E+TK)}Dz-^*oyu6Olx&J?YABa*!8mY!L2bUW zrup4~cRwMmnPHdA%fn%T%~Tz}U1}iEq;d^iIl>81hiRsYDQI9z1{!i3-{lG0b~UiZ zKdFM9I3QJMqd7gsb!2|CL{mXP$K9;#VJ9u8?+2MG2?r-R5_uMAT0-O9#*iFOPpqw@ z*z#faGzLhdeUCg>-4rJqItJ=fvKyGet~wnAOwl| zBory|6iWqaiO>W^NirT4(vC*Zv-$>%DalXI12>i%4JkH|Qw@5R4RU~v+fjsHdYQdG z2HH%SeC@5`h;bnae$^8*fSXMgTG!4twf3?L8UMD^Rp+AVQqZ|iB6fy{zF+4a{|Zyq zo&g+V9BGruTBSJ23Ar2Fxm3#0@3fr*|8F8Z{k zFL4*~Hdg++x-!CJ)S#Scdk>ry7&j}W)6QE@)vXg(K6ler` z*$7r#iJIe2;cwH0ylmEf-3z1>H5~*W#R3M5Ovlp~b#lwjyszqIigI#ul+R!vJ0*LB zXUZQH-cuzZvQ*CooCQb|Zh$&)+cie>Y?JmA<0c-}>A!rEZVo}I?&)k* zt!k=7czgu}&v~eNQXq+3yH#Nj%ebXye!U&Vy8cP3jcU!7QIvk!KAS~2Q7gYa4h9?^ z+b=FA>;+ncIMCO!kRUIkIIWtVB{WkqY!F=MWc7s*z;Bqk!HiK?&R8b6TGMrscGy+p zi_nb#jQ5#~RS5PJkU!~?5bv=T?yO+qj9-#9a z6^}eMl-vZ5BnXt)%jZ)HM;GaMa*FT>7HrIuC^Pobw71~o(p1&dk0`TM-_WprJ>|&1 zHjcmC>5TRcNs-gti#TR>6gx&95St0&H1wLT&6nrvX7fYXiL?G5On5PCMh7H|C(*7o ze_vJnR&uII3m3u9C9Nr-x^5|8(2)Y6Fjc1;@ef5KHlVYX4mz1sex=dH^$$XOUM+dF z$5A@~_02OR+d!o#r6ONbWiPtN=B>D=DX6A2+gh)<=kUoHN4yQ7>d}*zS8Ckv<%~zc zcq0np%e{vL=RkZ2VPw@)B><@K(qFp^Q&j7mC&&4>Yorhkyh-iHAw~qmI_egzp!+Bv z#oXe<@D<-5dowX*JRPzgO5#$XXYK-cHMcp46j5BJ#>arM`)wsKtdGmd?M%} zP;5$7uR|3L7Z=He+&&!|bGnlt=KOuiEY&71bHbm1$)H{rxSZ&wfdFi~2v9frVJ+1Z z*|~3t)L0>$#?p~1VbJ-wiqW%+W0_F7>V5|$9tEl3A?yGCIACHm?BhEUZhuJCb`|4j z&Qw>bgEf_jYnX;*x5!+;Z{1bd^D8sH;8#HYdas0CP;_4kEuNOjvsmLQniPLFiRTHm zoI6GN#ujkcYCU?7VF!q}!T3K}LNgfvL4v>+pAYAIRNK-x_P7cH(>3E=-On6lW1vrv zk-}O8M{#SZ%N@rz6U_9aNBc`$Tb^S|j|JpcrH-Y@9A)#Mcx<6cFeTT!sEzpAn3k)l zAe$oDi*_4^&O74Kwc4#5iwN#*wN<~$Xh$k1X-W267_<65odR+n+%`OgkhGt{64fWW zYsYVal}LcVk21F7nx2id_e8@B!QB<3V@v?|<61}*>&Ln77}$^^R3jt+IRyfk@)%w& zMwh5oYq%1nL6OA~lIMf11kJLGw>PR-mp-6iyi{QoSk-BcA5WDf^fLfOK zK6huflV9?I7@Rn%2>tO09HyJ!WQvwPwx;a~T(O2-7(m-t;owUF7kp8Mb1Bq_T%bj9 zHpy8yT8tLQgo3<^9fm}Tm6O+S8uVmF!SD09p>=FAB>(VGXeiIS$tp+P(P^&c^7cmq z$qf)?Ev;)G@nUx8!_=KF$nvU6iVXY;;lDs&pyNEJuoudd4-qh+}lO0D)8N z1~kYq6X4~`3)EIY^aXilaM}a`AnYF%JHa1dqgTBX*A5SYW@b zqbz5%C>v6mg21K8(y`f~3D4!&z~q63MA0_}3V1~JfO{k$cUN@qIEJ1~9QCBX^4k%s z$-U7ne2N2K{{sdYNI%Ew%!b&~)xL1Cc2;q>WTGtP{_wT}mFWdT50IXfDVo0pLG0J}_SC73#!^tmDUBwb040fLxv3)6eVT zwN^(z();<@Ft zA$p_Y)avm8D=b(qNLmTupEN{f1aPF_l_dVDOyCv=kzMdIx!?(@MFn|cQF>@`@1FC- zoiT-zgb*Vp?(?7y|8@?PQMpKBUe{JdDpNGG78*8@H zzO0s$pOJxY-Bi5+V^14vR?;&3@%xQ?Ci_J^SN}p>HZt$ zGI-VNHVhbfR^!n?r(lnC4@lhvGH#BLwVS;us&{rZJMqnUx)rAOV=c)&roKG+?3=B9 zJXC9|cmc9;Rfo1Ve&Qhu+1NzyE)of1REIjBSD2+pg6bPPh|ckut^VUh1R8K}j3~@9 z#_1}^*20k;g1gDN+h`9zIRNaEoPGfY^O;;|+$yEUM=^DqIsw=W4W`o-90wl~ZIA%N zIhGgdRGS|FEZY{_wCrOH2NLP;J4My|8*FrO0J{ok$i{a7-+sw~{fxQNxL*+C9a29h zL_g-Cb_o12$5(_AyEOZ{7}K1fnJ4B<|8uP5kS+fhO765yxnx`ZD{=y$D zc81=&lR=BjqdvmKnx#%SlF46~*Ds|92iKCDZV{KYEvMjal~DpwE~nl7?(J)UBH+cS zrnNQMB-B*II#;hq&q>%imtFvP>=XBN<&5o@A;yP@VgZ?{9Sv(=OPPWLi3_HqWYX+ETRWdw4T@Q!L?{ zo5Di5(Y+6*@;5*VDWpfTCxv%2DO$HjNhKo3wO}C!edHz_lv7WLmRgXUet!o$i!ZmP zL`U5`C!Sgax~t0@jOlIo2ja_sfvAoB$_grMjnDD}Zh_;L{P`-|`Y zN;M6V^sxU^J!>!r!TeX9iSAFlCH_bG3BmvZqWZ5olhTU`$Wn)PLmff?hMdrK?}8w~ zwHByF|Fa89BQA(yEof*BX&q=|U3(FmTwOGz8rvA%xG$bitmaYjcj4GPoz-HYZW(q!k`&et|v4By)<2~czR6P4>O2D;X*7e^;Q z0=@DrG5V1spbd{+^%jL*_k5_z@tF~weuNsWQ&FU8vu3#B@V-K&6QV}NJ_9DF;gb`+ z%dJbeLkHona{R6=`rkcxm}}KrQTpq+3p>R+96MF}nsBns5<~Ru{M{<5Wy!=2`yT%m z_d8egi#r7Lo57C5n!gi!JdSQPm?fQV{6UzA;~ak5fRg>MSKn|rQma zH?vnR0AMcPi`g#AR}5+7VMxVpNm>wAM(1}i7|2pQG9S#Jd+27j<9GcnuV+V4 zVkpLp+~f+A*wh#e?dCBFg-8c0f;z0v$!qZ&@ic)!=$>i7!&+KS^oYCTw63^-(hr`9 zi32@T@;ojTnLB&Y%rw&2zJ&+>uj$95x&OU8VE+@Q(wak=s%uI|P8PXl*+h$fiWu=! z!mkKHWD!mmKMH$Ohckh>%|aw9Q5#u4zS!bPR6>iPfLsXzP)VD}gckwfIq9Y?CtO%R zzzXk2HDJ1r<#*_3GXyCrS4=~~n`x#3G@HOB3OAHlm+u?|E0rNvxQjPtr6^L%99N(R zSPviehrLjggS_|^v}H70R8enw#$^z=kStM_v9-z2ohrHBa3j1rI;K$}Mm#hQ&D=!8 z`mmq&n0j98AZ$THY)9)le_zQQ#BHf5ISN*{%pL(f07HOxM#z&^`-tl9CvgzPKyY;9 zMPn>c6jg6M9BDjWTsm<_A(>)?;D>q&KqIv%oOEWib}kV&I(6_vcVBx)WwMyiI99M> z#E%jYjjTn^s=GI4^~y`cY|7mDAw91sXhxF7s-YPxK#-_5fIGl(##@LDE+PU6M`l;^ zvU+9h77~ew63BSai*j;yN>9uvIa7q+8G1qU(i=d$74nPtiLsu&CHYhzkh@g^fb%Wf zI(_;Z)FN$oJ}y4;dW#Ow-vtKi?XBSKyF~|I^d4I&XNRVF1fHE<;;r1mZBo#_j0_czXJIW@on0%Bhc>j5l5XW3-kSME(G7N}>ndW9 z&yyD zvuFxcVMU6{Gsu>uVJ7*a`erfN;;E{?3};ashWUTtEhv>?%WWuO+!Z#fJl79$)b{wtjw)F*^k z)oLFkvLU&B5yYc;u zm+Jnhzi;jj%$rEPJ4NGgGPgxFS;?N#_PpD`4|im_t`R;dkI`wV`H%WkGVefQkR{c~ zU2WYdmala7TglX0Vv9yav^mGIqba9`wYsZiH&thP6{8rgqE?6naM&(p!KPZD0`8}C zqHqQ)ul~@k8F74H9N*k-!t>CCwxGDi$Yg`=T4-R|HXFB`-EpRbPPXsnfaqGj=xbHe z0>2>hE-bA3hOzND`8Ohpw&>cv=bo0no5{YaWGG7NpXl_r?TW5^i#6PE^fmyy}?Qi9+8Zoay4;sm;URm;hhfXbZ$l6}J-p>-E zGuzy3=s&8=!gf_xUx@Aym89#cvOSr$M}B5ks|?0i@gA$P%*)I^)ViBojR>l7=_?@b zXHdfUY;J%ATp5a?{jH22mp-XAI%-d;SUXL?ug=eb<=WX{%_JnvVP#f-2EK8<+w3|8 z#$uc*IK$QsrCkNqhyKLsqX0u#ZIGNs#F#w#QH6SQ-F-!?275#QOm3MHY#3l9+q)Tdgx&KQW7|G*muJUiUNO&53>VxVUc^pJy^Y~D}; zu(rb(^^Q&hM@phi<&*qad1EXUX|HHrQl#2VeB`tsShCQa)VIrC(W_M_2gKsn;m#S= z_fDqAWovdi+!3BK2!sdlKC_a($D7?N(^E&zp?AQY)R`iN-I--xyBeS-_xs%|N&DO3 z(lMO@8$meH7rO#XO&T#ptDzo^`tX_#(CZH1ta+IDP;-SJbxHIU5e{e@ufN;i*mfN^ zrG%t9@En{+47juO5U=;FtmB)@2eY6aR#1p{h8GejNWQE7xb+J?Qhuiu^9VH>psma; zf1KqOgbM7nW~FpWtUp0C5&fRp!8YnJW{~#ydcEmlWkGzMm?gVRaSmc$} zOI?9W!Jg9>vG*pG4ymmA+G-^wA)qN5!+)p^s){7djMEI*tcjQ9XJ}^il!WgWwfxd8 zx3=;{I^!1pn_Rt(!5+Z+ZS`VvZ!?{1gwbc%$^&ly01P3+Cidp?*cSQ>R#a`wsm7Fdkv1s`s+Kb}o3y7P0?WN2`;44( ztI-v5HCu$g;gy69w;jz%uFioyuCKoCh46vNeIs0g`doo^35z^yP7YgvkZq;JcLVsp z+2*yUTRRQ}5YQZ4LNpjoyd)NYh#@kZLbH;hsxg>iu&K(Q0z}hN7#zH**+o3tz&SN6 z0XlT+Z^!C-*Q$4us|YpnQ)YJdL5t6w(NFj{tj*2TZ+^kTk@x|v>#my@pTF)mQ-lD$ z5BOdj?d%}V%L7wBHlEE#2&8^sLIeo;a3Nl9JYPacH^)@5nAbasmzX-hAru=9oA;(L z=&9f&;#fjK1Cm$|A*U80>@GW;Dq><^;1C3Kb~Z&olrc75VJr5dQRoo)7uNFu8X_U$ zF7$R4tlWKh3CQ4`1!4Bxo(5qyDh1(Uq~<`l11o zLV|3DE$rmXlyw*V(P=WEZHJW%BW)h~=H@(Cq)4_WyHba?_P62Mo&DyrVK5xxnkF&P zm3>H>GxLCc2iewQmZ@L#u5|EQX84q&*2)tUMU`qKcN*@T%vu`GX~HvaRg8BCylf@FXr|KxZ{uP_DX zn7%=G$Us5ND?}loFe~15O4whD`lsaX-t!hGIK%LUXvA z)n0Z;>{We;{B<#8M%gQxUfCJS*YiIHb5c|tXq4u4sZ z=j=XHid*YFJ~__ukYMVpq}P0|RC<1>HNARx)J6~BF0z>#sbUd%A$EM!>dAgK3QTNT zM1h)wD)o#MD8#RbYfclY_>47!R`e#KPeBcxjjfEEG?c&1boPbw+-)Ixa}iGEL|%1= z;lk@~@ot1VVAA)ANz7+EIU!dha@073lByQ#GHn+9y#7)dO$xEuRR zA1})>NW3_EA{9z-o+Lj$@%9W^YB&1)2x08ufpZEvCG>Mqn_bB^TMAEltJuTd=mC1s~9*IXMO1}KvsfYrv zCwA61Ym!*F*MacUN1b{!4Pud~Qo2~k@B!mZ6t|c#yosz_BVFJH1}pJP{gmoBs=#nj zN5cn!_lO&9=9rX(os;DI6Z&)dof+d%SI}g+)R3rbR8hhRS)F1P`SM zZCywlD!-4Y6dxV?%v^K_S9AknQEdP~wUr04%f|*7L_q0RsQY5nL2U?gX%6u=M)+7` zlGh5s<&!7@={y4aq{r__iyY^vP}c^D>VAe*JzDwolg84<U776WK%|u@f%-?95~KFJM|MVUwcL2MJ!+ z!TylFoPPAIWh=N4WCI6=idDg91pNGG9^ikC12j;}LxB9ZyT1_>Le2pR1mppqz^6z7 zP_@v){ilf`CZ0a`2jkbDx_{#EzQ`r1t&x~)CQc;k5SS90xJio(r!B)jp2@58D=Q-bIbYYkPFc9Xx)`ZU&C%;l7sRPKDxq zd9a<8o6NvpW-qmY7v_IT!3!#%+=CD|p`i>apThkSnA~1*jbwW=nBof0=3 zexy-K;K5PQ#9C4X`S^ByZKv1rqqUfTv+46BQ$xbp<6D^7NwFNYydsRN^lI`&S~8m* z#(2I+ae1zb*Y#rA*xJL{;+jM_LKC$)lT?xaV`58{MFR!eU@={4%H*8C1#MI@54jyJ zenG9lMyI_tFIr&Eq?Y?ltG`ef;KSxR<#C1@N_LfF*gRug%dlNr8)+K$SgWITJxTMxLCIQt zY+RG}5`SN}Wqb0i=!vP)lJdjzX_=S6t3|mf-3e4mU4d2$%0Q-lSEtGo0DsG}f;n$;KOt^@d$c^5q7fV60MmdJKv=766RzuzEn}v4Rbsq!XFHBI*N(3LOTcpNwED=Y_%dmnAP>_tZaZKw^zC{bC5go_tWr9e3&j2W z@A9RRYJq#?q45Ox0;*F6K!fU3@{z3qJqv8fQaA zx<$dQe~fNHW1WsmgDB55$WFC4^wH>7m;6ytssF(4^Lo9Q-F7 zpPKzAFnZI^4=%IAD<&8-#aKb1aw=CA~M57R9Ipt^yXZ4d9I4XsU7 zQ(VmGiMia^cV}V`k?PxgSLbuAS0j@I?4Bps8$|hYtyj-}vRBVRXh;V1X3q{nx0hCR zFf@Dvf=XS*VWfA*VWk&G6~0nR-HSk!PDO*Eyn<@o26w_xCx87q^r#`Yo$was=5%v-nzYW~Yjrm?Ku<0gO|Pia;oMvE&^ ztA1@a&7KCjX8c(N95REsq1a_R#Z6vso$vT>{Pnu~#&VO3nj860z6Rax?f;MK|Sl<~_(7u|fXtKj0o-MrwJ?e!|ojL~;6PReTcm~Z-)fDPs`y~}71+Ye9hZkZj)jcoR7U_!FEQ|L6AG*{zU5jU zJcc!Pj+n9SF|*M2-_Z_U@ze>PZaVxYy;O;QeH&14a9g6VYJC~q0VV9NLfc`_e^zyc zZ~xHCf1Flhj!<50{TW$!Z3WnzKMqiSOGCijj`--n1OA406jf7Wv*F#@HEbZVGRMEo!4D zGUCjw4G^Hg{vDhp!9oz}GPDHaIvjxROfG~UvcA$(h{2IRBIHozAHo*Mjp2S+>(mRe!-xm{@=jzwloxMB(^Zse)$^<8rSN(W=6OiyC3i>&{SjeD z=*J#BMq5m8-n?A3h||`~E#U^GpQZAaNA)#H3s6R6k{5D7p=Pw{fK^plLa?kvctIgx zI3j+kF#pbcVV3DOIeVTk$Rsr;LVTv^5hdO&NTWv*U4uHr7PbpxawGptXTnC&A+m;( z{gOt(AW405s-zu)eIz2y6nxI)6pYMliEw~gc?Q+R7o<^`Ck0`-S+&xtRpW@+tK&m$ z0x)>Ill>!?BhWoVHcG?SxitUD#*)sB4B9fwh<;(@%}eN+c;IZ!H)8z9m7iCz zF5n(H8b2P9CE0vsTMSSfr0VBP#-2JIxS!`thKE?fWT`D<)|ls9%)>qD*A&|w;D^i7 z`tj+@1jj7}HGir<-;w&vTkI7pf%viOZ>Y&vcMcnJOZk0>ZFSTw_hD8Upvya;0ehvy zKBEzJmHsd){9&$TWN2YzRke48SNOyDhj}mIUXv1K z^PlNE_$i0pnSVCo0|NnJ0VVwD#{uxfOxX_pf(yLqlx%`R6?8?^_NY+4DpILQA|jih z;s^VEe!%vz@P2H^%~Y>5z@5EfUY44O%HRo_L!@Yfg-HTV0aP|9@uc%Z{A@rsg0;S3 z-uSnfk0Y<^Tt2Q@KT8GVGdBh&Mia=t1!j$OCHYT0wgo6JI-c2nn>5X0OrWk**P$u* zbkZt1+h@L_kSxS&?@&uD&C0{nC&B!C13kcFgcLKxhg~|c6l^3pzUDC99j$ghJEpEl zvF(K|XDd8&Vs zpH`%V3U4~V|KvU!p$2fG!}TJDmF)U~GzIK3nvlqvm#cjYyT1#ES8e8jqlV=3hzgJy zEE1|EQVONwZDn7Q=xj3{%dXme+P$oDzJq-(Qoh66Z@QRJP;e~Y{n}2pIW|7Bon}G- zpC>T@2z$7mrP#sx@EA)dgEml&M3qYOP>&2iIEIKzB_wXX+L^QdN>O@z4_^I zeK4K&*&rt#I=$<@(W@ICeVLm^!5TU9WgoeUm!uHPgoBr6Wlq>ZgQ2&Gc_$SO-%8lb z0P+Ss6r`T=ohDw56;^Kw=bed{$f=hK_qP09vA?b7Qq!g6)8KpMg?bW-Tyb&~3yB`U zsmjr3U7eQVG_^?*eV~e?*uSC5Zlu#@tu*mIJD@-DD)F9lgd%zB({OE^&w^d0!EIhw zgMn&@vcfPs0U7q-akJ?>Ri$CyLu_ezx1m@n=|f8n1VmEo7=49-EK|`Y==#+BZ^Bxl z3VJp~GoH->6|1QV&0p=lZs~-6>=kD~WKg{Z7H`cj$9cMRVvYQ?vMQR?X{P-w|G~sA z^TZk4Xwv6zgAg$WA*i6{R2ac!U;pjmOXEK)uCKCsNssPel%to4I{UTW8A4h%{*5Hp z@hb4MHV<+-OVueAY@tNpW2!jIY8T3Qh_i$po*wx$LAjQfqpHd@*XvK;*Hdr+NOS=} zw`s?*aM5d^4Rn4$1fel@#e24jTq(@8)wSi6N~|RlyF$abh*F|zQ+Ww(i6re&eyKH5 zXDsut`^S@;I~0iA0AsD<)~1U{i;6O7SF1!)EK? zn7#@4S}*2CE;kMy6V}NPZRH{$@A9+#F;V(uF_|d7v_-YdC*3-xs4E6 z;=&EfE^byPo-FDdg%5)>`q#>{zmSd?0V2g}!<%8hL!?VzRW#%s0lFSw;m*ZNXo$_l z3s%jgGs=!ye23Nq!^$l1^cA7IV&C)A*1AY%=*8s|h)>ymme8}{tk05e>%9-zjCS}0 zBYCZcbJ|dHB<9x0N47Z)Ec2(WF*;|r1P@Gzb9xsVpRhN?{6$dwIIHhMp;NczJl82*IE1W_3Xa}0?*Na>=@SLZ=@S5Z zCEQ$B_$w3nNUm`Pe_D0oN4t(h^ICMljAqVf9C(z7Nr<56@Aj^^h^;1Mqu6g9^UmBT z4lRDhkV!07LV2Oxl`w)^DWkaWyql2e5G9f2Y-sl=Cq5n0Fj%-f*uC}^ zio`9gc(m58CFQ*(Dm+eka7I|^;=G9xLo&@pS=?P_aNQOGPW&^xx zPTnB&MT18$c|H}-8VQ@dy{x?C;~A2eX_lxA756y6dDz5IVkS3E7%@USmkZpfbvNN1 zLoM$7PuAATVu$zB_7+ou$qYezxcTAwa26n_hNV>!rRubr_t-VG6+*p?sQ#Cr*;!)@ zbeQixL=mnI%ci&))nYjCgUkadHa2zmMrMmv{#<~RM=?}hAv;y`YncDJ;_At(hOjF~s-BT^@R z<;L6PZSU%`zXjbwi2o>U%(c4(Zj&|nbeTv)@=#7Uwv@ZXp0%sW{9SRm(|^IS3!oNW z)bC(dclswHPh&DOS+IlI+tch)z;yQ8f1ij5cWFM(hAz;X1rd`nUUzA_g!J*;t zKoEUUp(%%C*r}6&Y0yYPRFYMiDdp(k7rI5w)fCVS;Y}_z%gyUoE_L2DGcUR}JDyio z7Z+q-fRpXCNohK==aCrC>CA_$8?TEWuam>6m>e!yVW-!rUsr(KpOl%LqX*2lPnCim z@Uxr8I$2)MyafcZtn^ft=>>_D<$?r1f#-U;;x8O49LRf)(RSGa4!sf(R%U`cCFGdN z69w(qnSwTX-V$?e(GuKNie4j^^xgwzf|>c*Xlipc%mIt8*4rs>w+jPRgnB5Z}(vW8jH}cGUu7cFi3~6y|wjrj>K~k%(ZU`T;3t7Ky5Q`GPxRLtu)9SF$W7 z)PN>0N$DZHa!69mMZF^XycbwHNq`UoTrU1 zed10OPR+s@qk$JPrnp^KYreL}5sn?y?NGj2f^3yn*dr^1TY|b-!*t3$Mi#vhQ?Hf? z%4>nf2}U!>CRKtBP74!qLLDPc)4mHbQ(P^Iw2|6hvVl5tBSwPg=A|J{&Z*Y4$RQR4 zCYdrO)Mdc9d4D)r|Jbc8bxLSiuWnYGE$+f=ME*uWosNR1V-&WW`rUA0S|jszG(J81 zn0BB=?x^s#DoX6-nh zj@55$h5{QE)&P+T!@K74=GHZoy7F|AJu0ylmAz6E*bZ|_w`f+9xYg?STic2u224~v z5O~0%TXN4IrBy?h;VM(5H;@i~x7FEgF1!jwA>4ffxPT|h1=7TQ%d@q;z%~}czWwi^ zJ#+T9#4#ns_Uth&OshI!jnJjprMji?8dO2+CaPs?(dCT0+D})AuCeDogQ@S(?Q7X_ zS5oti9K>Tj;B5iY-Le|}%ss{j`qatCwnc!~7GrBDgtsYBzPdVC+}y+PttnNiUj|L! zq}a;TL}BLaomX(AwFmUYOd*ZKj{<9;bUxJU{*itiYQ&9(|Fl3SIzs6!YSx6EYFUpj zuNZ=ablo@^X-J5Zct_tw&i1TXkpE`2g}1dX$++v#I^TOO&z74}^RRN2w{t1Q9UTMU zQwrk~HCU|I4cwi^`5{33GQ}kqMIrAUJjQ;B+5*>5i&n&KR}H|Mva#wi5rlAcAybE& zIy=!>dpW*y%*G>N07nVXpsbxPo0Hb3k0iI^h3ujHf)_kSYI-e=&UZ9i6l6@b@kgu z)W^BE<0PxpRTqNgRs@dnXA0q;vyYlO$Xa4ClQaYd+jm1%`YkUQT$`pRTM4Y(W@Q)5 zN{nK(mF>AswpM4eJ>DI*Pj}E_hz8Qqjj8KlMhI7$9yzqcx&l)ZMngL|lEnk6w%g&Q z!Cn@PiLb&pd3M7Cljw{;wK_DlhR6>fZ5p+wMqR5Ed5v>!K;bY`!VD)iS>neGsGiE0 zIJ3gr*EX$5Q5H_7vZt5}jLnfKM1W&kJfC~V!RzlM_bTy}8w=HG&@kSnV>l!eg9gml zOIx96Ai8wKPowIp-Iy%d`|bdB0qT<|5D;4uf3cl?um7CFI#&qj6>_DL6}r`2OS-y} z8YoR%*0#*D8_d3NSj-Y#_lFR~&TIh<0)x*mpc^)>TlzEJz&NLM!E#=I;N(R*Id*kTY+_H4;?9Kr0n|xb0YZ+gi zyzm8c>*seb-XMy{iP)xST1{$Z4!}w)H{H(Sr;Z?YMpv-CP+&G_$dz| zW85#_rnd*5!pT%kcdcu*B|*b-#DA#5;orf(=66d5edQjI19AX+Ua^N?GYJ9bA{N^g0s`EOfW9{d&m37u5OjsM9KVKq5v>6kU@IF~Pz6yYlNYYK%X`FcT(UgE7p?`ip zBI&o^-}(ykv$!tl5rl^fm%Ha?uyeELJZ&;;earkX?Nh$9xRw~``?!3GzE4=_DAYZ6 zU&s>I#1Gg;x=e7J;}^+s65UXH!*dgqIAKEtQB*v##?$^tLZCYmGd4iOD$OUz98O8h zuH=TxDr5(srwzC5oHI0dV+~Eut_A1_c!HPocS1AN9`CJTX>Z1OdcPTs>NlTPz`RU9WxY{M*dT^(;GhigOCRV46;todBF~yG(iMPRmQA286 z<+Owa545)FWa3IY+sLrzX(`vwH*FFS&1L2FlhOgGKAG68S6WizrcDivpU9XzvDCgZ zOWxIJfNt}i8X6TB(j0-U$|PJ)qypCB2%v?TdNtsfF6$I!`GRQ`+rOCMvGwyT*$@$I!IZSPmzUazXg)AZ1p~OP#5bq#I3Kd0rUa4~^Gc6DS1Vaj z-oRc3+E3*vzS#e8Qjc7TCC9Bnnd3fMQ_uY)vFSc?9;F%jMaTqOkp8$>zC`LIn zkjYLO3|Buu)ZQwMrQLB_jsh zFMyTB&rKxYax!cczqcIl(Or}S_?vbVyykpNl+M7Zp_=V89aBuwFO(DfguR`Ah=?MNjzf$v{OoP0@ zr94g_Eqi1%z{1l6D}ox4eB9}ktfXpBwP~nSB4m{sb#sJZnu&7wWY~_NlJN71LQ zk&a%J!0}Yxh?9Lwhv_XtKD`0V)QG@Ls%~#t_({%>*#jM-N{Q)0Z8h~d<-O*vdzl33 z)Pqi*r*IY}OJIV21e_1Q(1SrJU5H?|ehoa}crEqi=cZP^3uppW-AX;CYZRC@EaE47 zq#tz#e+lq&^!qp#|gkMY&7Gn#IkqWmvu6+g%5YALz=7FQ5$Pob0<-|%scA}UG z_IRRLm1uP&PZlsV#ZuzJ02>z6E=o)~612`F8l$zU!Wy_dQdf^CL1$czQ9dx`FO-6W z3Xoa36fwD2F$J>O!d$JX``VHc*>XCY;#}?|SNk@LQ(4X&H$~L15bFamv${TTJu&mV z+GjO;Y8HGwLMwM6&OCshXxn|hS=vvv_9VXIjfdYKq|ctQ`dg#HHS;UCT>-_jMvv?s zh$L&q(gDW7Jg^$pobVkoMdYUv^BjJO02pfjSj&oZc>SH+1?r-eGmBY^*gjocU(=qD zrtHBRO`qs&ff=!*PdXY0WksHujB%kyf?%&!=3Bd1t&!NGg;zkzqS#E|@fT&Ng7S=+ zNkx=BeJ&tV*O}jpK=k)P6C-^yB`y#MZp*bl5+swol+?j8@Lvq~0QF5!jF)evLA zm}#G`cr`Q_jP2um|NDLL_`#B{TvN@c+MzcRrN&`~)O8@YgaT$eNLOHH8dZdd>3SA6Jp(xjktz>8MxOF3?UCfLr zDi!LiD5R0nMXhDDGBaj!$zaQ1+e%NQlDqshQH1|DXQu4Q@ys(b?|I+%_xrx{%^A=6 zo%zuT?iZD+{lucynxzhAdZ!w0mDu7M@FkB!o`>4DM;@b^e=Wh$s^n=L3NA+dV{#fL zcJruRNu!*ZCF>ybk^8j>#jhX1|KG}~S$*1P%~@L;!wl<)$$2@wJCpNpo2R2jXnWgz z$Ao%~rw+FMweQR^|KmoY-#dqkLh;jKIEN@Y?oLwCh2Xn=c)&ghd;5*Cdb`ME4HFI>xx`UCBvbxJTB<~W_OsUF+eNa3oT_pi{A>+rHF9B-i~ z8?y4xF*1%9c*pAWebrRiR@u;cxUelXf$90QK0L5ot&8Yx0dosi^Fnw5Eb!&i9r@9B z#eB=LDyQw+=U?^iw;dC?n6XPp*PBgm`pAo3?m2er?(ddoxD5k`*o&@IXizJ<`(l;$ zrU^7N=8=+|-MW0->=w=ADpIMW{ny;+ZGmpS4W*ODPoqzC@Qnqu6srlSYjpPsQIPr? zqqg0}Z7&(M2M^k~C^qce>-u&5RCXsmON(pZR5x;w zp&h93^TiC+Up~0@?H~EKWO7Kn|2IO{7u4dv8(zp?KV-bfS&OD zrPu4C|Al1gxZ=+3XH0%*iC?^1IW5wIxjs{;H!+{a$WR+?5<5?5TrjR}ZcQg9oqRv~ zDr3xlbW_=)-lfYQFNt`vIjZSP8#~ug?q+-Zlzy;nx?QV%hwHBXQRd)3j(jKOb-iC{ zl`2ox4Dj5Wf=uXL?K?vg**0ZXEZrS+s?Xb;*i%gHwI6fr{DjnJB(hpY_sNlB8_1$; z$^I>dZlw6M+I!dfC{Q|`AWDF)e)05U&LlSuJ!3N+{5k^WM=bX`FtuVzFIJiLf#HzW z7(_L=)j=q^E2xW^P#Y1-#dRHaHSJw75p=DbHSNq8)R;IYS2|R3HawBbt$IWM?BiMf zmvBSh#`c}w@A7uP^%eFXjy&(>SZ=F*f8D&-QT8rFJS~@GldV#w4c&L$h=NINxq5e-bAs{oH7V2b zZZoHJMETSlzNk)Yr=>wJB4=~xu*-u>mkfh*7Z0*}qW>wpny9kvtVw_NV`oJrb`OhB z*~lRrt=hAZvxrcwr2TAfZc1C5h-R-kRBsT@*PYJH6|PV+_MLvY&ftzHHLSL}qdum8 zu>b7g7S|aQl|Mhz`{FFlkRNQRoP5*C8P2ui=FOx}zb?%_`RD4vPxa#KH|e_`t8ZVM zd{peEA23Yz*xS6?&p&&uOGB91DEl^1G4({`J7I!&y71_P(og1#*QHh-3S9W`i8Du! za{mB3>snyi*nD=pXjmb=`6|!nOEh8VV}Gnodz6)f?huWgza`%M{TUH$!O)TLKgQp; zsD?YLP3X0B$X9;SFN~XdNQb;szuydlUb4E8J0hzkUC!^6y-F-7mzP$HY>V)+3X=Wp z53;?MclXFHa;4vMlJVL~n`PJywy%r_Zi$dnmBeU$aedeYl<_trxs~hZl>W-BZ78#) zKXD}yZNY))a#my8#2`O$Y`i5sIe-%cTE3tgX!|1D)S;=3x#0D|m@rE(cN|_zZnou& zgz`FsrmTcwuXrn^m}(V_CoVuS zhbmJv3GYOMh;A@^T`){5OcH;qqr^*9%Zw!1K8YDf)dqFG1E7tGDTX+MbXDmf{KX4b z{+QTo+Y<;f#%KZzQ1h9s1SFrYiDvRj;BmSZWRf`#g3K}Gl$C(IK{^C=0gTUFg^G79 zARul67!_lFB8Cwg+N0zwfeA{Sz2g^xNEqTN7nJxxpeK#A;315Esn?m&qGt`hRIkK&w(z?QFPmF%Pte^AU`Wh`0W*fRBhNuT^d(b^RKC?UJ5VaExI zbb*G&P(${GS_bcnz^qk?S%y%2(H<;TUj+zSHR}Zy*RlX4XwPa)iV}f!P-yxMK7H_vc4X0z?Dc$Kmxz z5t*zZN%h297@tN3dVS<|=py7$t<0AJ@Fl^hFTd7I${hfhOB9MpJgn;UrMOoej1ND6 z;^}qEr5F!jH=5r;rhn~{p+vDABkoN=aEd@1jIcI zr006VXoJIV}NINQbaN!k&y0sO=-%L^)Mcn zjifi3A&x>_DYi!l<3C-NrK^argo|av1NieiaQt+U!h$7pjv Date: Fri, 31 Jul 2015 12:54:10 +0200 Subject: [PATCH 0005/2005] Update README installApp is now deprecated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 552eac41..32484b40 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ dependencies. 3. Create shell wrapper in *build/install/textsecure-cli/bin*: - ./gradlew installApp + ./gradlew installDist 4. Create tar file in *build/distributions*: From df8a19c06053a60f31b678cff995a3ac03629de4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 31 Jul 2015 12:54:37 +0200 Subject: [PATCH 0006/2005] Update dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e8ccffd5..b7250a83 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ repositories { } dependencies { - compile 'org.whispersystems:textsecure-java:1.6.1' + compile 'org.whispersystems:textsecure-java:1.6.2' compile 'com.madgag.spongycastle:prov:1.52.0.0' compile 'org.json:json:20141113' compile 'commons-io:commons-io:2.4' From 1cb8c78bf4d0409f2479655585d5cca3f4e13694 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 1 Aug 2015 13:56:19 +0200 Subject: [PATCH 0007/2005] Update README.md fix typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 32484b40..dcd69a0c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # textsecure-cli textsecure-cli is a commandline interface for [libtextsecure-java](https://github.com/WhisperSystems/libtextsecure-java). It supports registering, verifying, sending and receiving messages. However receiving messages currently doesn't work, because libtextsecure-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libtextsecure-java/pull/5). For registering you need a phone number where you can receive SMS or incoming calls. -It is primarily intented to be used on servers to notify admins of important events. +It is primarily intended to be used on servers to notify admins of important events. ## Usage @@ -38,7 +38,7 @@ The password and cryptographic keys are created when registering and stored in t This project uses [Gradle](http://gradle.org) for building and maintaining dependencies. -1. Checkout the source somewhere on your filesystem wit +1. Checkout the source somewhere on your filesystem with git clone https://github.com/AsamK/textsecure-cli.git From f06672db894287252459e65df21549a295e7e21c Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 7 Aug 2015 12:41:14 +0200 Subject: [PATCH 0008/2005] Use gradle-2.5-all needed by IDE --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c73fbfd3..9f997ac6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jul 31 12:47:28 CEST 2015 +#Fri Aug 07 11:43:01 CEST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip From e1b584ab84373455ccd95b0cc9224c926aef9343 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 7 Aug 2015 12:55:33 +0200 Subject: [PATCH 0009/2005] Implement downloading attachments --- src/main/java/cli/Main.java | 7 ++++++ src/main/java/cli/Manager.java | 44 +++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 3259cfb5..cc749591 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -20,6 +20,7 @@ import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; import org.apache.commons.io.IOUtils; +import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException; import org.whispersystems.textsecure.api.messages.*; import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage; @@ -262,6 +263,12 @@ public class Main { System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); if (attachment.isPointer()) { System.out.println(" Id: " + attachment.asPointer().getId() + " Key length: " + attachment.asPointer().getKey().length + (attachment.asPointer().getRelay().isPresent() ? " Relay: " + attachment.asPointer().getRelay().get() : "")); + try { + File file = m.retrieveAttachment(attachment.asPointer()); + System.out.println(" Stored plaintext in: " + file); + } catch (IOException | InvalidMessageException e) { + System.out.println("Failed to retrieve attachment: " + e.getMessage()); + } } } } diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 77e89556..49043832 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -18,10 +18,7 @@ package cli; import org.apache.commons.io.IOUtils; import org.json.JSONObject; -import org.whispersystems.libaxolotl.IdentityKeyPair; -import org.whispersystems.libaxolotl.InvalidKeyException; -import org.whispersystems.libaxolotl.InvalidKeyIdException; -import org.whispersystems.libaxolotl.InvalidVersionException; +import org.whispersystems.libaxolotl.*; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.state.PreKeyRecord; @@ -34,6 +31,7 @@ import org.whispersystems.textsecure.api.TextSecureMessagePipe; import org.whispersystems.textsecure.api.TextSecureMessageReceiver; import org.whispersystems.textsecure.api.TextSecureMessageSender; import org.whispersystems.textsecure.api.crypto.TextSecureCipher; +import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer; import org.whispersystems.textsecure.api.messages.TextSecureContent; import org.whispersystems.textsecure.api.messages.TextSecureDataMessage; import org.whispersystems.textsecure.api.messages.TextSecureEnvelope; @@ -54,6 +52,8 @@ public class Manager { private final static TrustStore TRUST_STORE = new WhisperTrustStore(); private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure"; + private final static String dataPath = settingsPath + "/data"; + private final static String attachmentsPath = settingsPath + "/attachments"; private String username; private String password; @@ -71,9 +71,8 @@ public class Manager { } public String getFileName() { - String path = settingsPath + "/data"; - new File(path).mkdirs(); - return path + "/" + username; + new File(dataPath).mkdirs(); + return dataPath + "/" + username; } public boolean userExists() { @@ -247,7 +246,7 @@ public class Manager { } public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { - TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey); + final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey); TextSecureMessagePipe messagePipe = null; try { @@ -272,6 +271,35 @@ public class Manager { } } + public File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException { + final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey); + + File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); + InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); + + new File(attachmentsPath).mkdirs(); + File outputFile = new File(attachmentsPath + "/" + pointer.getId()); + OutputStream output = null; + try { + output = new FileOutputStream(outputFile); + byte[] buffer = new byte[4096]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } finally { + if (output != null) { + output.close(); + } + tmpFile.delete(); + } + return outputFile; + } + public String canonicalizeNumber(String number) throws InvalidNumberException { String localNumber = username; return PhoneNumberFormatter.formatNumber(number, localNumber); From 9e1cd7e3988aff9db1ac72ec5f597dc9a6c0579a Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 7 Aug 2015 13:22:15 +0200 Subject: [PATCH 0010/2005] Fix coding issues --- src/main/java/cli/JsonAxolotlStore.java | 2 +- src/main/java/cli/JsonIdentityKeyStore.java | 2 +- src/main/java/cli/JsonPreKeyStore.java | 4 ++-- src/main/java/cli/JsonSessionStore.java | 6 +++--- src/main/java/cli/JsonSignedPreKeyStore.java | 4 ++-- src/main/java/cli/Main.java | 2 +- src/main/java/cli/Manager.java | 14 +++++--------- src/main/java/cli/Util.java | 6 +++--- src/main/java/cli/WhisperTrustStore.java | 2 +- 9 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/main/java/cli/JsonAxolotlStore.java b/src/main/java/cli/JsonAxolotlStore.java index d260b6b0..02b9cdf1 100644 --- a/src/main/java/cli/JsonAxolotlStore.java +++ b/src/main/java/cli/JsonAxolotlStore.java @@ -10,7 +10,7 @@ import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import java.io.IOException; import java.util.List; -public class JsonAxolotlStore implements AxolotlStore { +class JsonAxolotlStore implements AxolotlStore { private final JsonPreKeyStore preKeyStore; private final JsonSessionStore sessionStore; private final JsonSignedPreKeyStore signedPreKeyStore; diff --git a/src/main/java/cli/JsonIdentityKeyStore.java b/src/main/java/cli/JsonIdentityKeyStore.java index 75e6ba06..827bf055 100644 --- a/src/main/java/cli/JsonIdentityKeyStore.java +++ b/src/main/java/cli/JsonIdentityKeyStore.java @@ -11,7 +11,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -public class JsonIdentityKeyStore implements IdentityKeyStore { +class JsonIdentityKeyStore implements IdentityKeyStore { private final Map trustedKeys = new HashMap<>(); diff --git a/src/main/java/cli/JsonPreKeyStore.java b/src/main/java/cli/JsonPreKeyStore.java index 63e7ea77..a0133ec8 100644 --- a/src/main/java/cli/JsonPreKeyStore.java +++ b/src/main/java/cli/JsonPreKeyStore.java @@ -10,7 +10,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; -public class JsonPreKeyStore implements PreKeyStore { +class JsonPreKeyStore implements PreKeyStore { private final Map store = new HashMap<>(); @@ -18,7 +18,7 @@ public class JsonPreKeyStore implements PreKeyStore { } - public JsonPreKeyStore(JSONArray list) throws IOException { + public JsonPreKeyStore(JSONArray list) { for (int i = 0; i < list.length(); i++) { JSONObject k = list.getJSONObject(i); try { diff --git a/src/main/java/cli/JsonSessionStore.java b/src/main/java/cli/JsonSessionStore.java index c034fc2c..d2fe0a4a 100644 --- a/src/main/java/cli/JsonSessionStore.java +++ b/src/main/java/cli/JsonSessionStore.java @@ -9,15 +9,15 @@ import org.whispersystems.libaxolotl.state.SessionStore; import java.io.IOException; import java.util.*; -public class JsonSessionStore implements SessionStore { +class JsonSessionStore implements SessionStore { - private Map sessions = new HashMap<>(); + private final Map sessions = new HashMap<>(); public JsonSessionStore() { } - public JsonSessionStore(JSONArray list) throws IOException { + public JsonSessionStore(JSONArray list) { for (int i = 0; i < list.length(); i++) { JSONObject k = list.getJSONObject(i); try { diff --git a/src/main/java/cli/JsonSignedPreKeyStore.java b/src/main/java/cli/JsonSignedPreKeyStore.java index 5b24c8dc..f992d2d6 100644 --- a/src/main/java/cli/JsonSignedPreKeyStore.java +++ b/src/main/java/cli/JsonSignedPreKeyStore.java @@ -12,7 +12,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -public class JsonSignedPreKeyStore implements SignedPreKeyStore { +class JsonSignedPreKeyStore implements SignedPreKeyStore { private final Map store = new HashMap<>(); @@ -20,7 +20,7 @@ public class JsonSignedPreKeyStore implements SignedPreKeyStore { } - public JsonSignedPreKeyStore(JSONArray list) throws IOException { + public JsonSignedPreKeyStore(JSONArray list) { for (int i = 0; i < list.length(); i++) { JSONObject k = list.getJSONObject(i); try { diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index cc749591..319ca42a 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -232,7 +232,7 @@ public class Main { } private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { - Manager m; + final Manager m; public ReceiveMessageHandler(Manager m) { this.m = m; diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 49043832..5650921d 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -47,7 +47,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -public class Manager { +class Manager { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); @@ -64,7 +64,7 @@ public class Manager { private boolean registered = false; private JsonAxolotlStore axolotlStore; - TextSecureAccountManager accountManager; + private TextSecureAccountManager accountManager; public Manager(String username) { this.username = username; @@ -77,10 +77,7 @@ public class Manager { public boolean userExists() { File f = new File(getFileName()); - if (!f.exists() || f.isDirectory()) { - return false; - } - return true; + return !(!f.exists() || f.isDirectory()); } public boolean userHasKeys() { @@ -124,7 +121,6 @@ public class Manager { writer.close(); } catch (Exception e) { System.out.println("Saving file error: " + e.getMessage()); - return; } } @@ -300,12 +296,12 @@ public class Manager { return outputFile; } - public String canonicalizeNumber(String number) throws InvalidNumberException { + private String canonicalizeNumber(String number) throws InvalidNumberException { String localNumber = username; return PhoneNumberFormatter.formatNumber(number, localNumber); } - protected TextSecureAddress getPushAddress(String number) throws InvalidNumberException { + TextSecureAddress getPushAddress(String number) throws InvalidNumberException { String e164number = canonicalizeNumber(number); return new TextSecureAddress(e164number); } diff --git a/src/main/java/cli/Util.java b/src/main/java/cli/Util.java index 36921de2..907c4ed1 100644 --- a/src/main/java/cli/Util.java +++ b/src/main/java/cli/Util.java @@ -3,19 +3,19 @@ package cli; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -public class Util { +class Util { public static String getSecret(int size) { byte[] secret = getSecretBytes(size); return Base64.encodeBytes(secret); } - public static byte[] getSecretBytes(int size) { + private static byte[] getSecretBytes(int size) { byte[] secret = new byte[size]; getSecureRandom().nextBytes(secret); return secret; } - public static SecureRandom getSecureRandom() { + private static SecureRandom getSecureRandom() { try { return SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException e) { diff --git a/src/main/java/cli/WhisperTrustStore.java b/src/main/java/cli/WhisperTrustStore.java index 621b43e9..08519653 100644 --- a/src/main/java/cli/WhisperTrustStore.java +++ b/src/main/java/cli/WhisperTrustStore.java @@ -4,7 +4,7 @@ import org.whispersystems.textsecure.api.push.TrustStore; import java.io.InputStream; -public class WhisperTrustStore implements TrustStore { +class WhisperTrustStore implements TrustStore { @Override public InputStream getKeyStoreInputStream() { From 0e4fe8bc8fc664386d26fd90c28c7ea1652a991e Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 15 Sep 2015 12:48:04 +0200 Subject: [PATCH 0011/2005] Update gradle 2.7 --- gradle/wrapper/gradle-wrapper.jar | Bin 52271 -> 53638 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 30d399d8d2bf522ff5de94bf434a7cc43a9a74b5..e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2 100644 GIT binary patch delta 12445 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 Y#xNH6N;@d4i4=r8fA@elj|0oiiFiU0rr delta 11032 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

uuTaW!v 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 Z_edo3_@?-WFr*3mdXf_!sre83{{U7N0Dk}g diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9f997ac6..327f6b58 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Aug 07 11:43:01 CEST 2015 +#Tue Sep 15 12:33:25 CEST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip diff --git a/gradlew b/gradlew index 91a7e269..97fac783 100755 --- a/gradlew +++ b/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` From 2796fff56d74639d8c1c5296a42fea815979f41f Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 15 Sep 2015 13:03:49 +0200 Subject: [PATCH 0012/2005] Update textescure-java to 1.7 Adapt code: - Add USER_AGENT - verifyAccount renamed to verifyAccountWithCode --- build.gradle | 2 +- src/main/java/cli/Manager.java | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index b7250a83..65774e8c 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ repositories { } dependencies { - compile 'org.whispersystems:textsecure-java:1.6.2' + compile 'org.whispersystems:textsecure-java:1.7.0' compile 'com.madgag.spongycastle:prov:1.52.0.0' compile 'org.json:json:20141113' compile 'commons-io:commons-io:2.4' diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 5650921d..18224897 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -51,6 +51,8 @@ class Manager { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); + private final static String USER_AGENT = "textsecure-cli"; + private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure"; private final static String dataPath = settingsPath + "/data"; private final static String attachmentsPath = settingsPath + "/attachments"; @@ -103,7 +105,7 @@ class Manager { } axolotlStore = new JsonAxolotlStore(in.getJSONObject("axolotlStore")); registered = in.getBoolean("registered"); - accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password); + accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); } public void save() { @@ -138,7 +140,7 @@ class Manager { public void register(boolean voiceVerication) throws IOException { password = Util.getSecret(18); - accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password); + accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); if (voiceVerication) accountManager.requestVoiceVerificationCode(); @@ -201,7 +203,7 @@ class Manager { public void verifyAccount(String verificationCode) throws IOException { verificationCode = verificationCode.replace("-", ""); signalingKey = Util.getSecret(52); - accountManager.verifyAccount(verificationCode, signalingKey, false, axolotlStore.getLocalRegistrationId()); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId()); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; @@ -218,7 +220,7 @@ class Manager { public void sendMessage(List recipients, TextSecureDataMessage message) throws IOException, EncapsulatedExceptions { TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password, - axolotlStore, Optional.absent()); + axolotlStore, USER_AGENT, Optional.absent()); messageSender.sendMessage(recipients, message); } @@ -242,7 +244,7 @@ class Manager { } public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { - final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey); + final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); TextSecureMessagePipe messagePipe = null; try { @@ -268,7 +270,7 @@ class Manager { } public File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException { - final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey); + final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); From 76a1e8ec2f3ef857285ed9df775cf1a174557f8b Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 15 Sep 2015 13:24:27 +0200 Subject: [PATCH 0013/2005] Use System.err for error messages --- src/main/java/cli/Main.java | 48 +++++++++++++++++----------------- src/main/java/cli/Manager.java | 4 +-- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 319ca42a..31979d8c 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -57,7 +57,7 @@ public class Main { try { m.load(); } catch (Exception e) { - System.out.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage()); + System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage()); System.exit(2); } } @@ -70,29 +70,29 @@ public class Main { try { m.register(ns.getBoolean("voice")); } catch (IOException e) { - System.out.println("Request verify error: " + e.getMessage()); + System.err.println("Request verify error: " + e.getMessage()); System.exit(3); } break; case "verify": if (!m.userHasKeys()) { - System.out.println("User has no keys, first call register."); + System.err.println("User has no keys, first call register."); System.exit(1); } if (m.isRegistered()) { - System.out.println("User registration is already verified"); + System.err.println("User registration is already verified"); System.exit(1); } try { m.verifyAccount(ns.getString("verificationCode")); } catch (IOException e) { - System.out.println("Verify error: " + e.getMessage()); + System.err.println("Verify error: " + e.getMessage()); System.exit(3); } break; case "send": if (!m.isRegistered()) { - System.out.println("User is not registered."); + System.err.println("User is not registered."); System.exit(1); } String messageText = ns.getString("message"); @@ -100,7 +100,7 @@ public class Main { try { messageText = IOUtils.toString(System.in); } catch (IOException e) { - System.out.println("Failed to read message from stdin: " + e.getMessage()); + System.err.println("Failed to read message from stdin: " + e.getMessage()); System.exit(1); } } @@ -117,8 +117,8 @@ public class Main { String mime = Files.probeContentType(Paths.get(attachment)); textSecureAttachments.add(new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null)); } catch (IOException e) { - System.out.println("Failed to add attachment \"" + attachment + "\": " + e.getMessage()); - System.out.println("Aborting sending."); + System.err.println("Failed to add attachment \"" + attachment + "\": " + e.getMessage()); + System.err.println("Aborting sending."); System.exit(1); } } @@ -129,8 +129,8 @@ public class Main { try { recipients.add(m.getPushAddress(recipient)); } catch (InvalidNumberException e) { - System.out.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.out.println("Aborting sending."); + System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); + System.err.println("Aborting sending."); System.exit(1); } } @@ -138,18 +138,18 @@ public class Main { break; case "receive": if (!m.isRegistered()) { - System.out.println("User is not registered."); + System.err.println("User is not registered."); System.exit(1); } try { m.receiveMessages(5, true, new ReceiveMessageHandler(m)); } catch (IOException e) { - System.out.println("Error while receiving message: " + e.getMessage()); + System.err.println("Error while receiving message: " + e.getMessage()); System.exit(3); } catch (AssertionError e) { - System.out.println("Failed to receive message (Assertion): " + e.getMessage()); - System.out.println(e.getStackTrace()); - System.out.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); + System.err.println("Failed to receive message (Assertion): " + e.getMessage()); + System.err.println(e.getStackTrace()); + System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); System.exit(1); } break; @@ -211,22 +211,22 @@ public class Main { try { m.sendMessage(recipients, message); } catch (IOException e) { - System.out.println("Failed to send message: " + e.getMessage()); + System.err.println("Failed to send message: " + e.getMessage()); } catch (EncapsulatedExceptions e) { - System.out.println("Failed to send (some) messages:"); + System.err.println("Failed to send (some) messages:"); for (NetworkFailureException n : e.getNetworkExceptions()) { - System.out.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage()); + System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage()); } for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) { - System.out.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage()); + System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage()); } for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) { - System.out.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage()); + System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage()); } } catch (AssertionError e) { - System.out.println("Failed to send message (Assertion): " + e.getMessage()); - System.out.println(e.getStackTrace()); - System.out.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); + System.err.println("Failed to send message (Assertion): " + e.getMessage()); + System.err.println(e.getStackTrace()); + System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); System.exit(1); } } diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 18224897..3a56377e 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -122,7 +122,7 @@ class Manager { writer.flush(); writer.close(); } catch (Exception e) { - System.out.println("Saving file error: " + e.getMessage()); + System.err.println("Saving file error: " + e.getMessage()); } } @@ -259,7 +259,7 @@ class Manager { if (returnOnTimeout) return; } catch (InvalidVersionException e) { - System.out.println("Ignoring error: " + e.getMessage()); + System.err.println("Ignoring error: " + e.getMessage()); } save(); } From 117f839547a064ccc2253e9e682bcd6b3dd6b7c9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 15 Sep 2015 13:26:02 +0200 Subject: [PATCH 0014/2005] Use generic type inference from java 8 --- src/main/java/cli/Main.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 31979d8c..f80a74b6 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -105,10 +105,10 @@ public class Main { } } - final List attachments = ns.getList("attachment"); + final List attachments = ns.getList("attachment"); List textSecureAttachments = null; if (attachments != null) { - textSecureAttachments = new ArrayList(attachments.size()); + textSecureAttachments = new ArrayList<>(attachments.size()); for (String attachment : attachments) { try { File attachmentFile = new File(attachment); From a6f65ae3a066ce4346b912b01ffce6cde3d76766 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 15 Sep 2015 13:26:36 +0200 Subject: [PATCH 0015/2005] =?UTF-8?q?Print=20error=20if=20temporary=20atta?= =?UTF-8?q?chment=20file=20can=E2=80=99t=20be=20deleted?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/cli/Manager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 3a56377e..10291f20 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -293,7 +293,9 @@ class Manager { if (output != null) { output.close(); } - tmpFile.delete(); + if (!tmpFile.delete()) { + System.err.println("Failed to delete temp file: " + tmpFile); + } } return outputFile; } From b91abad2b53d74ba5549c95f09835b2f426912ea Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 22 Sep 2015 12:45:11 +0200 Subject: [PATCH 0016/2005] Update textsecure-java to 1.8.0 --- build.gradle | 2 +- src/main/java/cli/Manager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 65774e8c..2e3252fa 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ repositories { } dependencies { - compile 'org.whispersystems:textsecure-java:1.7.0' + compile 'org.whispersystems:textsecure-java:1.8.0' compile 'com.madgag.spongycastle:prov:1.52.0.0' compile 'org.json:json:20141113' compile 'commons-io:commons-io:2.4' diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 10291f20..3f9237fb 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -203,7 +203,7 @@ class Manager { public void verifyAccount(String verificationCode) throws IOException { verificationCode = verificationCode.replace("-", ""); signalingKey = Util.getSecret(52); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId()); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId(), false); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; From cd8de7878cb52d872a0400e34f23d45310170400 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 22 Sep 2015 13:23:16 +0200 Subject: [PATCH 0017/2005] Make use of attachment size and preview --- src/main/java/cli/Main.java | 6 ++++-- src/main/java/cli/Manager.java | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index f80a74b6..b1822aaf 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -262,9 +262,11 @@ public class Main { for (TextSecureAttachment attachment : message.getAttachments().get()) { System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); if (attachment.isPointer()) { - System.out.println(" Id: " + attachment.asPointer().getId() + " Key length: " + attachment.asPointer().getKey().length + (attachment.asPointer().getRelay().isPresent() ? " Relay: " + attachment.asPointer().getRelay().get() : "")); + final TextSecureAttachmentPointer pointer = attachment.asPointer(); + System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); + System.out.println((pointer.getSize().isPresent() ? " Size: " + pointer.getSize().get() : " bytes") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); try { - File file = m.retrieveAttachment(attachment.asPointer()); + File file = m.retrieveAttachment(pointer); System.out.println(" Stored plaintext in: " + file); } catch (IOException | InvalidMessageException e) { System.out.println("Failed to retrieve attachment: " + e.getMessage()); diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 3f9237fb..fef7c81d 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -292,11 +292,27 @@ class Manager { } finally { if (output != null) { output.close(); + output = null; } if (!tmpFile.delete()) { System.err.println("Failed to delete temp file: " + tmpFile); } } + if (pointer.getPreview().isPresent()) { + File previewFile = new File(outputFile + ".preview"); + try { + output = new FileOutputStream(previewFile); + byte[] preview = pointer.getPreview().get(); + output.write(preview, 0, preview.length); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } finally { + if (output != null) { + output.close(); + } + } + } return outputFile; } From 7f21bf0f23294d41653cd5328ce0746b43f68166 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 22 Sep 2015 14:43:24 +0200 Subject: [PATCH 0018/2005] Add version to jar manifest --- build.gradle | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 2e3252fa..628c5c16 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,8 @@ sourceCompatibility = "1.8"; mainClassName = 'cli.Main' +version = '0.0.2' + repositories { mavenCentral() } @@ -18,9 +20,11 @@ dependencies { } jar { - baseName = 'textsecure-cli' - version = '0.0.2' manifest { - attributes 'Main-Class': 'cli.Main' + attributes( + 'Implementation-Title': project.name, + 'Implementation-Version': project.version, + 'Main-Class': project.mainClassName, + ) } } From ab250030619b2b470d62ce60f462d9b43f5bbab2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 22 Sep 2015 14:44:11 +0200 Subject: [PATCH 0019/2005] Add version to user agent string --- src/main/java/cli/Manager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index fef7c81d..846e0dc9 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -51,7 +51,9 @@ class Manager { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); - private final static String USER_AGENT = "textsecure-cli"; + public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); + public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); + private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION; private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure"; private final static String dataPath = settingsPath + "/data"; From 685e431ca0a36262cd6f85c18fc01bb928e1e27f Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 22 Sep 2015 14:45:52 +0200 Subject: [PATCH 0020/2005] Add -v and --version command line arguments Only works running from a jar file --- src/main/java/cli/Main.java | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index b1822aaf..059fb46b 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -161,7 +161,15 @@ public class Main { private static Namespace parseArgs(String[] args) { ArgumentParser parser = ArgumentParsers.newArgumentParser("textsecure-cli") .defaultHelp(true) - .description("Commandline interface for TextSecure."); + .description("Commandline interface for TextSecure.") + .version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION); + + parser.addArgument("-u", "--username") + .help("Specify your phone number, that will be used for verification."); + parser.addArgument("-v", "--version") + .help("Show package version.") + .action(Arguments.version()); + Subparsers subparsers = parser.addSubparsers() .title("subcommands") .dest("command") @@ -188,12 +196,15 @@ public class Main { .help("Add file as attachment"); Subparser parserReceive = subparsers.addParser("receive"); - parser.addArgument("-u", "--username") - .required(true) - .help("Specify your phone number, that will be used for verification."); try { - return parser.parseArgs(args); + Namespace ns = parser.parseArgs(args); + if (ns.getString("username") == null) { + parser.printUsage(); + System.err.println("You need to specify a username (phone number)"); + System.exit(2); + } + return ns; } catch (ArgumentParserException e) { parser.handleError(e); return null; From 35f88c7adceb3def35420488f38303d7c5bdfab4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 22 Sep 2015 14:46:10 +0200 Subject: [PATCH 0021/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 628c5c16..55468056 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ sourceCompatibility = "1.8"; mainClassName = 'cli.Main' -version = '0.0.2' +version = '0.0.4' repositories { mavenCentral() From 171f9441b4950afc84afa87650ed5e92ff4de5dd Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 1 Oct 2015 09:34:55 +0200 Subject: [PATCH 0022/2005] Update dependencies --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 55468056..9fa51e1a 100644 --- a/build.gradle +++ b/build.gradle @@ -12,9 +12,9 @@ repositories { } dependencies { - compile 'org.whispersystems:textsecure-java:1.8.0' + compile 'org.whispersystems:textsecure-java:1.8.1' compile 'com.madgag.spongycastle:prov:1.52.0.0' - compile 'org.json:json:20141113' + compile 'org.json:json:20150729' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.5.0' } From ae8479df8ca04262e54c105bed72ba93a127e48e Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 6 Oct 2015 19:05:15 +0200 Subject: [PATCH 0023/2005] Fix formatting of attachment size --- src/main/java/cli/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 059fb46b..d314ff0b 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -275,7 +275,7 @@ public class Main { if (attachment.isPointer()) { final TextSecureAttachmentPointer pointer = attachment.asPointer(); System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); - System.out.println((pointer.getSize().isPresent() ? " Size: " + pointer.getSize().get() : " bytes") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); + System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); try { File file = m.retrieveAttachment(pointer); System.out.println(" Stored plaintext in: " + file); From 4d83d2168ae78fd414a4dcce5aa630df240aedb8 Mon Sep 17 00:00:00 2001 From: xardas Date: Tue, 6 Oct 2015 21:34:26 +0300 Subject: [PATCH 0024/2005] Make Json store use Jackson instead of Gson (as it's already linked) Closes #4 --- src/main/java/cli/JsonAxolotlStore.java | 46 +++++++---- src/main/java/cli/JsonIdentityKeyStore.java | 87 ++++++++++++++------ src/main/java/cli/JsonPreKeyStore.java | 68 ++++++++++----- src/main/java/cli/JsonSessionStore.java | 68 ++++++++++----- src/main/java/cli/JsonSignedPreKeyStore.java | 66 +++++++++++---- src/main/java/cli/Manager.java | 64 +++++++++----- 6 files changed, 277 insertions(+), 122 deletions(-) diff --git a/src/main/java/cli/JsonAxolotlStore.java b/src/main/java/cli/JsonAxolotlStore.java index 02b9cdf1..57282f12 100644 --- a/src/main/java/cli/JsonAxolotlStore.java +++ b/src/main/java/cli/JsonAxolotlStore.java @@ -1,5 +1,8 @@ package cli; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.json.JSONObject; import org.whispersystems.libaxolotl.*; import org.whispersystems.libaxolotl.state.AxolotlStore; @@ -11,17 +14,35 @@ import java.io.IOException; import java.util.List; class JsonAxolotlStore implements AxolotlStore { - private final JsonPreKeyStore preKeyStore; - private final JsonSessionStore sessionStore; - private final JsonSignedPreKeyStore signedPreKeyStore; - private final JsonIdentityKeyStore identityKeyStore; + @JsonProperty("preKeys") + @JsonDeserialize(using = JsonPreKeyStore.JsonPreKeyStoreDeserializer.class) + @JsonSerialize(using = JsonPreKeyStore.JsonPreKeyStoreSerializer.class) + protected JsonPreKeyStore preKeyStore; - public JsonAxolotlStore(JSONObject jsonAxolotl) throws IOException, InvalidKeyException { - this.preKeyStore = new JsonPreKeyStore(jsonAxolotl.getJSONArray("preKeys")); - this.sessionStore = new JsonSessionStore(jsonAxolotl.getJSONArray("sessionStore")); - this.signedPreKeyStore = new JsonSignedPreKeyStore(jsonAxolotl.getJSONArray("signedPreKeyStore")); - this.identityKeyStore = new JsonIdentityKeyStore(jsonAxolotl.getJSONObject("identityKeyStore")); + @JsonProperty("sessionStore") + @JsonDeserialize(using = JsonSessionStore.JsonSessionStoreDeserializer.class) + @JsonSerialize(using = JsonSessionStore.JsonPreKeyStoreSerializer.class) + protected JsonSessionStore sessionStore; + + @JsonProperty("signedPreKeyStore") + @JsonDeserialize(using = JsonSignedPreKeyStore.JsonSignedPreKeyStoreDeserializer.class) + @JsonSerialize(using = JsonSignedPreKeyStore.JsonSignedPreKeyStoreSerializer.class) + protected JsonSignedPreKeyStore signedPreKeyStore; + + @JsonProperty("identityKeyStore") + @JsonDeserialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreDeserializer.class) + @JsonSerialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreSerializer.class) + protected JsonIdentityKeyStore identityKeyStore; + + public JsonAxolotlStore() { + } + + public JsonAxolotlStore(JsonPreKeyStore preKeyStore, JsonSessionStore sessionStore, JsonSignedPreKeyStore signedPreKeyStore, JsonIdentityKeyStore identityKeyStore) { + this.preKeyStore = preKeyStore; + this.sessionStore = sessionStore; + this.signedPreKeyStore = signedPreKeyStore; + this.identityKeyStore = identityKeyStore; } public JsonAxolotlStore(IdentityKeyPair identityKeyPair, int registrationId) { @@ -31,13 +52,6 @@ class JsonAxolotlStore implements AxolotlStore { this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId); } - public JSONObject getJson() { - return new JSONObject().put("preKeys", preKeyStore.getJson()) - .put("sessionStore", sessionStore.getJson()) - .put("signedPreKeyStore", signedPreKeyStore.getJson()) - .put("identityKeyStore", identityKeyStore.getJson()); - } - @Override public IdentityKeyPair getIdentityKeyPair() { return identityKeyStore.getIdentityKeyPair(); diff --git a/src/main/java/cli/JsonIdentityKeyStore.java b/src/main/java/cli/JsonIdentityKeyStore.java index 827bf055..8577526d 100644 --- a/src/main/java/cli/JsonIdentityKeyStore.java +++ b/src/main/java/cli/JsonIdentityKeyStore.java @@ -1,7 +1,9 @@ package cli; -import org.json.JSONArray; -import org.json.JSONObject; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.*; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.libaxolotl.InvalidKeyException; @@ -18,37 +20,14 @@ class JsonIdentityKeyStore implements IdentityKeyStore { private final IdentityKeyPair identityKeyPair; private final int localRegistrationId; - public JsonIdentityKeyStore(JSONObject jsonAxolotl) throws IOException, InvalidKeyException { - localRegistrationId = jsonAxolotl.getInt("registrationId"); - identityKeyPair = new IdentityKeyPair(Base64.decode(jsonAxolotl.getString("identityKey"))); - - JSONArray list = jsonAxolotl.getJSONArray("trustedKeys"); - for (int i = 0; i < list.length(); i++) { - JSONObject k = list.getJSONObject(i); - try { - trustedKeys.put(k.getString("name"), new IdentityKey(Base64.decode(k.getString("identityKey")), 0)); - } catch (InvalidKeyException | IOException e) { - System.out.println("Error while decoding key for: " + k.getString("name")); - } - } - } public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) { this.identityKeyPair = identityKeyPair; this.localRegistrationId = localRegistrationId; } - public JSONObject getJson() { - JSONArray list = new JSONArray(); - for (String name : trustedKeys.keySet()) { - list.put(new JSONObject().put("name", name).put("identityKey", Base64.encodeBytes(trustedKeys.get(name).serialize()))); - } - - JSONObject result = new JSONObject(); - result.put("registrationId", localRegistrationId); - result.put("identityKey", Base64.encodeBytes(identityKeyPair.serialize())); - result.put("trustedKeys", list); - return result; + public void addTrustedKeys(Map keyMap) { + trustedKeys.putAll(keyMap); } @Override @@ -71,4 +50,58 @@ class JsonIdentityKeyStore implements IdentityKeyStore { IdentityKey trusted = trustedKeys.get(name); return (trusted == null || trusted.equals(identityKey)); } + + public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer { + + @Override + public JsonIdentityKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + try { + int localRegistrationId = node.get("registrationId").asInt(); + IdentityKeyPair identityKeyPair = new IdentityKeyPair(Base64.decode(node.get("identityKey").asText())); + + + Map trustedKeyMap = new HashMap<>(); + JsonNode trustedKeysNode = node.get("trustedKeys"); + if (trustedKeysNode.isArray()) { + for (JsonNode trustedKey : trustedKeysNode) { + String trustedKeyName = trustedKey.get("name").asText(); + try { + trustedKeyMap.put(trustedKeyName, new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0)); + } catch (InvalidKeyException | IOException e) { + System.out.println(String.format("Error while decoding key for: %s", trustedKeyName)); + } + } + } + + JsonIdentityKeyStore keyStore = new JsonIdentityKeyStore(identityKeyPair, localRegistrationId); + keyStore.addTrustedKeys(trustedKeyMap); + + return keyStore; + + } catch (InvalidKeyException e) { + throw new IOException(e); + } + } + } + + public static class JsonIdentityKeyStoreSerializer extends JsonSerializer { + + @Override + public void serialize(JsonIdentityKeyStore jsonIdentityKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { + json.writeStartObject(); + json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId()); + json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize())); + json.writeArrayFieldStart("trustedKeys"); + for (Map.Entry trustedKey : jsonIdentityKeyStore.trustedKeys.entrySet()) { + json.writeStartObject(); + json.writeStringField("name", trustedKey.getKey()); + json.writeStringField("identityKey", Base64.encodeBytes(trustedKey.getValue().serialize())); + json.writeEndObject(); + } + json.writeEndArray(); + json.writeEndObject(); + } + } } diff --git a/src/main/java/cli/JsonPreKeyStore.java b/src/main/java/cli/JsonPreKeyStore.java index a0133ec8..1ae297ec 100644 --- a/src/main/java/cli/JsonPreKeyStore.java +++ b/src/main/java/cli/JsonPreKeyStore.java @@ -1,7 +1,9 @@ package cli; -import org.json.JSONArray; -import org.json.JSONObject; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.*; import org.whispersystems.libaxolotl.InvalidKeyIdException; import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.PreKeyStore; @@ -14,27 +16,13 @@ class JsonPreKeyStore implements PreKeyStore { private final Map store = new HashMap<>(); + public JsonPreKeyStore() { } - public JsonPreKeyStore(JSONArray list) { - for (int i = 0; i < list.length(); i++) { - JSONObject k = list.getJSONObject(i); - try { - store.put(k.getInt("id"), Base64.decode(k.getString("record"))); - } catch (IOException e) { - System.out.println("Error while decoding prekey for: " + k.getString("name")); - } - } - } - - public JSONArray getJson() { - JSONArray result = new JSONArray(); - for (Integer id : store.keySet()) { - result.put(new JSONObject().put("id", id.toString()).put("record", Base64.encodeBytes(store.get(id)))); - } - return result; + public void addPreKeys(Map preKeys) { + store.putAll(preKeys); } @Override @@ -64,4 +52,46 @@ class JsonPreKeyStore implements PreKeyStore { public void removePreKey(int preKeyId) { store.remove(preKeyId); } + + public static class JsonPreKeyStoreDeserializer extends JsonDeserializer { + + @Override + public JsonPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + + Map preKeyMap = new HashMap<>(); + if (node.isArray()) { + for (JsonNode preKey : node) { + Integer preKeyId = preKey.get("id").asInt(); + try { + preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); + } catch (IOException e) { + System.out.println(String.format("Error while decoding prekey for: %s", preKeyId)); + } + } + } + + JsonPreKeyStore keyStore = new JsonPreKeyStore(); + keyStore.addPreKeys(preKeyMap); + + return keyStore; + + } + } + + public static class JsonPreKeyStoreSerializer extends JsonSerializer { + + @Override + public void serialize(JsonPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { + json.writeStartArray(); + for (Map.Entry preKey : jsonPreKeyStore.store.entrySet()) { + json.writeStartObject(); + json.writeNumberField("id", preKey.getKey()); + json.writeStringField("record", Base64.encodeBytes(preKey.getValue())); + json.writeEndObject(); + } + json.writeEndArray(); + } + } } diff --git a/src/main/java/cli/JsonSessionStore.java b/src/main/java/cli/JsonSessionStore.java index d2fe0a4a..c70fa5cf 100644 --- a/src/main/java/cli/JsonSessionStore.java +++ b/src/main/java/cli/JsonSessionStore.java @@ -1,7 +1,9 @@ package cli; -import org.json.JSONArray; -import org.json.JSONObject; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.*; import org.whispersystems.libaxolotl.AxolotlAddress; import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SessionStore; @@ -17,26 +19,10 @@ class JsonSessionStore implements SessionStore { } - public JsonSessionStore(JSONArray list) { - for (int i = 0; i < list.length(); i++) { - JSONObject k = list.getJSONObject(i); - try { - sessions.put(new AxolotlAddress(k.getString("name"), k.getInt("deviceId")), Base64.decode(k.getString("record"))); - } catch (IOException e) { - System.out.println("Error while decoding prekey for: " + k.getString("name")); - } - } + public void addSessions(Map sessions) { + this.sessions.putAll(sessions); } - public JSONArray getJson() { - JSONArray result = new JSONArray(); - for (AxolotlAddress address : sessions.keySet()) { - result.put(new JSONObject().put("name", address.getName()). - put("deviceId", address.getDeviceId()). - put("record", Base64.encodeBytes(sessions.get(address)))); - } - return result; - } @Override public synchronized SessionRecord loadSession(AxolotlAddress remoteAddress) { @@ -88,4 +74,46 @@ class JsonSessionStore implements SessionStore { } } } + + public static class JsonSessionStoreDeserializer extends JsonDeserializer { + + @Override + public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + Map sessionMap = new HashMap<>(); + if (node.isArray()) { + for (JsonNode session : node) { + String sessionName = session.get("name").asText(); + try { + sessionMap.put(new AxolotlAddress(sessionName, session.get("deviceId").asInt()), Base64.decode(session.get("record").asText())); + } catch (IOException e) { + System.out.println(String.format("Error while decoding session for: %s", sessionName)); + } + } + } + + JsonSessionStore sessionStore = new JsonSessionStore(); + sessionStore.addSessions(sessionMap); + + return sessionStore; + + } + } + + public static class JsonPreKeyStoreSerializer extends JsonSerializer { + + @Override + public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { + json.writeStartArray(); + for (Map.Entry preKey : jsonSessionStore.sessions.entrySet()) { + json.writeStartObject(); + json.writeStringField("name", preKey.getKey().getName()); + json.writeNumberField("deviceId", preKey.getKey().getDeviceId()); + json.writeStringField("record", Base64.encodeBytes(preKey.getValue())); + json.writeEndObject(); + } + json.writeEndArray(); + } + } } diff --git a/src/main/java/cli/JsonSignedPreKeyStore.java b/src/main/java/cli/JsonSignedPreKeyStore.java index f992d2d6..9b48fa97 100644 --- a/src/main/java/cli/JsonSignedPreKeyStore.java +++ b/src/main/java/cli/JsonSignedPreKeyStore.java @@ -1,7 +1,9 @@ package cli; -import org.json.JSONArray; -import org.json.JSONObject; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.*; import org.whispersystems.libaxolotl.InvalidKeyIdException; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyStore; @@ -20,23 +22,9 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { } - public JsonSignedPreKeyStore(JSONArray list) { - for (int i = 0; i < list.length(); i++) { - JSONObject k = list.getJSONObject(i); - try { - store.put(k.getInt("id"), Base64.decode(k.getString("record"))); - } catch (IOException e) { - System.out.println("Error while decoding prekey for: " + k.getString("name")); - } - } - } - public JSONArray getJson() { - JSONArray result = new JSONArray(); - for (Integer id : store.keySet()) { - result.put(new JSONObject().put("id", id.toString()).put("record", Base64.encodeBytes(store.get(id)))); - } - return result; + public void addSignedPreKeys(Map preKeys) { + store.putAll(preKeys); } @Override @@ -81,4 +69,46 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { public void removeSignedPreKey(int signedPreKeyId) { store.remove(signedPreKeyId); } + + public static class JsonSignedPreKeyStoreDeserializer extends JsonDeserializer { + + @Override + public JsonSignedPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + + Map preKeyMap = new HashMap<>(); + if (node.isArray()) { + for (JsonNode preKey : node) { + Integer preKeyId = preKey.get("id").asInt(); + try { + preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); + } catch (IOException e) { + System.out.println(String.format("Error while decoding prekey for: %s", preKeyId)); + } + } + } + + JsonSignedPreKeyStore keyStore = new JsonSignedPreKeyStore(); + keyStore.addSignedPreKeys(preKeyMap); + + return keyStore; + + } + } + + public static class JsonSignedPreKeyStoreSerializer extends JsonSerializer { + + @Override + public void serialize(JsonSignedPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { + json.writeStartArray(); + for (Map.Entry signedPreKey : jsonPreKeyStore.store.entrySet()) { + json.writeStartObject(); + json.writeNumberField("id", signedPreKey.getKey()); + json.writeStringField("record", Base64.encodeBytes(signedPreKey.getValue())); + json.writeEndObject(); + } + json.writeEndArray(); + } + } } diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 846e0dc9..4d8c733f 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -16,8 +16,13 @@ */ package cli; -import org.apache.commons.io.IOUtils; -import org.json.JSONObject; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.whispersystems.libaxolotl.*; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; @@ -59,6 +64,7 @@ class Manager { private final static String dataPath = settingsPath + "/data"; private final static String attachmentsPath = settingsPath + "/attachments"; + private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; private String password; private String signalingKey; @@ -72,6 +78,10 @@ class Manager { public Manager(String username) { this.username = username; + jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect + jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. + jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); + jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } public String getFileName() { @@ -88,43 +98,53 @@ class Manager { return axolotlStore != null; } - public void load() throws IOException, InvalidKeyException { - JSONObject in = new JSONObject(IOUtils.toString(new FileInputStream(getFileName()))); - username = in.getString("username"); - password = in.getString("password"); - if (in.has("signalingKey")) { - signalingKey = in.getString("signalingKey"); + private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException { + JsonNode node = parent.get(name); + if (node == null) { + throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name)); } - if (in.has("preKeyIdOffset")) { - preKeyIdOffset = in.getInt("preKeyIdOffset"); + + return node; + } + + + public void load() throws IOException, InvalidKeyException { + JsonNode rootNode = jsonProcessot.readTree(new File(getFileName())); + + username = getNotNullNode(rootNode, "username").asText(); + password = getNotNullNode(rootNode, "password").asText(); + if (rootNode.has("signalingKey")) { + signalingKey = getNotNullNode(rootNode, "signalingKey").asText(); + } + if (rootNode.has("preKeyIdOffset")) { + preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0); } else { preKeyIdOffset = 0; } - if (in.has("nextSignedPreKeyId")) { - nextSignedPreKeyId = in.getInt("nextSignedPreKeyId"); + if (rootNode.has("nextSignedPreKeyId")) { + nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt(); } else { nextSignedPreKeyId = 0; } - axolotlStore = new JsonAxolotlStore(in.getJSONObject("axolotlStore")); - registered = in.getBoolean("registered"); + axolotlStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonAxolotlStore.class); //new JsonAxolotlStore(in.getJSONObject("axolotlStore")); + registered = getNotNullNode(rootNode, "registered").asBoolean(); accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); } public void save() { - String out = new JSONObject().put("username", username) + ObjectNode rootNode = jsonProcessot.createObjectNode(); + rootNode.put("username", username) .put("password", password) .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) - .put("axolotlStore", axolotlStore.getJson()) - .put("registered", registered).toString(); + .put("registered", registered) + .putPOJO("axolotlStore", axolotlStore) + ; try { - OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(getFileName())); - writer.write(out); - writer.flush(); - writer.close(); + jsonProcessot.writeValue(new File(getFileName()), rootNode); } catch (Exception e) { - System.err.println("Saving file error: " + e.getMessage()); + System.err.println(String.format("Error saving file: %s", e.getMessage())); } } From 2c86b0bd9a7abfd4a751fc1d6ab723d3a2c54c8a Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 6 Oct 2015 21:42:08 +0200 Subject: [PATCH 0025/2005] Remove last remnants of org.json --- build.gradle | 1 - src/main/java/cli/JsonAxolotlStore.java | 1 - 2 files changed, 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9fa51e1a..907fb8f7 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,6 @@ repositories { dependencies { compile 'org.whispersystems:textsecure-java:1.8.1' compile 'com.madgag.spongycastle:prov:1.52.0.0' - compile 'org.json:json:20150729' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.5.0' } diff --git a/src/main/java/cli/JsonAxolotlStore.java b/src/main/java/cli/JsonAxolotlStore.java index 57282f12..cfed0cd8 100644 --- a/src/main/java/cli/JsonAxolotlStore.java +++ b/src/main/java/cli/JsonAxolotlStore.java @@ -3,7 +3,6 @@ package cli; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.json.JSONObject; import org.whispersystems.libaxolotl.*; import org.whispersystems.libaxolotl.state.AxolotlStore; import org.whispersystems.libaxolotl.state.PreKeyRecord; From 1781984221365b75d859e9feb81dcf79e7475e7d Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 6 Oct 2015 21:45:39 +0200 Subject: [PATCH 0026/2005] Fix formatting --- src/main/java/cli/JsonAxolotlStore.java | 6 ++++-- src/main/java/cli/JsonIdentityKeyStore.java | 2 +- src/main/java/cli/JsonPreKeyStore.java | 2 +- src/main/java/cli/JsonSessionStore.java | 2 +- src/main/java/cli/JsonSignedPreKeyStore.java | 2 +- src/main/java/cli/Manager.java | 4 ++-- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/cli/JsonAxolotlStore.java b/src/main/java/cli/JsonAxolotlStore.java index cfed0cd8..e7eb054c 100644 --- a/src/main/java/cli/JsonAxolotlStore.java +++ b/src/main/java/cli/JsonAxolotlStore.java @@ -3,13 +3,15 @@ package cli; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.whispersystems.libaxolotl.*; +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyIdException; 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 java.io.IOException; import java.util.List; class JsonAxolotlStore implements AxolotlStore { diff --git a/src/main/java/cli/JsonIdentityKeyStore.java b/src/main/java/cli/JsonIdentityKeyStore.java index 8577526d..e4d18b8f 100644 --- a/src/main/java/cli/JsonIdentityKeyStore.java +++ b/src/main/java/cli/JsonIdentityKeyStore.java @@ -69,7 +69,7 @@ class JsonIdentityKeyStore implements IdentityKeyStore { String trustedKeyName = trustedKey.get("name").asText(); try { trustedKeyMap.put(trustedKeyName, new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0)); - } catch (InvalidKeyException | IOException e) { + } catch (InvalidKeyException | IOException e) { System.out.println(String.format("Error while decoding key for: %s", trustedKeyName)); } } diff --git a/src/main/java/cli/JsonPreKeyStore.java b/src/main/java/cli/JsonPreKeyStore.java index 1ae297ec..393f1805 100644 --- a/src/main/java/cli/JsonPreKeyStore.java +++ b/src/main/java/cli/JsonPreKeyStore.java @@ -66,7 +66,7 @@ class JsonPreKeyStore implements PreKeyStore { Integer preKeyId = preKey.get("id").asInt(); try { preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); - } catch (IOException e) { + } catch (IOException e) { System.out.println(String.format("Error while decoding prekey for: %s", preKeyId)); } } diff --git a/src/main/java/cli/JsonSessionStore.java b/src/main/java/cli/JsonSessionStore.java index c70fa5cf..3cb78945 100644 --- a/src/main/java/cli/JsonSessionStore.java +++ b/src/main/java/cli/JsonSessionStore.java @@ -87,7 +87,7 @@ class JsonSessionStore implements SessionStore { String sessionName = session.get("name").asText(); try { sessionMap.put(new AxolotlAddress(sessionName, session.get("deviceId").asInt()), Base64.decode(session.get("record").asText())); - } catch (IOException e) { + } catch (IOException e) { System.out.println(String.format("Error while decoding session for: %s", sessionName)); } } diff --git a/src/main/java/cli/JsonSignedPreKeyStore.java b/src/main/java/cli/JsonSignedPreKeyStore.java index 9b48fa97..4dc0cad3 100644 --- a/src/main/java/cli/JsonSignedPreKeyStore.java +++ b/src/main/java/cli/JsonSignedPreKeyStore.java @@ -83,7 +83,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { Integer preKeyId = preKey.get("id").asInt(); try { preKeyMap.put(preKeyId, Base64.decode(preKey.get("record").asText())); - } catch (IOException e) { + } catch (IOException e) { System.out.println(String.format("Error while decoding prekey for: %s", preKeyId)); } } diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 4d8c733f..7ca2c123 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -56,8 +56,8 @@ class Manager { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); - public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); - public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); + public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); + public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION; private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure"; From 2d3bc7e5fe6e76c0da4db1ecadaa7418f6b6b9fa Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 25 Oct 2015 21:15:15 +0100 Subject: [PATCH 0027/2005] Update dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 907fb8f7..b8cf9e9a 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ repositories { dependencies { compile 'org.whispersystems:textsecure-java:1.8.1' - compile 'com.madgag.spongycastle:prov:1.52.0.0' + compile 'com.madgag.spongycastle:prov:1.53.0.0' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.5.0' } From 75351e6ab7aeb0ee9c52742020b35f2457936a4e Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 10 Nov 2015 19:08:18 +0100 Subject: [PATCH 0028/2005] Update dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b8cf9e9a..1294600f 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ repositories { } dependencies { - compile 'org.whispersystems:textsecure-java:1.8.1' + compile 'org.whispersystems:textsecure-java:1.8.2' compile 'com.madgag.spongycastle:prov:1.53.0.0' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.5.0' From 5accb9b02fb7352b557266a4f6822669823fff8f Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 18 Nov 2015 19:18:06 +0100 Subject: [PATCH 0029/2005] Update dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1294600f..241fcd71 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ repositories { } dependencies { - compile 'org.whispersystems:textsecure-java:1.8.2' + compile 'org.whispersystems:textsecure-java:1.8.3' compile 'com.madgag.spongycastle:prov:1.53.0.0' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.5.0' From 96bd68e034516dc94113a4b635a56f0fb5aa81e6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 Nov 2015 18:05:34 +0100 Subject: [PATCH 0030/2005] Add commandline option to specify receive timeou --- src/main/java/cli/Main.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index d314ff0b..9e142ff1 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -141,8 +141,14 @@ public class Main { System.err.println("User is not registered."); System.exit(1); } + int timeout = ns.getInt("timeout"); + boolean returnOnTimeout = true; + if (timeout < 0) { + returnOnTimeout = false; + timeout = 5; + } try { - m.receiveMessages(5, true, new ReceiveMessageHandler(m)); + m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m)); } catch (IOException e) { System.err.println("Error while receiving message: " + e.getMessage()); System.exit(3); @@ -196,6 +202,9 @@ public class Main { .help("Add file as attachment"); Subparser parserReceive = subparsers.addParser("receive"); + parserReceive.addArgument("-t", "--timeout") + .type(int.class) + .help("Number of seconds to wait for new messages (negative values disable timeout)"); try { Namespace ns = parser.parseArgs(args); From c5b884f62b3a52477cf3d9fd2ca6fce84f9fc805 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 Nov 2015 19:01:02 +0100 Subject: [PATCH 0031/2005] Show GroupInfo of messages, if present --- src/main/java/cli/Main.java | 57 ++++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 9e142ff1..07353a19 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -273,25 +273,39 @@ public class Main { } else { if (content.getDataMessage().isPresent()) { TextSecureDataMessage message = content.getDataMessage().get(); - System.out.println("Body: " + message.getBody().get()); + System.out.println("Message timestamp: " + message.getTimestamp()); + + if (message.getBody().isPresent()) { + System.out.println("Body: " + message.getBody().get()); + } + if (message.getGroupInfo().isPresent()) { + TextSecureGroup groupInfo = message.getGroupInfo().get(); + System.out.println("Group info:"); + System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); + if (groupInfo.getName().isPresent()) { + System.out.println(" Name: " + groupInfo.getName().get()); + } + System.out.println(" Type: " + groupInfo.getType()); + if (groupInfo.getMembers().isPresent()) { + for (String member : groupInfo.getMembers().get()) { + System.out.println(" Member: " + member); + } + } + if (groupInfo.getAvatar().isPresent()) { + System.out.println(" Avatar:"); + printAttachment(groupInfo.getAvatar().get()); + } + } if (message.isEndSession()) { + System.out.println("Is end session"); m.handleEndSession(envelope.getSource()); - } else if (message.getAttachments().isPresent()) { + } + + if (message.getAttachments().isPresent()) { System.out.println("Attachments: "); for (TextSecureAttachment attachment : message.getAttachments().get()) { - System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); - if (attachment.isPointer()) { - final TextSecureAttachmentPointer pointer = attachment.asPointer(); - System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); - System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); - try { - File file = m.retrieveAttachment(pointer); - System.out.println(" Stored plaintext in: " + file); - } catch (IOException | InvalidMessageException e) { - System.out.println("Failed to retrieve attachment: " + e.getMessage()); - } - } + printAttachment(attachment); } } } @@ -305,5 +319,20 @@ public class Main { } System.out.println(); } + + private void printAttachment(TextSecureAttachment attachment) { + System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); + if (attachment.isPointer()) { + final TextSecureAttachmentPointer pointer = attachment.asPointer(); + System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); + System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); + try { + File file = m.retrieveAttachment(pointer); + System.out.println(" Stored plaintext in: " + file); + } catch (IOException | InvalidMessageException e) { + System.out.println("Failed to retrieve attachment: " + e.getMessage()); + } + } + } } } From 1b3cd511576f03c60a95c6a12633a5b3a5bc183f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 Nov 2015 18:07:12 +0100 Subject: [PATCH 0032/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 241fcd71..1bf3b6e7 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ sourceCompatibility = "1.8"; mainClassName = 'cli.Main' -version = '0.0.4' +version = '0.0.5' repositories { mavenCentral() From b5400f77c3bf7df3e917962f8cd39f85574c58a5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Nov 2015 16:02:00 +0100 Subject: [PATCH 0033/2005] Update gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e8c6bf7bb47dff6b81c2cf7a349eb7e912c9fbe2..941144813d241db74e1bf25b6804c679fbe7f0a3 100644 GIT binary patch delta 1832 zcmZWpX;4#F6ux<3QxZ_busk*eK^O`Vu%ktG6)Vcp2&gDR2`N|<3#ha(D3nsQI213A zb@@?+0xp$E9F&$i!2l5w2r0u9r_)xbTCs?r16sw>`_8S<)XZeQbH99Nz2~N^nqOMY z7sf{Mjpia`W`=S?KdKbEn-PdqJzi^98MxVc%afe(93Gn0$cUzH5uXFiQh-S@2iTK0 z$e}{WA87oXtR^X}eTQPZLN(BjYu-b?Z6 z@Ue-$;kbQAB@vbhCHTj#%ke#v=-k2!js=-*p~B?bZymSa=<+BdA~^kb*^p<9_C#(# zRCCfd7aC+fZKE>(->SZdiE~YKC<>Ktine@uqTe z@B>COIboM%es7xTKec^ZL0P*In9h?&oI?YASn zqL$3)0r@V)SIXV6RC*00PU+JO7%myZ)DV1#dEgXclQFF}MdHGGjG&sL@x~ zWCTBzU*hwjlqRJ-_bUdW3nnwA=o}Px8qQIN&lPo4?RRtC&G7kp9q4!4&0fzlVy zzV&TDNoz1*Rckchz1D2N#G4xd>uzoaWK_w3X)1cYp-O_YL)&`5%(ghdAKIb-&D*I? z)-Hi_OM5(^bq9?r?MQ=jSI36`y$M(K>e;=|R*QHtT&DKmri~4f36nK zOD7JlI5wTdul|iA=wunI41Bi16K~EC;Zd!?g^TgPRrn;5hfoxWv+Q*Mf>(?G-brFO zV0ou22o09eKvQ;?!MpeF-Vf^CJP;6;6Jb*inYym z;$lFu#h-L;J!Xv1Ng+b+24YbGR25Soz~6UJ@tLe`x;oNAEgzv32I7%}P{y|!M;K+d zhGm9J@aap7u|NyEj~8iKUMmjLz6*lGT9!Fp4%KvOmjU&PP!Sb`T8vk9Q=hnQmKi+@ zDxY&!Z`?|xUN&-!G zxc{V3k{Blxxe&s#*o#cDzmm0?(9}!J0)mOiFLctZ*t*KORT4;skP4#bS^e&)`6Te zUh5*3In6YzQCzHg({8s84whVh^t&xtS-vY?df)?gt)hN4`}G6awD^&{L!b4J7JL^l zcVF*7%}-gn>fX`2i|fqFkZWNn-Dj4xClr|LMrw1?<4(n}wbLIQw|U>l54&CQclv?) zqshs2Ir*pjnvL$il^o9-bo zee)9}^a|ZQ-f-Ot0{nC$O~ImJ%v1=`xSbs#bmwLkkYiq7CMKjp0)hfh*iquD*W}iU z(P?b&KfK`TF(Iyif4=S9oT;c{PKHW1859kwQW&lL(2x z#3fcSxb=AIKW|YBr*O61sB4|1rd#s{{VND0^!S6!F!jatQJquG(c=0nHO$}^5w;n^ zCSDDEWe7*|j$t;UtaUD;wsjF=S*r%|R_ptSNo@&;r`pzWSY~_&kEupHLW?mO&pwx8 z5w~2@B3`%@hdAjn@3-mlDmD=&SJtTdEI4bhsfM+aYIyHo>>Wfl$=p zfm9`>q??iI?chVXnB|Oz4n}MtkZ2CT3ho9Do-o>H5fX|JI-6FG4!Ih5LSnwB?fqN^rDeMUJQNS)J*v)l*6wx_e~mNUB>a3E(J zUYclQX^+>P`*au|x)QR`p^a;L>Kq8abmKLbU6BiX!S~8?Y(CUem+wpyUJwzYa!?I5 rqbl^g6qFWzQVtK*!8xXd&L9$46Ms35&-J-yW8Mvj?@>9W7&z`fj!1YV diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 327f6b58..c957a226 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Sep 15 12:33:25 CEST 2015 +#Mon Nov 23 15:59:33 CET 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-bin.zip diff --git a/gradlew b/gradlew index 97fac783..9d82f789 100755 --- a/gradlew +++ b/gradlew @@ -56,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar From 6208eb72d823a5b4937c2a30f3219fec3a3b3a59 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Nov 2015 12:58:52 +0100 Subject: [PATCH 0034/2005] Increase timeout --- src/main/java/cli/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 07353a19..44a89505 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -145,7 +145,7 @@ public class Main { boolean returnOnTimeout = true; if (timeout < 0) { returnOnTimeout = false; - timeout = 5; + timeout = 3600; } try { m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m)); From d0dae4e064ce2b41e4e6fcde786eec6e179656d6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Nov 2015 14:38:30 +0100 Subject: [PATCH 0035/2005] Fix NPE when not specifying receive timeout --- src/main/java/cli/Main.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 44a89505..728ea35b 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -141,7 +141,10 @@ public class Main { System.err.println("User is not registered."); System.exit(1); } - int timeout = ns.getInt("timeout"); + int timeout = 5; + if (ns.getInt("timeout") != null) { + timeout = ns.getInt("timeout"); + } boolean returnOnTimeout = true; if (timeout < 0) { returnOnTimeout = false; From c41ac8e7a32e0eaa3ab8786be14da46f4c20a08a Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Nov 2015 13:41:08 +0100 Subject: [PATCH 0036/2005] Store group info in json --- src/main/java/cli/GroupInfo.java | 28 +++++++++++++++ src/main/java/cli/JsonGroupStore.java | 50 +++++++++++++++++++++++++++ src/main/java/cli/Main.java | 8 +++-- src/main/java/cli/Manager.java | 45 ++++++++++++++++++++---- 4 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 src/main/java/cli/GroupInfo.java create mode 100644 src/main/java/cli/JsonGroupStore.java diff --git a/src/main/java/cli/GroupInfo.java b/src/main/java/cli/GroupInfo.java new file mode 100644 index 00000000..f3060d08 --- /dev/null +++ b/src/main/java/cli/GroupInfo.java @@ -0,0 +1,28 @@ +package cli; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class GroupInfo { + @JsonProperty + public final byte[] groupId; + + @JsonProperty + public String name; + + @JsonProperty + public List members = new ArrayList<>(); + + @JsonProperty + public long avatarId; + + public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId) { + this.groupId = groupId; + this.name = name; + this.members.addAll(members); + this.avatarId = avatarId; + } +} diff --git a/src/main/java/cli/JsonGroupStore.java b/src/main/java/cli/JsonGroupStore.java new file mode 100644 index 00000000..29f9abfe --- /dev/null +++ b/src/main/java/cli/JsonGroupStore.java @@ -0,0 +1,50 @@ +package cli; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class JsonGroupStore { + @JsonProperty("groups") + @JsonSerialize(using = JsonGroupStore.MapToListSerializer.class) + @JsonDeserialize(using = JsonGroupStore.GroupsDeserializer.class) + private Map groups = new HashMap<>(); + + private static final ObjectMapper jsonProcessot = new ObjectMapper(); + + void updateGroup(GroupInfo group) { + groups.put(Base64.encodeBytes(group.groupId), group); + } + + GroupInfo getGroup(byte[] groupId) { + return groups.get(Base64.encodeBytes(groupId)); + } + + public static class MapToListSerializer extends JsonSerializer> { + @Override + public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { + jgen.writeObject(value.values()); + } + } + + public static class GroupsDeserializer extends JsonDeserializer> { + @Override + public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + Map groups = new HashMap<>(); + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + for (JsonNode n : node) { + GroupInfo g = jsonProcessot.treeToValue(n, GroupInfo.class); + groups.put(Base64.encodeBytes(g.groupId), g); + } + + return groups; + } + } +} diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 728ea35b..85ca227d 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -262,15 +262,13 @@ public class Main { } @Override - public void handleMessage(TextSecureEnvelope envelope) { + public void handleMessage(TextSecureEnvelope envelope, TextSecureContent content, GroupInfo group) { System.out.println("Envelope from: " + envelope.getSource()); System.out.println("Timestamp: " + envelope.getTimestamp()); if (envelope.isReceipt()) { System.out.println("Got receipt."); } else if (envelope.isWhisperMessage() | envelope.isPreKeyWhisperMessage()) { - TextSecureContent content = m.decryptMessage(envelope); - if (content == null) { System.out.println("Failed to decrypt message."); } else { @@ -288,6 +286,10 @@ public class Main { System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); if (groupInfo.getName().isPresent()) { System.out.println(" Name: " + groupInfo.getName().get()); + } else if (group != null) { + System.out.println(" Name: " + group.name); + } else { + System.out.println(" Name: "); } System.out.println(" Type: " + groupInfo.getType()); if (groupInfo.getMembers().isPresent()) { diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 7ca2c123..2258c99d 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -36,10 +36,7 @@ import org.whispersystems.textsecure.api.TextSecureMessagePipe; import org.whispersystems.textsecure.api.TextSecureMessageReceiver; import org.whispersystems.textsecure.api.TextSecureMessageSender; import org.whispersystems.textsecure.api.crypto.TextSecureCipher; -import org.whispersystems.textsecure.api.messages.TextSecureAttachmentPointer; -import org.whispersystems.textsecure.api.messages.TextSecureContent; -import org.whispersystems.textsecure.api.messages.TextSecureDataMessage; -import org.whispersystems.textsecure.api.messages.TextSecureEnvelope; +import org.whispersystems.textsecure.api.messages.*; import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.api.push.TrustStore; import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; @@ -75,6 +72,7 @@ class Manager { private JsonAxolotlStore axolotlStore; private TextSecureAccountManager accountManager; + private JsonGroupStore groupStore; public Manager(String username) { this.username = username; @@ -107,7 +105,6 @@ class Manager { return node; } - public void load() throws IOException, InvalidKeyException { JsonNode rootNode = jsonProcessot.readTree(new File(getFileName())); @@ -128,6 +125,10 @@ class Manager { } axolotlStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonAxolotlStore.class); //new JsonAxolotlStore(in.getJSONObject("axolotlStore")); registered = getNotNullNode(rootNode, "registered").asBoolean(); + JsonNode groupStoreNode = rootNode.get("groupStore"); + if (groupStoreNode != null) { + groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class); + } accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); } @@ -140,6 +141,7 @@ class Manager { .put("nextSignedPreKeyId", nextSignedPreKeyId) .put("registered", registered) .putPOJO("axolotlStore", axolotlStore) + .putPOJO("groupStore", groupStore) ; try { jsonProcessot.writeValue(new File(getFileName()), rootNode); @@ -152,6 +154,7 @@ class Manager { IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); int registrationId = KeyHelper.generateRegistrationId(false); axolotlStore = new JsonAxolotlStore(identityKey, registrationId); + groupStore = new JsonGroupStore(); registered = false; } @@ -262,7 +265,7 @@ class Manager { } public interface ReceiveMessageHandler { - void handleMessage(TextSecureEnvelope envelope); + void handleMessage(TextSecureEnvelope envelope, TextSecureContent decryptedContent, GroupInfo group); } public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { @@ -274,9 +277,37 @@ class Manager { while (true) { TextSecureEnvelope envelope; + TextSecureContent content = null; + GroupInfo group = null; try { envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS); - handler.handleMessage(envelope); + if (!envelope.isReceipt()) { + content = decryptMessage(envelope); + if (content != null) { + if (content.getDataMessage().isPresent()) { + TextSecureDataMessage message = content.getDataMessage().get(); + if (message.getGroupInfo().isPresent()) { + TextSecureGroup groupInfo = message.getGroupInfo().get(); + switch (groupInfo.getType()) { + case UPDATE: + group = new GroupInfo(groupInfo.getGroupId(), groupInfo.getName().get(), groupInfo.getMembers().get(), groupInfo.getAvatar().get().asPointer().getId()); + groupStore.updateGroup(group); + break; + case DELIVER: + group = groupStore.getGroup(groupInfo.getGroupId()); + break; + case QUIT: + group = groupStore.getGroup(groupInfo.getGroupId()); + if (group != null) { + group.members.remove(envelope.getSource()); + } + break; + } + } + } + } + } + handler.handleMessage(envelope, content, group); } catch (TimeoutException e) { if (returnOnTimeout) return; From 2517919c49ccf3ed3c4bfa90782a4afa54fbf547 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Nov 2015 14:38:08 +0100 Subject: [PATCH 0037/2005] Enable sending to groups --- src/main/java/cli/Main.java | 37 +++++++++++++++++++++++++++++++--- src/main/java/cli/Manager.java | 4 ++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 85ca227d..cac746d2 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -101,6 +101,7 @@ public class Main { messageText = IOUtils.toString(System.in); } catch (IOException e) { System.err.println("Failed to read message from stdin: " + e.getMessage()); + System.err.println("Aborting sending."); System.exit(1); } } @@ -123,9 +124,29 @@ public class Main { } } } + TextSecureGroup group = null; + List recipientStrings = null; + if (ns.getString("group") != null) { + try { + GroupInfo g = m.getGroupInfo(Base64.decode(ns.getString("group"))); + if (g == null) { + System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": Unknown group"); + System.err.println("Aborting sending."); + System.exit(1); + } + group = new TextSecureGroup(g.groupId); + recipientStrings = g.members; + } catch (IOException e) { + System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); + } + } else { + recipientStrings = ns.getList("recipient"); + } List recipients = new ArrayList<>(ns.getList("recipient").size()); - for (String recipient : ns.getList("recipient")) { + for (String recipient : recipientStrings) { try { recipients.add(m.getPushAddress(recipient)); } catch (InvalidNumberException e) { @@ -134,7 +155,8 @@ public class Main { System.exit(1); } } - sendMessage(m, messageText, textSecureAttachments, recipients); + + sendMessage(m, messageText, textSecureAttachments, recipients, group); break; case "receive": if (!m.isRegistered()) { @@ -195,6 +217,8 @@ public class Main { .help("The verification code you received via sms or voice call."); Subparser parserSend = subparsers.addParser("send"); + parserSend.addArgument("-g", "--group") + .help("Specify the recipient group ID."); parserSend.addArgument("recipient") .help("Specify the recipients' phone number.") .nargs("*"); @@ -216,6 +240,10 @@ public class Main { System.err.println("You need to specify a username (phone number)"); System.exit(2); } + if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) { + System.err.println("You cannot specify recipients by phone number and groups a the same time"); + System.exit(2); + } return ns; } catch (ArgumentParserException e) { parser.handleError(e); @@ -224,11 +252,14 @@ public class Main { } private static void sendMessage(Manager m, String messageText, List textSecureAttachments, - List recipients) { + List recipients, TextSecureGroup group) { final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); if (textSecureAttachments != null) { messageBuilder.withAttachments(textSecureAttachments); } + if (group != null) { + messageBuilder.asGroupMessage(group); + } TextSecureDataMessage message = messageBuilder.build(); try { diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 2258c99d..758dabc3 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -378,4 +378,8 @@ class Manager { String e164number = canonicalizeNumber(number); return new TextSecureAddress(e164number); } + + GroupInfo getGroupInfo(byte[] groupId) { + return groupStore.getGroup(groupId); + } } From 2c6796e3ce830b0285223baa11ec5e8553b4d52f Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Nov 2015 14:17:32 +0100 Subject: [PATCH 0038/2005] Move endsession and attachment handling to Manager --- src/main/java/cli/Main.java | 26 +++---------- src/main/java/cli/Manager.java | 68 +++++++++++++++++++++++++++++----- 2 files changed, 65 insertions(+), 29 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index cac746d2..199ea1eb 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -125,7 +125,7 @@ public class Main { } } TextSecureGroup group = null; - List recipientStrings = null; + List recipients = null; if (ns.getString("group") != null) { try { GroupInfo g = m.getGroupInfo(Base64.decode(ns.getString("group"))); @@ -135,25 +135,14 @@ public class Main { System.exit(1); } group = new TextSecureGroup(g.groupId); - recipientStrings = g.members; + recipients = g.members; } catch (IOException e) { System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": " + e.getMessage()); System.err.println("Aborting sending."); System.exit(1); } } else { - recipientStrings = ns.getList("recipient"); - } - - List recipients = new ArrayList<>(ns.getList("recipient").size()); - for (String recipient : recipientStrings) { - try { - recipients.add(m.getPushAddress(recipient)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - System.exit(1); - } + recipients = ns.getList("recipient"); } sendMessage(m, messageText, textSecureAttachments, recipients, group); @@ -252,7 +241,7 @@ public class Main { } private static void sendMessage(Manager m, String messageText, List textSecureAttachments, - List recipients, TextSecureGroup group) { + List recipients, TextSecureGroup group) { final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); if (textSecureAttachments != null) { messageBuilder.withAttachments(textSecureAttachments); @@ -335,7 +324,6 @@ public class Main { } if (message.isEndSession()) { System.out.println("Is end session"); - m.handleEndSession(envelope.getSource()); } if (message.getAttachments().isPresent()) { @@ -362,11 +350,9 @@ public class Main { final TextSecureAttachmentPointer pointer = attachment.asPointer(); System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); - try { - File file = m.retrieveAttachment(pointer); + File file = m.getAttachmentFile(pointer.getId()); + if (file.exists()) { System.out.println(" Stored plaintext in: " + file); - } catch (IOException | InvalidMessageException e) { - System.out.println("Failed to retrieve attachment: " + e.getMessage()); } } } diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 758dabc3..a1d58f03 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -44,6 +44,7 @@ import org.whispersystems.textsecure.api.util.InvalidNumberException; import org.whispersystems.textsecure.api.util.PhoneNumberFormatter; import java.io.*; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -242,14 +243,32 @@ class Manager { accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); } - public void sendMessage(List recipients, TextSecureDataMessage message) + public void sendMessage(List recipients, TextSecureDataMessage message) throws IOException, EncapsulatedExceptions { TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password, axolotlStore, USER_AGENT, Optional.absent()); - messageSender.sendMessage(recipients, message); + + List recipientsTS = new ArrayList<>(recipients.size()); + for (String recipient : recipients) { + try { + recipientsTS.add(getPushAddress(recipient)); + } catch (InvalidNumberException e) { + System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + return; + } + } + + messageSender.sendMessage(recipientsTS, message); + + if (message.isEndSession()) { + for (TextSecureAddress recipient : recipientsTS) { + handleEndSession(recipient.getNumber()); + } + } } - public TextSecureContent decryptMessage(TextSecureEnvelope envelope) { + private TextSecureContent decryptMessage(TextSecureEnvelope envelope) { TextSecureCipher cipher = new TextSecureCipher(new TextSecureAddress(username), axolotlStore); try { return cipher.decrypt(envelope); @@ -260,7 +279,7 @@ class Manager { } } - public void handleEndSession(String source) { + private void handleEndSession(String source) { axolotlStore.deleteAllSessions(source); } @@ -290,7 +309,20 @@ class Manager { TextSecureGroup groupInfo = message.getGroupInfo().get(); switch (groupInfo.getType()) { case UPDATE: - group = new GroupInfo(groupInfo.getGroupId(), groupInfo.getName().get(), groupInfo.getMembers().get(), groupInfo.getAvatar().get().asPointer().getId()); + long avatarId = 0; + if (groupInfo.getAvatar().isPresent()) { + TextSecureAttachment avatar = groupInfo.getAvatar().get(); + if (avatar.isPointer()) { + avatarId = avatar.asPointer().getId(); + try { + retrieveAttachment(avatar.asPointer()); + } catch (IOException | InvalidMessageException e) { + System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage()); + } + } + } + + group = new GroupInfo(groupInfo.getGroupId(), groupInfo.getName().get(), groupInfo.getMembers().get(), avatarId); groupStore.updateGroup(group); break; case DELIVER: @@ -304,6 +336,20 @@ class Manager { break; } } + if (message.isEndSession()) { + handleEndSession(envelope.getSource()); + } + if (message.getAttachments().isPresent()) { + for (TextSecureAttachment attachment : message.getAttachments().get()) { + if (attachment.isPointer()) { + try { + retrieveAttachment(attachment.asPointer()); + } catch (IOException | InvalidMessageException e) { + System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage()); + } + } + } + } } } } @@ -322,14 +368,18 @@ class Manager { } } - public File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException { + public File getAttachmentFile(long attachmentId) { + return new File(attachmentsPath + "/" + attachmentId); + } + + private File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException { final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); new File(attachmentsPath).mkdirs(); - File outputFile = new File(attachmentsPath + "/" + pointer.getId()); + File outputFile = getAttachmentFile(pointer.getId()); OutputStream output = null; try { output = new FileOutputStream(outputFile); @@ -374,12 +424,12 @@ class Manager { return PhoneNumberFormatter.formatNumber(number, localNumber); } - TextSecureAddress getPushAddress(String number) throws InvalidNumberException { + private TextSecureAddress getPushAddress(String number) throws InvalidNumberException { String e164number = canonicalizeNumber(number); return new TextSecureAddress(e164number); } - GroupInfo getGroupInfo(byte[] groupId) { + public GroupInfo getGroupInfo(byte[] groupId) { return groupStore.getGroup(groupId); } } From 09decf7916245fe1e031c8925b9f2128a449b150 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Nov 2015 14:38:47 +0100 Subject: [PATCH 0039/2005] Implement sending EndSession messages --- src/main/java/cli/Main.java | 80 +++++++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 199ea1eb..c4a6a4c0 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -95,35 +95,7 @@ public class Main { System.err.println("User is not registered."); System.exit(1); } - String messageText = ns.getString("message"); - if (messageText == null) { - try { - messageText = IOUtils.toString(System.in); - } catch (IOException e) { - System.err.println("Failed to read message from stdin: " + e.getMessage()); - System.err.println("Aborting sending."); - System.exit(1); - } - } - final List attachments = ns.getList("attachment"); - List textSecureAttachments = null; - if (attachments != null) { - textSecureAttachments = new ArrayList<>(attachments.size()); - for (String attachment : attachments) { - try { - File attachmentFile = new File(attachment); - InputStream attachmentStream = new FileInputStream(attachmentFile); - final long attachmentSize = attachmentFile.length(); - String mime = Files.probeContentType(Paths.get(attachment)); - textSecureAttachments.add(new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null)); - } catch (IOException e) { - System.err.println("Failed to add attachment \"" + attachment + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - System.exit(1); - } - } - } TextSecureGroup group = null; List recipients = null; if (ns.getString("group") != null) { @@ -145,7 +117,42 @@ public class Main { recipients = ns.getList("recipient"); } - sendMessage(m, messageText, textSecureAttachments, recipients, group); + if (ns.getBoolean("endsession")) { + sendEndSessionMessage(m, recipients); + } else { + final List attachments = ns.getList("attachment"); + List textSecureAttachments = null; + if (attachments != null) { + textSecureAttachments = new ArrayList<>(attachments.size()); + for (String attachment : attachments) { + try { + File attachmentFile = new File(attachment); + InputStream attachmentStream = new FileInputStream(attachmentFile); + final long attachmentSize = attachmentFile.length(); + String mime = Files.probeContentType(Paths.get(attachment)); + textSecureAttachments.add(new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null)); + } catch (IOException e) { + System.err.println("Failed to add attachment \"" + attachment + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); + } + } + } + + String messageText = ns.getString("message"); + if (messageText == null) { + try { + messageText = IOUtils.toString(System.in); + } catch (IOException e) { + System.err.println("Failed to read message from stdin: " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); + } + } + + sendMessage(m, messageText, textSecureAttachments, recipients, group); + } + break; case "receive": if (!m.isRegistered()) { @@ -216,6 +223,9 @@ public class Main { parserSend.addArgument("-a", "--attachment") .nargs("*") .help("Add file as attachment"); + parserSend.addArgument("-e", "--endsession") + .help("Clear session state and send end session message.") + .action(Arguments.storeTrue()); Subparser parserReceive = subparsers.addParser("receive"); parserReceive.addArgument("-t", "--timeout") @@ -251,6 +261,18 @@ public class Main { } TextSecureDataMessage message = messageBuilder.build(); + sendMessage(m, message, recipients); + } + + private static void sendEndSessionMessage(Manager m, List recipients) { + final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().asEndSessionMessage(); + + TextSecureDataMessage message = messageBuilder.build(); + + sendMessage(m, message, recipients); + } + + private static void sendMessage(Manager m, TextSecureDataMessage message, List recipients) { try { m.sendMessage(recipients, message); } catch (IOException e) { From 1689dfcb3857f2dd5ea74f9184b80d9499f534c0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Nov 2015 15:02:16 +0100 Subject: [PATCH 0040/2005] Implement quitGroups --- src/main/java/cli/Main.java | 52 ++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index c4a6a4c0..db2a941b 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -20,15 +20,12 @@ import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; import org.apache.commons.io.IOUtils; -import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException; import org.whispersystems.textsecure.api.messages.*; import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage; -import org.whispersystems.textsecure.api.push.TextSecureAddress; import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException; import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException; -import org.whispersystems.textsecure.api.util.InvalidNumberException; import java.io.File; import java.io.FileInputStream; @@ -96,7 +93,7 @@ public class Main { System.exit(1); } - TextSecureGroup group = null; + byte[] groupId = null; List recipients = null; if (ns.getString("group") != null) { try { @@ -106,7 +103,7 @@ public class Main { System.err.println("Aborting sending."); System.exit(1); } - group = new TextSecureGroup(g.groupId); + groupId = g.groupId; recipients = g.members; } catch (IOException e) { System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": " + e.getMessage()); @@ -150,7 +147,7 @@ public class Main { } } - sendMessage(m, messageText, textSecureAttachments, recipients, group); + sendMessage(m, messageText, textSecureAttachments, recipients, groupId); } break; @@ -179,6 +176,28 @@ public class Main { System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); System.exit(1); } + break; + case "quitGroup": + if (!m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); + } + + try { + GroupInfo g = m.getGroupInfo(Base64.decode(ns.getString("group"))); + if (g == null) { + System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": Unknown group"); + System.err.println("Aborting sending."); + System.exit(1); + } + + sendQuitGroupMessage(m, g.members, g.groupId); + } catch (IOException e) { + System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); + } + break; } m.save(); @@ -227,6 +246,11 @@ public class Main { .help("Clear session state and send end session message.") .action(Arguments.storeTrue()); + Subparser parserLeaveGroup = subparsers.addParser("quitGroup"); + parserLeaveGroup.addArgument("-g", "--group") + .required(true) + .help("Specify the recipient group ID."); + Subparser parserReceive = subparsers.addParser("receive"); parserReceive.addArgument("-t", "--timeout") .type(int.class) @@ -251,13 +275,13 @@ public class Main { } private static void sendMessage(Manager m, String messageText, List textSecureAttachments, - List recipients, TextSecureGroup group) { + List recipients, byte[] groupId) { final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); if (textSecureAttachments != null) { messageBuilder.withAttachments(textSecureAttachments); } - if (group != null) { - messageBuilder.asGroupMessage(group); + if (groupId != null) { + messageBuilder.asGroupMessage(new TextSecureGroup(groupId)); } TextSecureDataMessage message = messageBuilder.build(); @@ -272,6 +296,16 @@ public class Main { sendMessage(m, message, recipients); } + private static void sendQuitGroupMessage(Manager m, List recipients, byte[] groupId) { + final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder(); + TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.QUIT).withId(groupId).build(); + messageBuilder.asGroupMessage(group); + + TextSecureDataMessage message = messageBuilder.build(); + + sendMessage(m, message, recipients); + } + private static void sendMessage(Manager m, TextSecureDataMessage message, List recipients) { try { m.sendMessage(recipients, message); From fb862e4dde44caf0b5f32f6d55ea00e27146af0d Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Nov 2015 17:02:28 +0100 Subject: [PATCH 0041/2005] Implement updateGroup --- src/main/java/cli/GroupInfo.java | 10 ++- src/main/java/cli/Main.java | 121 ++++++++++++++++++++++++++++--- src/main/java/cli/Manager.java | 36 ++++++--- src/main/java/cli/Util.java | 2 +- 4 files changed, 144 insertions(+), 25 deletions(-) diff --git a/src/main/java/cli/GroupInfo.java b/src/main/java/cli/GroupInfo.java index f3060d08..fe31baf9 100644 --- a/src/main/java/cli/GroupInfo.java +++ b/src/main/java/cli/GroupInfo.java @@ -2,9 +2,9 @@ package cli; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.ArrayList; import java.util.Collection; -import java.util.List; +import java.util.HashSet; +import java.util.Set; public class GroupInfo { @JsonProperty @@ -14,11 +14,15 @@ public class GroupInfo { public String name; @JsonProperty - public List members = new ArrayList<>(); + public Set members = new HashSet<>(); @JsonProperty public long avatarId; + public GroupInfo(byte[] groupId) { + this.groupId = groupId; + } + public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId) { this.groupId = groupId; this.name = name; diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index db2a941b..8aedd958 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -26,6 +26,7 @@ import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMess import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException; import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.textsecure.api.util.InvalidNumberException; import java.io.File; import java.io.FileInputStream; @@ -99,14 +100,14 @@ public class Main { try { GroupInfo g = m.getGroupInfo(Base64.decode(ns.getString("group"))); if (g == null) { - System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": Unknown group"); + System.err.println("Failed to send to group \"" + ns.getString("group") + "\": Unknown group"); System.err.println("Aborting sending."); System.exit(1); } groupId = g.groupId; - recipients = g.members; + recipients = new ArrayList<>(g.members); } catch (IOException e) { - System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": " + e.getMessage()); + System.err.println("Failed to send to group \"" + ns.getString("group") + "\": " + e.getMessage()); System.err.println("Aborting sending."); System.exit(1); } @@ -123,11 +124,7 @@ public class Main { textSecureAttachments = new ArrayList<>(attachments.size()); for (String attachment : attachments) { try { - File attachmentFile = new File(attachment); - InputStream attachmentStream = new FileInputStream(attachmentFile); - final long attachmentSize = attachmentFile.length(); - String mime = Files.probeContentType(Paths.get(attachment)); - textSecureAttachments.add(new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null)); + textSecureAttachments.add(createAttachment(attachment)); } catch (IOException e) { System.err.println("Failed to add attachment \"" + attachment + "\": " + e.getMessage()); System.err.println("Aborting sending."); @@ -186,14 +183,82 @@ public class Main { try { GroupInfo g = m.getGroupInfo(Base64.decode(ns.getString("group"))); if (g == null) { - System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": Unknown group"); + System.err.println("Failed to send to group \"" + ns.getString("group") + "\": Unknown group"); System.err.println("Aborting sending."); System.exit(1); } - sendQuitGroupMessage(m, g.members, g.groupId); + sendQuitGroupMessage(m, new ArrayList<>(g.members), g.groupId); } catch (IOException e) { - System.err.println("Failed to send to grup \"" + ns.getString("group") + "\": " + e.getMessage()); + System.err.println("Failed to send to group \"" + ns.getString("group") + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); + } + break; + case "updateGroup": + if (!m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); + } + + try { + GroupInfo g; + if (ns.getString("group") != null) { + g = m.getGroupInfo(Base64.decode(ns.getString("group"))); + if (g == null) { + System.err.println("Failed to send to group \"" + ns.getString("group") + "\": Unknown group"); + System.err.println("Aborting sending."); + System.exit(1); + } + } else { + // Create new group + g = new GroupInfo(Util.getSecretBytes(16)); + g.members.add(m.getUsername()); + System.out.println("Creating new group \"" + Base64.encodeBytes(g.groupId) + "\" …"); + } + + String name = ns.getString("name"); + if (name != null) { + g.name = name; + } + + final List members = ns.getList("member"); + + if (members != null) { + for (String member : members) { + try { + g.members.add(m.canonicalizeNumber(member)); + } catch (InvalidNumberException e) { + System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage()); + System.err.println("Aborting…"); + System.exit(1); + } + } + } + + TextSecureGroup.Builder group = TextSecureGroup.newBuilder(TextSecureGroup.Type.UPDATE) + .withId(g.groupId) + .withName(g.name) + .withMembers(new ArrayList<>(g.members)); + + String avatar = ns.getString("avatar"); + if (avatar != null) { + try { + group.withAvatar(createAttachment(avatar)); + // TODO + g.avatarId = 0; + } catch (IOException e) { + System.err.println("Failed to add attachment \"" + avatar + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); + } + } + + m.setGroupInfo(g); + + sendUpdateGroupMessage(m, group.build()); + } catch (IOException e) { + System.err.println("Failed to send to group \"" + ns.getString("group") + "\": " + e.getMessage()); System.err.println("Aborting sending."); System.exit(1); } @@ -204,6 +269,14 @@ public class Main { System.exit(0); } + private static TextSecureAttachmentStream createAttachment(String attachment) throws IOException { + File attachmentFile = new File(attachment); + InputStream attachmentStream = new FileInputStream(attachmentFile); + final long attachmentSize = attachmentFile.length(); + String mime = Files.probeContentType(Paths.get(attachment)); + return new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null); + } + private static Namespace parseArgs(String[] args) { ArgumentParser parser = ArgumentParsers.newArgumentParser("textsecure-cli") .defaultHelp(true) @@ -251,6 +324,17 @@ public class Main { .required(true) .help("Specify the recipient group ID."); + Subparser parserUpdateGroup = subparsers.addParser("updateGroup"); + parserUpdateGroup.addArgument("-g", "--group") + .help("Specify the recipient group ID."); + parserUpdateGroup.addArgument("-n", "--name") + .help("Specify the new group name."); + parserUpdateGroup.addArgument("-a", "--avatar") + .help("Specify a new group avatar image file"); + parserUpdateGroup.addArgument("-m", "--member") + .nargs("*") + .help("Specify one or more members to add to the group"); + Subparser parserReceive = subparsers.addParser("receive"); parserReceive.addArgument("-t", "--timeout") .type(int.class) @@ -298,7 +382,10 @@ public class Main { private static void sendQuitGroupMessage(Manager m, List recipients, byte[] groupId) { final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder(); - TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.QUIT).withId(groupId).build(); + TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.QUIT) + .withId(groupId) + .build(); + messageBuilder.asGroupMessage(group); TextSecureDataMessage message = messageBuilder.build(); @@ -306,6 +393,16 @@ public class Main { sendMessage(m, message, recipients); } + private static void sendUpdateGroupMessage(Manager m, TextSecureGroup g) { + final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder(); + + messageBuilder.asGroupMessage(g); + + TextSecureDataMessage message = messageBuilder.build(); + + sendMessage(m, message, g.getMembers().get()); + } + private static void sendMessage(Manager m, TextSecureDataMessage message, List recipients) { try { m.sendMessage(recipients, message); diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index a1d58f03..a0fb5b01 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -44,9 +44,7 @@ import org.whispersystems.textsecure.api.util.InvalidNumberException; import org.whispersystems.textsecure.api.util.PhoneNumberFormatter; import java.io.*; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -248,7 +246,7 @@ class Manager { TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password, axolotlStore, USER_AGENT, Optional.absent()); - List recipientsTS = new ArrayList<>(recipients.size()); + Set recipientsTS = new HashSet<>(recipients.size()); for (String recipient : recipients) { try { recipientsTS.add(getPushAddress(recipient)); @@ -259,7 +257,7 @@ class Manager { } } - messageSender.sendMessage(recipientsTS, message); + messageSender.sendMessage(new ArrayList<>(recipientsTS), message); if (message.isEndSession()) { for (TextSecureAddress recipient : recipientsTS) { @@ -309,20 +307,32 @@ class Manager { TextSecureGroup groupInfo = message.getGroupInfo().get(); switch (groupInfo.getType()) { case UPDATE: - long avatarId = 0; + group = groupStore.getGroup(groupInfo.getGroupId()); + if (group == null) { + group = new GroupInfo(groupInfo.getGroupId()); + } + if (groupInfo.getAvatar().isPresent()) { TextSecureAttachment avatar = groupInfo.getAvatar().get(); if (avatar.isPointer()) { - avatarId = avatar.asPointer().getId(); + long avatarId = avatar.asPointer().getId(); try { retrieveAttachment(avatar.asPointer()); + group.avatarId = avatarId; } catch (IOException | InvalidMessageException e) { System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage()); } } } - group = new GroupInfo(groupInfo.getGroupId(), groupInfo.getName().get(), groupInfo.getMembers().get(), avatarId); + if (groupInfo.getName().isPresent()) { + group.name = groupInfo.getName().get(); + } + + if (groupInfo.getMembers().isPresent()) { + group.members.addAll(groupInfo.getMembers().get()); + } + groupStore.updateGroup(group); break; case DELIVER: @@ -419,7 +429,7 @@ class Manager { return outputFile; } - private String canonicalizeNumber(String number) throws InvalidNumberException { + public String canonicalizeNumber(String number) throws InvalidNumberException { String localNumber = username; return PhoneNumberFormatter.formatNumber(number, localNumber); } @@ -432,4 +442,12 @@ class Manager { public GroupInfo getGroupInfo(byte[] groupId) { return groupStore.getGroup(groupId); } + + public void setGroupInfo(GroupInfo group) { + groupStore.updateGroup(group); + } + + public String getUsername() { + return username; + } } diff --git a/src/main/java/cli/Util.java b/src/main/java/cli/Util.java index 907c4ed1..215e14b8 100644 --- a/src/main/java/cli/Util.java +++ b/src/main/java/cli/Util.java @@ -9,7 +9,7 @@ class Util { return Base64.encodeBytes(secret); } - private static byte[] getSecretBytes(int size) { + public static byte[] getSecretBytes(int size) { byte[] secret = new byte[size]; getSecureRandom().nextBytes(secret); return secret; From 95fdff18f51e4897d776c4a0139818c89244fa67 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Nov 2015 17:36:54 +0100 Subject: [PATCH 0042/2005] Update README --- README.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index dcd69a0c..8790fc8d 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,49 @@ # textsecure-cli -textsecure-cli is a commandline interface for [libtextsecure-java](https://github.com/WhisperSystems/libtextsecure-java). It supports registering, verifying, sending and receiving messages. However receiving messages currently doesn't work, because libtextsecure-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libtextsecure-java/pull/5). For registering you need a phone number where you can receive SMS or incoming calls. +textsecure-cli is a commandline interface for [libtextsecure-java](https://github.com/WhisperSystems/libtextsecure-java). It supports registering, verifying, sending and receiving messages. However receiving messages currently only works with a patched libtextsecure-java, because libtextsecure-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libtextsecure-java/pull/5). For registering you need a phone number where you can receive SMS or incoming calls. It is primarily intended to be used on servers to notify admins of important events. ## Usage -usage: textsecure-cli [-h] -u USERNAME {register,verify,send,receive} ... +usage: textsecure-cli [-h] [-u USERNAME] [-v] {register,verify,send,quitGroup,updateGroup,receive} ... -* Register a number +* Register a number (with SMS verification) textsecure-cli -u USERNAME register -* Register a number with voice verification +* Register a number (with voice verification) textsecure-cli -u USERNAME register -v -* Verify the number using the code received via SMS +* Verify the number using the code received via SMS or voice textsecure-cli -u USERNAME verify CODE * Send a message to one or more recipients - textsecure-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] + textsecure-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] * Pipe the message content from another process. uname -a | textsecure-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]] +* Groups + + * Create a group + + textsecure-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]] + + * Update a group + + textsecure-cli -u USERNAME updateGroup -g GROUP_ID -n "New group name" + + * Send a message to a group + + textsecure-cli -u USERNAME send -m "This is a message" -g GROUP_ID + ## Storage -The password and cryptographic keys are created when registering and stored in the current users home directory. +The password and cryptographic keys are created when registering and stored in the current users home directory: $HOME/.config/textsecure/data/ From eeba9687cb9d4d891c21cf181de2e828546dfc32 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 28 Nov 2015 01:05:45 +0100 Subject: [PATCH 0043/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1bf3b6e7..ec4925c7 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ sourceCompatibility = "1.8"; mainClassName = 'cli.Main' -version = '0.0.5' +version = '0.1.0' repositories { mavenCentral() From 9b7e9cc61ed2b6bb06c7519fa567011f0dc90ca6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 6 Dec 2015 15:06:27 +0100 Subject: [PATCH 0044/2005] Set required java version to 1.7 --- build.gradle | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ec4925c7..c56e0d85 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,8 @@ apply plugin: 'java' apply plugin: 'application' -sourceCompatibility = "1.8"; +sourceCompatibility = JavaVersion.VERSION_1_7 +targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'cli.Main' From b6684906fc5b6a089f2bbb973d53fb8898c6cdb1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 6 Dec 2015 15:45:31 +0100 Subject: [PATCH 0045/2005] Fix groups for upgraded clients --- src/main/java/cli/Manager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index a0fb5b01..cc81c14d 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -128,6 +128,9 @@ class Manager { if (groupStoreNode != null) { groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class); } + if (groupStore == null) { + groupStore = new JsonGroupStore(); + } accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); } From c1abc12907167618c58282daf5315c5c694a8136 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 6 Dec 2015 17:22:39 +0100 Subject: [PATCH 0046/2005] Check if the username is a valid phone number with country code --- src/main/java/cli/Main.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 8aedd958..43535749 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -27,6 +27,7 @@ import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException; import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException; import org.whispersystems.textsecure.api.util.InvalidNumberException; +import org.whispersystems.textsecure.api.util.PhoneNumberFormatter; import java.io.File; import java.io.FileInputStream; @@ -347,6 +348,10 @@ public class Main { System.err.println("You need to specify a username (phone number)"); System.exit(2); } + if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) { + System.err.println("Invalid username (phone number), make sure you include the country code."); + System.exit(2); + } if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) { System.err.println("You cannot specify recipients by phone number and groups a the same time"); System.exit(2); From 4b5bfcba8001d23768ad1131b76eaf6325d260d4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 10 Dec 2015 21:42:44 +0100 Subject: [PATCH 0047/2005] Extract getTextSecureAttachments method --- src/main/java/cli/Main.java | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 43535749..08c7b935 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -119,19 +119,13 @@ public class Main { if (ns.getBoolean("endsession")) { sendEndSessionMessage(m, recipients); } else { - final List attachments = ns.getList("attachment"); List textSecureAttachments = null; - if (attachments != null) { - textSecureAttachments = new ArrayList<>(attachments.size()); - for (String attachment : attachments) { - try { - textSecureAttachments.add(createAttachment(attachment)); - } catch (IOException e) { - System.err.println("Failed to add attachment \"" + attachment + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - System.exit(1); - } - } + try { + textSecureAttachments = getTextSecureAttachments(ns.getList("attachment")); + } catch (IOException e) { + System.err.println("Failed to add attachment: " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); } String messageText = ns.getString("message"); @@ -270,6 +264,18 @@ public class Main { System.exit(0); } + private static List getTextSecureAttachments(List attachments) { + private static List getTextSecureAttachments(List attachments) throws IOException { + List textSecureAttachments = null; + if (attachments != null) { + textSecureAttachments = new ArrayList<>(attachments.size()); + for (String attachment : attachments) { + textSecureAttachments.add(createAttachment(attachment)); + } + } + return textSecureAttachments; + } + private static TextSecureAttachmentStream createAttachment(String attachment) throws IOException { File attachmentFile = new File(attachment); InputStream attachmentStream = new FileInputStream(attachmentFile); From ef5d3a65f8de2ae8e17adfb7c307d88b0c04778a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 12 Dec 2015 11:38:15 +0100 Subject: [PATCH 0048/2005] Refactoring, move more functionality into Manager --- .../java/cli/AttachmentInvalidException.java | 14 + src/main/java/cli/GroupNotFoundException.java | 14 + src/main/java/cli/JsonGroupStore.java | 8 +- src/main/java/cli/Main.java | 266 ++++++------------ src/main/java/cli/Manager.java | 146 +++++++++- 5 files changed, 258 insertions(+), 190 deletions(-) create mode 100644 src/main/java/cli/AttachmentInvalidException.java create mode 100644 src/main/java/cli/GroupNotFoundException.java diff --git a/src/main/java/cli/AttachmentInvalidException.java b/src/main/java/cli/AttachmentInvalidException.java new file mode 100644 index 00000000..289855e9 --- /dev/null +++ b/src/main/java/cli/AttachmentInvalidException.java @@ -0,0 +1,14 @@ +package cli; + +public class AttachmentInvalidException extends Exception { + private final String attachment; + + public AttachmentInvalidException(String attachment, Exception e) { + super(e); + this.attachment = attachment; + } + + public String getAttachment() { + return attachment; + } +} diff --git a/src/main/java/cli/GroupNotFoundException.java b/src/main/java/cli/GroupNotFoundException.java new file mode 100644 index 00000000..85e1cf0b --- /dev/null +++ b/src/main/java/cli/GroupNotFoundException.java @@ -0,0 +1,14 @@ +package cli; + +public class GroupNotFoundException extends Exception { + private final byte[] groupId; + + public GroupNotFoundException(byte[] groupId) { + super(); + this.groupId = groupId; + } + + public byte[] getGroupId() { + return groupId; + } +} diff --git a/src/main/java/cli/JsonGroupStore.java b/src/main/java/cli/JsonGroupStore.java index 29f9abfe..0fbfdc85 100644 --- a/src/main/java/cli/JsonGroupStore.java +++ b/src/main/java/cli/JsonGroupStore.java @@ -23,8 +23,12 @@ public class JsonGroupStore { groups.put(Base64.encodeBytes(group.groupId), group); } - GroupInfo getGroup(byte[] groupId) { - return groups.get(Base64.encodeBytes(groupId)); + GroupInfo getGroup(byte[] groupId) throws GroupNotFoundException { + GroupInfo g = groups.get(Base64.encodeBytes(groupId)); + if (g == null) { + throw new GroupNotFoundException(groupId); + } + return g; } public static class MapToListSerializer extends JsonSerializer> { diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 08c7b935..49975daa 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -26,18 +26,11 @@ import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMess import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException; import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException; -import org.whispersystems.textsecure.api.util.InvalidNumberException; import org.whispersystems.textsecure.api.util.PhoneNumberFormatter; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Paths; import java.security.Security; -import java.util.ArrayList; -import java.util.List; public class Main { @@ -95,39 +88,22 @@ public class Main { System.exit(1); } - byte[] groupId = null; - List recipients = null; - if (ns.getString("group") != null) { - try { - GroupInfo g = m.getGroupInfo(Base64.decode(ns.getString("group"))); - if (g == null) { - System.err.println("Failed to send to group \"" + ns.getString("group") + "\": Unknown group"); - System.err.println("Aborting sending."); - System.exit(1); - } - groupId = g.groupId; - recipients = new ArrayList<>(g.members); - } catch (IOException e) { - System.err.println("Failed to send to group \"" + ns.getString("group") + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - System.exit(1); - } - } else { - recipients = ns.getList("recipient"); - } - if (ns.getBoolean("endsession")) { - sendEndSessionMessage(m, recipients); - } else { - List textSecureAttachments = null; - try { - textSecureAttachments = getTextSecureAttachments(ns.getList("attachment")); - } catch (IOException e) { - System.err.println("Failed to add attachment: " + e.getMessage()); + if (ns.getList("recipient") == null) { + System.err.println("No recipients given"); System.err.println("Aborting sending."); System.exit(1); } - + try { + m.sendEndSessionMessage(ns.getList("recipient")); + } catch (IOException e) { + handleIOException(e); + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + } catch (AssertionError e) { + handleAssertionError(e); + } + } else { String messageText = ns.getString("message"); if (messageText == null) { try { @@ -139,7 +115,26 @@ public class Main { } } - sendMessage(m, messageText, textSecureAttachments, recipients, groupId); + try { + if (ns.getString("group") != null) { + byte[] groupId = decodeGroupId(ns.getString("group")); + m.sendGroupMessage(messageText, ns.getList("attachment"), groupId); + } else { + m.sendMessage(messageText, ns.getList("attachment"), ns.getList("recipient")); + } + } catch (IOException e) { + handleIOException(e); + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + } catch (AssertionError e) { + handleAssertionError(e); + } catch (GroupNotFoundException e) { + handleGroupNotFoundException(e); + } catch (AttachmentInvalidException e) { + System.err.println("Failed to add attachment (\"" + e.getAttachment() + "\"): " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); + } } break; @@ -176,19 +171,17 @@ public class Main { } try { - GroupInfo g = m.getGroupInfo(Base64.decode(ns.getString("group"))); - if (g == null) { - System.err.println("Failed to send to group \"" + ns.getString("group") + "\": Unknown group"); - System.err.println("Aborting sending."); - System.exit(1); - } - - sendQuitGroupMessage(m, new ArrayList<>(g.members), g.groupId); + m.sendQuitGroupMessage(decodeGroupId(ns.getString("group"))); } catch (IOException e) { - System.err.println("Failed to send to group \"" + ns.getString("group") + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - System.exit(1); + handleIOException(e); + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + } catch (AssertionError e) { + handleAssertionError(e); + } catch (GroupNotFoundException e) { + handleGroupNotFoundException(e); } + break; case "updateGroup": if (!m.isRegistered()) { @@ -197,65 +190,24 @@ public class Main { } try { - GroupInfo g; + byte[] groupId = null; if (ns.getString("group") != null) { - g = m.getGroupInfo(Base64.decode(ns.getString("group"))); - if (g == null) { - System.err.println("Failed to send to group \"" + ns.getString("group") + "\": Unknown group"); - System.err.println("Aborting sending."); - System.exit(1); - } - } else { - // Create new group - g = new GroupInfo(Util.getSecretBytes(16)); - g.members.add(m.getUsername()); - System.out.println("Creating new group \"" + Base64.encodeBytes(g.groupId) + "\" …"); + groupId = decodeGroupId(ns.getString("group")); } - - String name = ns.getString("name"); - if (name != null) { - g.name = name; + byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.getList("member"), ns.getString("avatar")); + if (groupId == null) { + System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …"); } - - final List members = ns.getList("member"); - - if (members != null) { - for (String member : members) { - try { - g.members.add(m.canonicalizeNumber(member)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage()); - System.err.println("Aborting…"); - System.exit(1); - } - } - } - - TextSecureGroup.Builder group = TextSecureGroup.newBuilder(TextSecureGroup.Type.UPDATE) - .withId(g.groupId) - .withName(g.name) - .withMembers(new ArrayList<>(g.members)); - - String avatar = ns.getString("avatar"); - if (avatar != null) { - try { - group.withAvatar(createAttachment(avatar)); - // TODO - g.avatarId = 0; - } catch (IOException e) { - System.err.println("Failed to add attachment \"" + avatar + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - System.exit(1); - } - } - - m.setGroupInfo(g); - - sendUpdateGroupMessage(m, group.build()); } catch (IOException e) { - System.err.println("Failed to send to group \"" + ns.getString("group") + "\": " + e.getMessage()); + handleIOException(e); + } catch (AttachmentInvalidException e) { + System.err.println("Failed to add avatar attachment (\"" + e.getAttachment() + ") for group\": " + e.getMessage()); System.err.println("Aborting sending."); System.exit(1); + } catch (GroupNotFoundException e) { + handleGroupNotFoundException(e); + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); } break; @@ -264,24 +216,21 @@ public class Main { System.exit(0); } - private static List getTextSecureAttachments(List attachments) { - private static List getTextSecureAttachments(List attachments) throws IOException { - List textSecureAttachments = null; - if (attachments != null) { - textSecureAttachments = new ArrayList<>(attachments.size()); - for (String attachment : attachments) { - textSecureAttachments.add(createAttachment(attachment)); - } - } - return textSecureAttachments; + private static void handleGroupNotFoundException(GroupNotFoundException e) { + System.err.println("Failed to send to group \"" + Base64.encodeBytes(e.getGroupId()) + "\": Unknown group"); + System.err.println("Aborting sending."); + System.exit(1); } - private static TextSecureAttachmentStream createAttachment(String attachment) throws IOException { - File attachmentFile = new File(attachment); - InputStream attachmentStream = new FileInputStream(attachmentFile); - final long attachmentSize = attachmentFile.length(); - String mime = Files.probeContentType(Paths.get(attachment)); - return new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null); + private static byte[] decodeGroupId(String groupId) { + try { + return Base64.decode(groupId); + } catch (IOException e) { + System.err.println("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); + return null; + } } private static Namespace parseArgs(String[] args) { @@ -369,75 +318,30 @@ public class Main { } } - private static void sendMessage(Manager m, String messageText, List textSecureAttachments, - List recipients, byte[] groupId) { - final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); - if (textSecureAttachments != null) { - messageBuilder.withAttachments(textSecureAttachments); + private static void handleAssertionError(AssertionError e) { + System.err.println("Failed to send message (Assertion): " + e.getMessage()); + System.err.println(e.getStackTrace()); + System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); + System.exit(1); + } + + private static void handleEncapsulatedExceptions(EncapsulatedExceptions e) { + System.err.println("Failed to send (some) messages:"); + for (NetworkFailureException n : e.getNetworkExceptions()) { + System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage()); } - if (groupId != null) { - messageBuilder.asGroupMessage(new TextSecureGroup(groupId)); + for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) { + System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage()); } - TextSecureDataMessage message = messageBuilder.build(); - - sendMessage(m, message, recipients); - } - - private static void sendEndSessionMessage(Manager m, List recipients) { - final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().asEndSessionMessage(); - - TextSecureDataMessage message = messageBuilder.build(); - - sendMessage(m, message, recipients); - } - - private static void sendQuitGroupMessage(Manager m, List recipients, byte[] groupId) { - final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder(); - TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.QUIT) - .withId(groupId) - .build(); - - messageBuilder.asGroupMessage(group); - - TextSecureDataMessage message = messageBuilder.build(); - - sendMessage(m, message, recipients); - } - - private static void sendUpdateGroupMessage(Manager m, TextSecureGroup g) { - final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder(); - - messageBuilder.asGroupMessage(g); - - TextSecureDataMessage message = messageBuilder.build(); - - sendMessage(m, message, g.getMembers().get()); - } - - private static void sendMessage(Manager m, TextSecureDataMessage message, List recipients) { - try { - m.sendMessage(recipients, message); - } catch (IOException e) { - System.err.println("Failed to send message: " + e.getMessage()); - } catch (EncapsulatedExceptions e) { - System.err.println("Failed to send (some) messages:"); - for (NetworkFailureException n : e.getNetworkExceptions()) { - System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage()); - } - for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) { - System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage()); - } - for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) { - System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage()); - } - } catch (AssertionError e) { - System.err.println("Failed to send message (Assertion): " + e.getMessage()); - System.err.println(e.getStackTrace()); - System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); - System.exit(1); + for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) { + System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage()); } } + private static void handleIOException(IOException e) { + System.err.println("Failed to send message: " + e.getMessage()); + } + private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final Manager m; diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index cc81c14d..730923ad 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -44,6 +44,8 @@ import org.whispersystems.textsecure.api.util.InvalidNumberException; import org.whispersystems.textsecure.api.util.PhoneNumberFormatter; import java.io.*; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -244,7 +246,132 @@ class Manager { accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); } - public void sendMessage(List recipients, TextSecureDataMessage message) + + private static List getTextSecureAttachments(List attachments) throws AttachmentInvalidException { + List textSecureAttachments = null; + if (attachments != null) { + textSecureAttachments = new ArrayList<>(attachments.size()); + for (String attachment : attachments) { + try { + textSecureAttachments.add(createAttachment(attachment)); + } catch (IOException e) { + throw new AttachmentInvalidException(attachment, e); + } + } + } + return textSecureAttachments; + } + + private static TextSecureAttachmentStream createAttachment(String attachment) throws IOException { + File attachmentFile = new File(attachment); + InputStream attachmentStream = new FileInputStream(attachmentFile); + final long attachmentSize = attachmentFile.length(); + String mime = Files.probeContentType(Paths.get(attachment)); + return new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null); + } + + public void sendGroupMessage(String messageText, List attachments, + byte[] groupId) + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); + if (attachments != null) { + messageBuilder.withAttachments(getTextSecureAttachments(attachments)); + } + if (groupId != null) { + TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.DELIVER) + .withId(groupId) + .build(); + messageBuilder.asGroupMessage(group); + } + TextSecureDataMessage message = messageBuilder.build(); + + sendMessage(message, getGroupInfo(groupId).members); + } + + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { + TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.QUIT) + .withId(groupId) + .build(); + + TextSecureDataMessage message = TextSecureDataMessage.newBuilder() + .asGroupMessage(group) + .build(); + + sendMessage(message, getGroupInfo(groupId).members); + } + + public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + GroupInfo g; + if (groupId == null) { + // Create new group + g = new GroupInfo(Util.getSecretBytes(16)); + g.members.add(getUsername()); + } else { + g = getGroupInfo(groupId); + } + + if (name != null) { + g.name = name; + } + + if (members != null) { + for (String member : members) { + try { + g.members.add(canonicalizeNumber(member)); + } catch (InvalidNumberException e) { + System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage()); + System.err.println("Aborting…"); + System.exit(1); + } + } + } + + TextSecureGroup.Builder group = TextSecureGroup.newBuilder(TextSecureGroup.Type.UPDATE) + .withId(g.groupId) + .withName(g.name) + .withMembers(new ArrayList<>(g.members)); + + if (avatarFile != null) { + try { + group.withAvatar(createAttachment(avatarFile)); + // TODO + g.avatarId = 0; + } catch (IOException e) { + throw new AttachmentInvalidException(avatarFile, e); + } + } + + setGroupInfo(g); + + TextSecureDataMessage message = TextSecureDataMessage.newBuilder() + .asGroupMessage(group.build()) + .build(); + + sendMessage(message, g.members); + return g.groupId; + } + + public void sendMessage(String messageText, List attachments, + Collection recipients) + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); + if (attachments != null) { + messageBuilder.withAttachments(getTextSecureAttachments(attachments)); + } + TextSecureDataMessage message = messageBuilder.build(); + + sendMessage(message, recipients); + } + + public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { + TextSecureDataMessage message = TextSecureDataMessage.newBuilder() + .asEndSessionMessage() + .build(); + + sendMessage(message, recipients); + } + + private void sendMessage(TextSecureDataMessage message, Collection recipients) throws IOException, EncapsulatedExceptions { TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password, axolotlStore, USER_AGENT, Optional.absent()); @@ -310,8 +437,9 @@ class Manager { TextSecureGroup groupInfo = message.getGroupInfo().get(); switch (groupInfo.getType()) { case UPDATE: - group = groupStore.getGroup(groupInfo.getGroupId()); - if (group == null) { + try { + group = groupStore.getGroup(groupInfo.getGroupId()); + } catch (GroupNotFoundException e) { group = new GroupInfo(groupInfo.getGroupId()); } @@ -339,12 +467,16 @@ class Manager { groupStore.updateGroup(group); break; case DELIVER: - group = groupStore.getGroup(groupInfo.getGroupId()); + try { + group = groupStore.getGroup(groupInfo.getGroupId()); + } catch (GroupNotFoundException e) { + } break; case QUIT: - group = groupStore.getGroup(groupInfo.getGroupId()); - if (group != null) { + try { + group = groupStore.getGroup(groupInfo.getGroupId()); group.members.remove(envelope.getSource()); + } catch (GroupNotFoundException e) { } break; } @@ -442,7 +574,7 @@ class Manager { return new TextSecureAddress(e164number); } - public GroupInfo getGroupInfo(byte[] groupId) { + public GroupInfo getGroupInfo(byte[] groupId) throws GroupNotFoundException { return groupStore.getGroup(groupId); } From 80cc0cad92949d6f7a9c4de6a92b4a1ffe9d6eaa Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 12 Dec 2015 20:05:14 +0100 Subject: [PATCH 0049/2005] Fix build with Java 7 --- src/main/java/cli/Main.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 49975daa..ae2f23fa 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -95,7 +95,7 @@ public class Main { System.exit(1); } try { - m.sendEndSessionMessage(ns.getList("recipient")); + m.sendEndSessionMessage(ns.getList("recipient")); } catch (IOException e) { handleIOException(e); } catch (EncapsulatedExceptions e) { @@ -120,7 +120,7 @@ public class Main { byte[] groupId = decodeGroupId(ns.getString("group")); m.sendGroupMessage(messageText, ns.getList("attachment"), groupId); } else { - m.sendMessage(messageText, ns.getList("attachment"), ns.getList("recipient")); + m.sendMessage(messageText, ns.getList("attachment"), ns.getList("recipient")); } } catch (IOException e) { handleIOException(e); @@ -194,7 +194,7 @@ public class Main { if (ns.getString("group") != null) { groupId = decodeGroupId(ns.getString("group")); } - byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.getList("member"), ns.getString("avatar")); + byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.getList("member"), ns.getString("avatar")); if (groupId == null) { System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …"); } From 845e93ec0f8931b7f501318853b5750c37721621 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 12 Dec 2015 20:05:44 +0100 Subject: [PATCH 0050/2005] Cleanup --- src/main/java/cli/Manager.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 730923ad..2a1ddb22 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -305,7 +305,7 @@ class Manager { if (groupId == null) { // Create new group g = new GroupInfo(Util.getSecretBytes(16)); - g.members.add(getUsername()); + g.members.add(username); } else { g = getGroupInfo(groupId); } @@ -564,7 +564,7 @@ class Manager { return outputFile; } - public String canonicalizeNumber(String number) throws InvalidNumberException { + private String canonicalizeNumber(String number) throws InvalidNumberException { String localNumber = username; return PhoneNumberFormatter.formatNumber(number, localNumber); } @@ -574,15 +574,11 @@ class Manager { return new TextSecureAddress(e164number); } - public GroupInfo getGroupInfo(byte[] groupId) throws GroupNotFoundException { + private GroupInfo getGroupInfo(byte[] groupId) throws GroupNotFoundException { return groupStore.getGroup(groupId); } - public void setGroupInfo(GroupInfo group) { + private void setGroupInfo(GroupInfo group) { groupStore.updateGroup(group); } - - public String getUsername() { - return username; - } } From 9a1b348ed2d31ac63bf0ef6f64ad92a2f16d173c Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 12 Dec 2015 21:30:24 +0100 Subject: [PATCH 0051/2005] Implement daemon mode with dbus interface --- build.gradle | 4 +++ src/main/java/cli/Main.java | 45 ++++++++++++++++++++++++++----- src/main/java/cli/Manager.java | 21 ++++++++++++--- src/main/java/cli/TextSecure.java | 13 +++++++++ 4 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 src/main/java/cli/TextSecure.java diff --git a/build.gradle b/build.gradle index c56e0d85..19893391 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,9 @@ mainClassName = 'cli.Main' version = '0.1.0' repositories { + maven { + url "https://raw.github.com/AsamK/maven/master/releases/" + } mavenCentral() } @@ -17,6 +20,7 @@ dependencies { compile 'com.madgag.spongycastle:prov:1.53.0.0' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.5.0' + compile 'org.freedesktop.dbus:dbus-java:2.7.0' } jar { diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index ae2f23fa..64bdfae3 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -20,6 +20,8 @@ import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; import org.apache.commons.io.IOUtils; +import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException; import org.whispersystems.textsecure.api.messages.*; import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage; @@ -155,13 +157,10 @@ public class Main { try { m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m)); } catch (IOException e) { - System.err.println("Error while receiving message: " + e.getMessage()); + System.err.println("Error while receiving messages: " + e.getMessage()); System.exit(3); } catch (AssertionError e) { - System.err.println("Failed to receive message (Assertion): " + e.getMessage()); - System.err.println(e.getStackTrace()); - System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); - System.exit(1); + handleAssertionError(e); } break; case "quitGroup": @@ -210,6 +209,35 @@ public class Main { handleEncapsulatedExceptions(e); } + break; + case "daemon": + if (!m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); + } + try { + int busType; + if (ns.getBoolean("system")) { + busType = DBusConnection.SYSTEM; + } else { + busType = DBusConnection.SESSION; + } + DBusConnection conn = DBusConnection.getConnection(busType); + conn.requestBusName("org.asamk.TextSecure"); + conn.exportObject("/org/asamk/TextSecure", m); + } catch (DBusException e) { + e.printStackTrace(); + System.exit(3); + } + try { + m.receiveMessages(3600, false, new ReceiveMessageHandler(m)); + } catch (IOException e) { + System.err.println("Error while receiving messages: " + e.getMessage()); + System.exit(3); + } catch (AssertionError e) { + handleAssertionError(e); + } + break; } m.save(); @@ -296,6 +324,11 @@ public class Main { .type(int.class) .help("Number of seconds to wait for new messages (negative values disable timeout)"); + Subparser parserDaemon = subparsers.addParser("daemon"); + parserDaemon.addArgument("--system") + .action(Arguments.storeTrue()) + .help("Use DBus system bus instead of user bus."); + try { Namespace ns = parser.parseArgs(args); if (ns.getString("username") == null) { @@ -319,7 +352,7 @@ public class Main { } private static void handleAssertionError(AssertionError e) { - System.err.println("Failed to send message (Assertion): " + e.getMessage()); + System.err.println("Failed to send/receive message (Assertion): " + e.getMessage()); System.err.println(e.getStackTrace()); System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); System.exit(1); diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 2a1ddb22..b15457af 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -50,7 +50,7 @@ import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -class Manager { +class Manager implements TextSecure { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); @@ -270,6 +270,7 @@ class Manager { return new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null); } + @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { @@ -351,9 +352,17 @@ class Manager { return g.groupId; } + @Override + public void sendMessage(String message, List attachments, String recipient) + throws EncapsulatedExceptions, AttachmentInvalidException, IOException { + List recipients = new ArrayList<>(1); + recipients.add(recipient); + sendMessage(message, attachments, recipients); + } + public void sendMessage(String messageText, List attachments, Collection recipients) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException { final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getTextSecureAttachments(attachments)); @@ -394,6 +403,7 @@ class Manager { handleEndSession(recipient.getNumber()); } } + save(); } private TextSecureContent decryptMessage(TextSecureEnvelope envelope) { @@ -498,6 +508,7 @@ class Manager { } } } + save(); handler.handleMessage(envelope, content, group); } catch (TimeoutException e) { if (returnOnTimeout) @@ -505,7 +516,6 @@ class Manager { } catch (InvalidVersionException e) { System.err.println("Ignoring error: " + e.getMessage()); } - save(); } } finally { if (messagePipe != null) @@ -581,4 +591,9 @@ class Manager { private void setGroupInfo(GroupInfo group) { groupStore.updateGroup(group); } + + @Override + public boolean isRemote() { + return false; + } } diff --git a/src/main/java/cli/TextSecure.java b/src/main/java/cli/TextSecure.java new file mode 100644 index 00000000..011e696f --- /dev/null +++ b/src/main/java/cli/TextSecure.java @@ -0,0 +1,13 @@ +package cli; + +import org.freedesktop.dbus.DBusInterface; +import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; + +import java.io.IOException; +import java.util.List; + +public interface TextSecure extends DBusInterface { + void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; + + void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; +} From 208e12bdc6bfcfd141d46a40b105d2559340ead0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 12 Dec 2015 22:09:06 +0100 Subject: [PATCH 0052/2005] Always call save() after modifying something --- src/main/java/cli/Main.java | 1 - src/main/java/cli/Manager.java | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 64bdfae3..28854846 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -240,7 +240,6 @@ public class Main { break; } - m.save(); System.exit(0); } diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index b15457af..86850ed4 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -136,7 +136,7 @@ class Manager implements TextSecure { accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); } - public void save() { + private void save() { ObjectNode rootNode = jsonProcessot.createObjectNode(); rootNode.put("username", username) .put("password", password) @@ -160,6 +160,7 @@ class Manager implements TextSecure { axolotlStore = new JsonAxolotlStore(identityKey, registrationId); groupStore = new JsonGroupStore(); registered = false; + save(); } public boolean isRegistered() { @@ -177,6 +178,7 @@ class Manager implements TextSecure { accountManager.requestSmsVerificationCode(); registered = false; + save(); } private static final int BATCH_SIZE = 100; @@ -194,6 +196,8 @@ class Manager implements TextSecure { } preKeyIdOffset = (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE; + save(); + return records; } @@ -210,6 +214,7 @@ class Manager implements TextSecure { PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair); axolotlStore.storePreKey(Medium.MAX_VALUE, record); + save(); return record; } @@ -222,6 +227,7 @@ class Manager implements TextSecure { axolotlStore.storeSignedPreKey(nextSignedPreKeyId, record); nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE; + save(); return record; } catch (InvalidKeyException e) { @@ -244,6 +250,7 @@ class Manager implements TextSecure { SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(axolotlStore.getIdentityKeyPair()); accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); + save(); } @@ -392,6 +399,7 @@ class Manager implements TextSecure { } catch (InvalidNumberException e) { System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); System.err.println("Aborting sending."); + save(); return; } } From a69e8facd2efdfd29bcb128c76573adb62431f1f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 12 Dec 2015 22:09:51 +0100 Subject: [PATCH 0053/2005] Inline unnecessary methods --- src/main/java/cli/Manager.java | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 86850ed4..3b11d7b4 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -293,7 +293,7 @@ class Manager implements TextSecure { } TextSecureDataMessage message = messageBuilder.build(); - sendMessage(message, getGroupInfo(groupId).members); + sendMessage(message, groupStore.getGroup(groupId).members); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { @@ -305,7 +305,7 @@ class Manager implements TextSecure { .asGroupMessage(group) .build(); - sendMessage(message, getGroupInfo(groupId).members); + sendMessage(message, groupStore.getGroup(groupId).members); } public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { @@ -315,7 +315,7 @@ class Manager implements TextSecure { g = new GroupInfo(Util.getSecretBytes(16)); g.members.add(username); } else { - g = getGroupInfo(groupId); + g = groupStore.getGroup(groupId); } if (name != null) { @@ -349,7 +349,7 @@ class Manager implements TextSecure { } } - setGroupInfo(g); + groupStore.updateGroup(g); TextSecureDataMessage message = TextSecureDataMessage.newBuilder() .asGroupMessage(group.build()) @@ -592,14 +592,6 @@ class Manager implements TextSecure { return new TextSecureAddress(e164number); } - private GroupInfo getGroupInfo(byte[] groupId) throws GroupNotFoundException { - return groupStore.getGroup(groupId); - } - - private void setGroupInfo(GroupInfo group) { - groupStore.updateGroup(group); - } - @Override public boolean isRemote() { return false; From 5859e7b9f78b5d8963c56c66b75530ee3f094bee Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 12 Dec 2015 22:56:30 +0100 Subject: [PATCH 0054/2005] Add possibility to send messages via dbus daemon --- src/main/java/cli/Main.java | 431 ++++++++++++++++++------------ src/main/java/cli/Manager.java | 4 +- src/main/java/cli/TextSecure.java | 4 + 3 files changed, 263 insertions(+), 176 deletions(-) diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java index 28854846..2884ef91 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/cli/Main.java @@ -33,6 +33,8 @@ import org.whispersystems.textsecure.api.util.PhoneNumberFormatter; import java.io.File; import java.io.IOException; import java.security.Security; +import java.util.ArrayList; +import java.util.List; public class Main { @@ -46,84 +48,180 @@ public class Main { } final String username = ns.getString("username"); - final Manager m = new Manager(username); - if (m.userExists()) { - try { - m.load(); - } catch (Exception e) { - System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage()); - System.exit(2); + Manager m; + TextSecure ts; + DBusConnection dBusConn = null; + try { + if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { + try { + m = null; + int busType; + if (ns.getBoolean("dbus_system")) { + busType = DBusConnection.SYSTEM; + } else { + busType = DBusConnection.SESSION; + } + dBusConn = DBusConnection.getConnection(busType); + ts = (TextSecure) dBusConn.getRemoteObject( + "org.asamk.TextSecure", "/org/asamk/TextSecure", + TextSecure.class); + } catch (DBusException e) { + e.printStackTrace(); + if (dBusConn != null) { + dBusConn.disconnect(); + } + System.exit(3); + return; + } + } else { + m = new Manager(username); + ts = m; + if (m.userExists()) { + try { + m.load(); + } catch (Exception e) { + System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage()); + System.exit(2); + return; + } + } } - } - switch (ns.getString("command")) { - case "register": - if (!m.userHasKeys()) { - m.createNewIdentity(); - } - try { - m.register(ns.getBoolean("voice")); - } catch (IOException e) { - System.err.println("Request verify error: " + e.getMessage()); - System.exit(3); - } - break; - case "verify": - if (!m.userHasKeys()) { - System.err.println("User has no keys, first call register."); - System.exit(1); - } - if (m.isRegistered()) { - System.err.println("User registration is already verified"); - System.exit(1); - } - try { - m.verifyAccount(ns.getString("verificationCode")); - } catch (IOException e) { - System.err.println("Verify error: " + e.getMessage()); - System.exit(3); - } - break; - case "send": - if (!m.isRegistered()) { - System.err.println("User is not registered."); - System.exit(1); - } - - if (ns.getBoolean("endsession")) { - if (ns.getList("recipient") == null) { - System.err.println("No recipients given"); - System.err.println("Aborting sending."); + switch (ns.getString("command")) { + case "register": + if (dBusConn != null) { + System.err.println("register is not yet implementd via dbus"); + System.exit(1); + } + if (!m.userHasKeys()) { + m.createNewIdentity(); + } + try { + m.register(ns.getBoolean("voice")); + } catch (IOException e) { + System.err.println("Request verify error: " + e.getMessage()); + System.exit(3); + } + break; + case "verify": + if (dBusConn != null) { + System.err.println("verify is not yet implementd via dbus"); + System.exit(1); + } + if (!m.userHasKeys()) { + System.err.println("User has no keys, first call register."); + System.exit(1); + } + if (m.isRegistered()) { + System.err.println("User registration is already verified"); System.exit(1); } try { - m.sendEndSessionMessage(ns.getList("recipient")); + m.verifyAccount(ns.getString("verificationCode")); } catch (IOException e) { - handleIOException(e); - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - } catch (AssertionError e) { - handleAssertionError(e); + System.err.println("Verify error: " + e.getMessage()); + System.exit(3); } - } else { - String messageText = ns.getString("message"); - if (messageText == null) { + break; + case "send": + if (dBusConn == null && !m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); + } + + if (ns.getBoolean("endsession")) { + if (ns.getList("recipient") == null) { + System.err.println("No recipients given"); + System.err.println("Aborting sending."); + System.exit(1); + } try { - messageText = IOUtils.toString(System.in); + ts.sendEndSessionMessage(ns.getList("recipient")); } catch (IOException e) { - System.err.println("Failed to read message from stdin: " + e.getMessage()); + handleIOException(e); + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + } catch (AssertionError e) { + handleAssertionError(e); + } + } else { + String messageText = ns.getString("message"); + if (messageText == null) { + try { + messageText = IOUtils.toString(System.in); + } catch (IOException e) { + System.err.println("Failed to read message from stdin: " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); + } + } + + try { + List attachments = ns.getList("attachment"); + if (attachments == null) { + attachments = new ArrayList<>(); + } + if (ns.getString("group") != null) { + byte[] groupId = decodeGroupId(ns.getString("group")); + ts.sendGroupMessage(messageText, attachments, groupId); + } else { + ts.sendMessage(messageText, attachments, ns.getList("recipient")); + } + } catch (IOException e) { + handleIOException(e); + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + } catch (AssertionError e) { + handleAssertionError(e); + } catch (GroupNotFoundException e) { + handleGroupNotFoundException(e); + } catch (AttachmentInvalidException e) { + System.err.println("Failed to add attachment (\"" + e.getAttachment() + "\"): " + e.getMessage()); System.err.println("Aborting sending."); System.exit(1); } } + break; + case "receive": + if (dBusConn != null) { + System.err.println("receive is not yet implementd via dbus"); + System.exit(1); + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); + } + int timeout = 5; + if (ns.getInt("timeout") != null) { + timeout = ns.getInt("timeout"); + } + boolean returnOnTimeout = true; + if (timeout < 0) { + returnOnTimeout = false; + timeout = 3600; + } try { - if (ns.getString("group") != null) { - byte[] groupId = decodeGroupId(ns.getString("group")); - m.sendGroupMessage(messageText, ns.getList("attachment"), groupId); - } else { - m.sendMessage(messageText, ns.getList("attachment"), ns.getList("recipient")); - } + m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m)); + } catch (IOException e) { + System.err.println("Error while receiving messages: " + e.getMessage()); + System.exit(3); + } catch (AssertionError e) { + handleAssertionError(e); + } + break; + case "quitGroup": + if (dBusConn != null) { + System.err.println("quitGroup is not yet implementd via dbus"); + System.exit(1); + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); + } + + try { + m.sendQuitGroupMessage(decodeGroupId(ns.getString("group"))); } catch (IOException e) { handleIOException(e); } catch (EncapsulatedExceptions e) { @@ -132,115 +230,88 @@ public class Main { handleAssertionError(e); } catch (GroupNotFoundException e) { handleGroupNotFoundException(e); - } catch (AttachmentInvalidException e) { - System.err.println("Failed to add attachment (\"" + e.getAttachment() + "\"): " + e.getMessage()); - System.err.println("Aborting sending."); + } + + break; + case "updateGroup": + if (dBusConn != null) { + System.err.println("updateGroup is not yet implementd via dbus"); System.exit(1); } - } - - break; - case "receive": - if (!m.isRegistered()) { - System.err.println("User is not registered."); - System.exit(1); - } - int timeout = 5; - if (ns.getInt("timeout") != null) { - timeout = ns.getInt("timeout"); - } - boolean returnOnTimeout = true; - if (timeout < 0) { - returnOnTimeout = false; - timeout = 3600; - } - try { - m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m)); - } catch (IOException e) { - System.err.println("Error while receiving messages: " + e.getMessage()); - System.exit(3); - } catch (AssertionError e) { - handleAssertionError(e); - } - break; - case "quitGroup": - if (!m.isRegistered()) { - System.err.println("User is not registered."); - System.exit(1); - } - - try { - m.sendQuitGroupMessage(decodeGroupId(ns.getString("group"))); - } catch (IOException e) { - handleIOException(e); - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - } catch (AssertionError e) { - handleAssertionError(e); - } catch (GroupNotFoundException e) { - handleGroupNotFoundException(e); - } - - break; - case "updateGroup": - if (!m.isRegistered()) { - System.err.println("User is not registered."); - System.exit(1); - } - - try { - byte[] groupId = null; - if (ns.getString("group") != null) { - groupId = decodeGroupId(ns.getString("group")); + if (!m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); } - byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.getList("member"), ns.getString("avatar")); - if (groupId == null) { - System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …"); - } - } catch (IOException e) { - handleIOException(e); - } catch (AttachmentInvalidException e) { - System.err.println("Failed to add avatar attachment (\"" + e.getAttachment() + ") for group\": " + e.getMessage()); - System.err.println("Aborting sending."); - System.exit(1); - } catch (GroupNotFoundException e) { - handleGroupNotFoundException(e); - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - } - break; - case "daemon": - if (!m.isRegistered()) { - System.err.println("User is not registered."); - System.exit(1); - } - try { - int busType; - if (ns.getBoolean("system")) { - busType = DBusConnection.SYSTEM; - } else { - busType = DBusConnection.SESSION; + try { + byte[] groupId = null; + if (ns.getString("group") != null) { + groupId = decodeGroupId(ns.getString("group")); + } + byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.getList("member"), ns.getString("avatar")); + if (groupId == null) { + System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …"); + } + } catch (IOException e) { + handleIOException(e); + } catch (AttachmentInvalidException e) { + System.err.println("Failed to add avatar attachment (\"" + e.getAttachment() + ") for group\": " + e.getMessage()); + System.err.println("Aborting sending."); + System.exit(1); + } catch (GroupNotFoundException e) { + handleGroupNotFoundException(e); + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); } - DBusConnection conn = DBusConnection.getConnection(busType); - conn.requestBusName("org.asamk.TextSecure"); - conn.exportObject("/org/asamk/TextSecure", m); - } catch (DBusException e) { - e.printStackTrace(); - System.exit(3); - } - try { - m.receiveMessages(3600, false, new ReceiveMessageHandler(m)); - } catch (IOException e) { - System.err.println("Error while receiving messages: " + e.getMessage()); - System.exit(3); - } catch (AssertionError e) { - handleAssertionError(e); - } - break; + break; + case "daemon": + if (dBusConn != null) { + System.err.println("Stop it."); + System.exit(1); + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); + } + DBusConnection conn = null; + try { + try { + int busType; + if (ns.getBoolean("system")) { + busType = DBusConnection.SYSTEM; + } else { + busType = DBusConnection.SESSION; + } + conn = DBusConnection.getConnection(busType); + conn.requestBusName("org.asamk.TextSecure"); + conn.exportObject("/org/asamk/TextSecure", m); + } catch (DBusException e) { + e.printStackTrace(); + System.exit(3); + } + try { + m.receiveMessages(3600, false, new ReceiveMessageHandler(m)); + } catch (IOException e) { + System.err.println("Error while receiving messages: " + e.getMessage()); + System.exit(3); + } catch (AssertionError e) { + handleAssertionError(e); + } + } finally { + if (conn != null) { + conn.disconnect(); + } + } + + break; + } + System.exit(0); + } finally { + if (dBusConn != null) { + dBusConn.disconnect(); + } } - System.exit(0); } private static void handleGroupNotFoundException(GroupNotFoundException e) { @@ -266,12 +337,20 @@ public class Main { .description("Commandline interface for TextSecure.") .version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION); - parser.addArgument("-u", "--username") - .help("Specify your phone number, that will be used for verification."); parser.addArgument("-v", "--version") .help("Show package version.") .action(Arguments.version()); + MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup(); + mut.addArgument("-u", "--username") + .help("Specify your phone number, that will be used for verification."); + mut.addArgument("--dbus") + .help("Make request via user dbus.") + .action(Arguments.storeTrue()); + mut.addArgument("--dbus-system") + .help("Make request via system dbus.") + .action(Arguments.storeTrue()); + Subparsers subparsers = parser.addSubparsers() .title("subcommands") .dest("command") @@ -330,14 +409,16 @@ public class Main { try { Namespace ns = parser.parseArgs(args); - if (ns.getString("username") == null) { - parser.printUsage(); - System.err.println("You need to specify a username (phone number)"); - System.exit(2); - } - if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) { - System.err.println("Invalid username (phone number), make sure you include the country code."); - System.exit(2); + if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) { + if (ns.getString("username") == null) { + parser.printUsage(); + System.err.println("You need to specify a username (phone number)"); + System.exit(2); + } + if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) { + System.err.println("Invalid username (phone number), make sure you include the country code."); + System.exit(2); + } } if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) { System.err.println("You cannot specify recipients by phone number and groups a the same time"); diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java index 3b11d7b4..caa3fb5d 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/cli/Manager.java @@ -367,8 +367,9 @@ class Manager implements TextSecure { sendMessage(message, attachments, recipients); } + @Override public void sendMessage(String messageText, List attachments, - Collection recipients) + List recipients) throws IOException, EncapsulatedExceptions, AttachmentInvalidException { final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); if (attachments != null) { @@ -379,6 +380,7 @@ class Manager implements TextSecure { sendMessage(message, recipients); } + @Override public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { TextSecureDataMessage message = TextSecureDataMessage.newBuilder() .asEndSessionMessage() diff --git a/src/main/java/cli/TextSecure.java b/src/main/java/cli/TextSecure.java index 011e696f..3220ba00 100644 --- a/src/main/java/cli/TextSecure.java +++ b/src/main/java/cli/TextSecure.java @@ -9,5 +9,9 @@ import java.util.List; public interface TextSecure extends DBusInterface { void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; + void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; + + void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions; + void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; } From 27d9424f1e7f607ac2dfad5b2164d065ffb79ef7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 13 Dec 2015 11:05:08 +0100 Subject: [PATCH 0055/2005] Rename package --- build.gradle | 2 +- src/main/java/{cli => org/asamk}/TextSecure.java | 4 +++- .../textsecure}/AttachmentInvalidException.java | 2 +- .../java/{cli => org/asamk/textsecure}/Base64.java | 2 +- .../{cli => org/asamk/textsecure}/GroupInfo.java | 2 +- .../asamk/textsecure}/GroupNotFoundException.java | 2 +- .../asamk/textsecure}/JsonAxolotlStore.java | 2 +- .../asamk/textsecure}/JsonGroupStore.java | 2 +- .../asamk/textsecure}/JsonIdentityKeyStore.java | 2 +- .../asamk/textsecure}/JsonPreKeyStore.java | 2 +- .../asamk/textsecure}/JsonSessionStore.java | 2 +- .../asamk/textsecure}/JsonSignedPreKeyStore.java | 2 +- .../java/{cli => org/asamk/textsecure}/Main.java | 12 ++++++++---- .../java/{cli => org/asamk/textsecure}/Manager.java | 3 ++- .../java/{cli => org/asamk/textsecure}/Util.java | 2 +- .../asamk/textsecure}/WhisperTrustStore.java | 4 ++-- .../{cli => org/asamk/textsecure}/whisper.store | Bin 17 files changed, 27 insertions(+), 20 deletions(-) rename src/main/java/{cli => org/asamk}/TextSecure.java (86%) rename src/main/java/{cli => org/asamk/textsecure}/AttachmentInvalidException.java (91%) rename src/main/java/{cli => org/asamk/textsecure}/Base64.java (99%) rename src/main/java/{cli => org/asamk/textsecure}/GroupInfo.java (96%) rename src/main/java/{cli => org/asamk/textsecure}/GroupNotFoundException.java (89%) rename src/main/java/{cli => org/asamk/textsecure}/JsonAxolotlStore.java (99%) rename src/main/java/{cli => org/asamk/textsecure}/JsonGroupStore.java (98%) rename src/main/java/{cli => org/asamk/textsecure}/JsonIdentityKeyStore.java (99%) rename src/main/java/{cli => org/asamk/textsecure}/JsonPreKeyStore.java (99%) rename src/main/java/{cli => org/asamk/textsecure}/JsonSessionStore.java (99%) rename src/main/java/{cli => org/asamk/textsecure}/JsonSignedPreKeyStore.java (99%) rename src/main/java/{cli => org/asamk/textsecure}/Main.java (98%) rename src/main/java/{cli => org/asamk/textsecure}/Manager.java (99%) rename src/main/java/{cli => org/asamk/textsecure}/Util.java (95%) rename src/main/java/{cli => org/asamk/textsecure}/WhisperTrustStore.java (73%) rename src/main/resources/{cli => org/asamk/textsecure}/whisper.store (100%) diff --git a/build.gradle b/build.gradle index 19893391..1eef1402 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'application' sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 -mainClassName = 'cli.Main' +mainClassName = 'org.asamk.textsecure.Main' version = '0.1.0' diff --git a/src/main/java/cli/TextSecure.java b/src/main/java/org/asamk/TextSecure.java similarity index 86% rename from src/main/java/cli/TextSecure.java rename to src/main/java/org/asamk/TextSecure.java index 3220ba00..d046c0a1 100644 --- a/src/main/java/cli/TextSecure.java +++ b/src/main/java/org/asamk/TextSecure.java @@ -1,5 +1,7 @@ -package cli; +package org.asamk; +import org.asamk.textsecure.AttachmentInvalidException; +import org.asamk.textsecure.GroupNotFoundException; import org.freedesktop.dbus.DBusInterface; import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; diff --git a/src/main/java/cli/AttachmentInvalidException.java b/src/main/java/org/asamk/textsecure/AttachmentInvalidException.java similarity index 91% rename from src/main/java/cli/AttachmentInvalidException.java rename to src/main/java/org/asamk/textsecure/AttachmentInvalidException.java index 289855e9..bf227269 100644 --- a/src/main/java/cli/AttachmentInvalidException.java +++ b/src/main/java/org/asamk/textsecure/AttachmentInvalidException.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; public class AttachmentInvalidException extends Exception { private final String attachment; diff --git a/src/main/java/cli/Base64.java b/src/main/java/org/asamk/textsecure/Base64.java similarity index 99% rename from src/main/java/cli/Base64.java rename to src/main/java/org/asamk/textsecure/Base64.java index b2a28591..f8f6b4cd 100644 --- a/src/main/java/cli/Base64.java +++ b/src/main/java/org/asamk/textsecure/Base64.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; /** *

Encodes and decodes to and from Base64 notation.

diff --git a/src/main/java/cli/GroupInfo.java b/src/main/java/org/asamk/textsecure/GroupInfo.java similarity index 96% rename from src/main/java/cli/GroupInfo.java rename to src/main/java/org/asamk/textsecure/GroupInfo.java index fe31baf9..dd6cacf7 100644 --- a/src/main/java/cli/GroupInfo.java +++ b/src/main/java/org/asamk/textsecure/GroupInfo.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/cli/GroupNotFoundException.java b/src/main/java/org/asamk/textsecure/GroupNotFoundException.java similarity index 89% rename from src/main/java/cli/GroupNotFoundException.java rename to src/main/java/org/asamk/textsecure/GroupNotFoundException.java index 85e1cf0b..57f4cef8 100644 --- a/src/main/java/cli/GroupNotFoundException.java +++ b/src/main/java/org/asamk/textsecure/GroupNotFoundException.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; public class GroupNotFoundException extends Exception { private final byte[] groupId; diff --git a/src/main/java/cli/JsonAxolotlStore.java b/src/main/java/org/asamk/textsecure/JsonAxolotlStore.java similarity index 99% rename from src/main/java/cli/JsonAxolotlStore.java rename to src/main/java/org/asamk/textsecure/JsonAxolotlStore.java index e7eb054c..f054e337 100644 --- a/src/main/java/cli/JsonAxolotlStore.java +++ b/src/main/java/org/asamk/textsecure/JsonAxolotlStore.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/src/main/java/cli/JsonGroupStore.java b/src/main/java/org/asamk/textsecure/JsonGroupStore.java similarity index 98% rename from src/main/java/cli/JsonGroupStore.java rename to src/main/java/org/asamk/textsecure/JsonGroupStore.java index 0fbfdc85..17a59f87 100644 --- a/src/main/java/cli/JsonGroupStore.java +++ b/src/main/java/org/asamk/textsecure/JsonGroupStore.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; diff --git a/src/main/java/cli/JsonIdentityKeyStore.java b/src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java similarity index 99% rename from src/main/java/cli/JsonIdentityKeyStore.java rename to src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java index e4d18b8f..050158fc 100644 --- a/src/main/java/cli/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/cli/JsonPreKeyStore.java b/src/main/java/org/asamk/textsecure/JsonPreKeyStore.java similarity index 99% rename from src/main/java/cli/JsonPreKeyStore.java rename to src/main/java/org/asamk/textsecure/JsonPreKeyStore.java index 393f1805..9688cf3e 100644 --- a/src/main/java/cli/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/textsecure/JsonPreKeyStore.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/cli/JsonSessionStore.java b/src/main/java/org/asamk/textsecure/JsonSessionStore.java similarity index 99% rename from src/main/java/cli/JsonSessionStore.java rename to src/main/java/org/asamk/textsecure/JsonSessionStore.java index 3cb78945..db352d3b 100644 --- a/src/main/java/cli/JsonSessionStore.java +++ b/src/main/java/org/asamk/textsecure/JsonSessionStore.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/cli/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java similarity index 99% rename from src/main/java/cli/JsonSignedPreKeyStore.java rename to src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java index 4dc0cad3..d8dbeb9a 100644 --- a/src/main/java/cli/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/cli/Main.java b/src/main/java/org/asamk/textsecure/Main.java similarity index 98% rename from src/main/java/cli/Main.java rename to src/main/java/org/asamk/textsecure/Main.java index 2884ef91..265059f7 100644 --- a/src/main/java/cli/Main.java +++ b/src/main/java/org/asamk/textsecure/Main.java @@ -14,12 +14,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package cli; +package org.asamk.textsecure; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; import org.apache.commons.io.IOUtils; +import org.asamk.TextSecure; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException; @@ -38,6 +39,9 @@ import java.util.List; public class Main { + public static final String TEXTSECURE_BUSNAME = "org.asamk.TextSecure"; + public static final String TEXTSECURE_OBJECTPATH = "/org/asamk/TextSecure"; + public static void main(String[] args) { // Workaround for BKS truststore Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1); @@ -63,7 +67,7 @@ public class Main { } dBusConn = DBusConnection.getConnection(busType); ts = (TextSecure) dBusConn.getRemoteObject( - "org.asamk.TextSecure", "/org/asamk/TextSecure", + TEXTSECURE_BUSNAME, TEXTSECURE_OBJECTPATH, TextSecure.class); } catch (DBusException e) { e.printStackTrace(); @@ -284,8 +288,8 @@ public class Main { busType = DBusConnection.SESSION; } conn = DBusConnection.getConnection(busType); - conn.requestBusName("org.asamk.TextSecure"); - conn.exportObject("/org/asamk/TextSecure", m); + conn.requestBusName(TEXTSECURE_BUSNAME); + conn.exportObject(TEXTSECURE_OBJECTPATH, m); } catch (DBusException e) { e.printStackTrace(); System.exit(3); diff --git a/src/main/java/cli/Manager.java b/src/main/java/org/asamk/textsecure/Manager.java similarity index 99% rename from src/main/java/cli/Manager.java rename to src/main/java/org/asamk/textsecure/Manager.java index caa3fb5d..f0714ce1 100644 --- a/src/main/java/cli/Manager.java +++ b/src/main/java/org/asamk/textsecure/Manager.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package cli; +package org.asamk.textsecure; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.asamk.TextSecure; import org.whispersystems.libaxolotl.*; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; diff --git a/src/main/java/cli/Util.java b/src/main/java/org/asamk/textsecure/Util.java similarity index 95% rename from src/main/java/cli/Util.java rename to src/main/java/org/asamk/textsecure/Util.java index 215e14b8..7cbc851e 100644 --- a/src/main/java/cli/Util.java +++ b/src/main/java/org/asamk/textsecure/Util.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; diff --git a/src/main/java/cli/WhisperTrustStore.java b/src/main/java/org/asamk/textsecure/WhisperTrustStore.java similarity index 73% rename from src/main/java/cli/WhisperTrustStore.java rename to src/main/java/org/asamk/textsecure/WhisperTrustStore.java index 08519653..48d96e8f 100644 --- a/src/main/java/cli/WhisperTrustStore.java +++ b/src/main/java/org/asamk/textsecure/WhisperTrustStore.java @@ -1,4 +1,4 @@ -package cli; +package org.asamk.textsecure; import org.whispersystems.textsecure.api.push.TrustStore; @@ -8,7 +8,7 @@ class WhisperTrustStore implements TrustStore { @Override public InputStream getKeyStoreInputStream() { - return cli.WhisperTrustStore.class.getResourceAsStream("whisper.store"); + return WhisperTrustStore.class.getResourceAsStream("whisper.store"); } @Override diff --git a/src/main/resources/cli/whisper.store b/src/main/resources/org/asamk/textsecure/whisper.store similarity index 100% rename from src/main/resources/cli/whisper.store rename to src/main/resources/org/asamk/textsecure/whisper.store From 60981479d7a3dc576e2ca0b8e36fd24ff19a39be Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 16 Dec 2015 21:34:57 +0100 Subject: [PATCH 0056/2005] Add example files for making a dbus activatable service --- data/org.asamk.TextSecure.conf | 16 ++++++++++++++++ data/org.asamk.TextSecure.service | 4 ++++ data/textsecure.service | 11 +++++++++++ 3 files changed, 31 insertions(+) create mode 100644 data/org.asamk.TextSecure.conf create mode 100644 data/org.asamk.TextSecure.service create mode 100644 data/textsecure.service diff --git a/data/org.asamk.TextSecure.conf b/data/org.asamk.TextSecure.conf new file mode 100644 index 00000000..5267a799 --- /dev/null +++ b/data/org.asamk.TextSecure.conf @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/data/org.asamk.TextSecure.service b/data/org.asamk.TextSecure.service new file mode 100644 index 00000000..a031626b --- /dev/null +++ b/data/org.asamk.TextSecure.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.asamk.TextSecure +Exec=/bin/false +SystemdService=dbus-org.asamk.TextSecure.service diff --git a/data/textsecure.service b/data/textsecure.service new file mode 100644 index 00000000..2ed892ce --- /dev/null +++ b/data/textsecure.service @@ -0,0 +1,11 @@ +[Unit] +Description=Send secure messages to TextSecure/Signal clients + +[Service] +Type=dbus +ExecStart=%dir%/bin/textsecure-cli -u %number% daemon --system +User=textsecure +BusName=org.asamk.TextSecure + +[Install] +Alias=dbus-org.asamk.TextSecure.service From 61392fd17d730ec561413cf07aa368c667caaab1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 16 Dec 2015 21:35:35 +0100 Subject: [PATCH 0057/2005] Take dbus name only when the service is ready --- src/main/java/org/asamk/textsecure/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/textsecure/Main.java b/src/main/java/org/asamk/textsecure/Main.java index 265059f7..2f2825a8 100644 --- a/src/main/java/org/asamk/textsecure/Main.java +++ b/src/main/java/org/asamk/textsecure/Main.java @@ -288,8 +288,8 @@ public class Main { busType = DBusConnection.SESSION; } conn = DBusConnection.getConnection(busType); - conn.requestBusName(TEXTSECURE_BUSNAME); conn.exportObject(TEXTSECURE_OBJECTPATH, m); + conn.requestBusName(TEXTSECURE_BUSNAME); } catch (DBusException e) { e.printStackTrace(); System.exit(3); From 2a0f9f76299f968f1c198de149a7702c32184f20 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 16 Dec 2015 22:20:44 +0100 Subject: [PATCH 0058/2005] Update README.md Add dbus info --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index 8790fc8d..184d7595 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,37 @@ usage: textsecure-cli [-h] [-u USERNAME] [-v] {register,verify,send,quitGroup,up textsecure-cli -u USERNAME send -m "This is a message" -g GROUP_ID +## DBus service + +textsecure-cli can run in daemon mode and provides an experimental dbus interface. +For dbus support you need jni/unix-java.so installed on your system (Debian: libunixsocket-java ArchLinux: libmatthew-unix-java (AUR)). + +* Run in daemon mode (dbus session bus) + + textsecure-cli -u USERNAME daemon + +* Send a message via dbus + + textsecure-cli --dbus send -m "Message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] + +### System bus + +To run on the system bus you need to take some additional steps. +It’s advisable to run textsecure-cli as a separate unix user, the following steps assume you created a user named *textsecure*. +These steps, executed as root, should work on all distributions using systemd. + +```bash +cp data/org.asamk.TextSecure.config /etc/dbus-1/system.d/ +cp data/org.asamk.TextSecure.service /usr/share/dbus-1/system-services/ +cp data/textsecure.service /etc/systemd/system/ +sed -i -e "s|%dir%||" -e "s|%number%||" /etc/systemd/system/textsecure.service +systemctl daemon-reload +systemctl enable textsecure.service +systemctl reload dbus.service +``` + +Then just execute the send command from above, the service will be autostarted by dbus the first time it is requested. + ## Storage The password and cryptographic keys are created when registering and stored in the current users home directory: From 3d560672720eb82ca816a96aa8ea6eb45506b38e Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 30 Dec 2015 17:39:07 +0100 Subject: [PATCH 0059/2005] Fix exceptions to work over dbus --- .../textsecure/AttachmentInvalidException.java | 15 +++++++-------- .../asamk/textsecure/GroupNotFoundException.java | 16 ++++++++-------- src/main/java/org/asamk/textsecure/Main.java | 6 +++--- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/asamk/textsecure/AttachmentInvalidException.java b/src/main/java/org/asamk/textsecure/AttachmentInvalidException.java index bf227269..5afa67e3 100644 --- a/src/main/java/org/asamk/textsecure/AttachmentInvalidException.java +++ b/src/main/java/org/asamk/textsecure/AttachmentInvalidException.java @@ -1,14 +1,13 @@ package org.asamk.textsecure; -public class AttachmentInvalidException extends Exception { - private final String attachment; +import org.freedesktop.dbus.exceptions.DBusExecutionException; + +public class AttachmentInvalidException extends DBusExecutionException { + public AttachmentInvalidException(String message) { + super(message); + } public AttachmentInvalidException(String attachment, Exception e) { - super(e); - this.attachment = attachment; - } - - public String getAttachment() { - return attachment; + super(attachment + ": " + e.getMessage()); } } diff --git a/src/main/java/org/asamk/textsecure/GroupNotFoundException.java b/src/main/java/org/asamk/textsecure/GroupNotFoundException.java index 57f4cef8..6c4cf5b1 100644 --- a/src/main/java/org/asamk/textsecure/GroupNotFoundException.java +++ b/src/main/java/org/asamk/textsecure/GroupNotFoundException.java @@ -1,14 +1,14 @@ package org.asamk.textsecure; -public class GroupNotFoundException extends Exception { - private final byte[] groupId; +import org.freedesktop.dbus.exceptions.DBusExecutionException; + +public class GroupNotFoundException extends DBusExecutionException { + + public GroupNotFoundException(String message) { + super(message); + } public GroupNotFoundException(byte[] groupId) { - super(); - this.groupId = groupId; - } - - public byte[] getGroupId() { - return groupId; + super("Group not found: " + Base64.encodeBytes(groupId)); } } diff --git a/src/main/java/org/asamk/textsecure/Main.java b/src/main/java/org/asamk/textsecure/Main.java index 2f2825a8..7213a945 100644 --- a/src/main/java/org/asamk/textsecure/Main.java +++ b/src/main/java/org/asamk/textsecure/Main.java @@ -180,7 +180,7 @@ public class Main { } catch (GroupNotFoundException e) { handleGroupNotFoundException(e); } catch (AttachmentInvalidException e) { - System.err.println("Failed to add attachment (\"" + e.getAttachment() + "\"): " + e.getMessage()); + System.err.println("Failed to add attachment: " + e.getMessage()); System.err.println("Aborting sending."); System.exit(1); } @@ -259,7 +259,7 @@ public class Main { } catch (IOException e) { handleIOException(e); } catch (AttachmentInvalidException e) { - System.err.println("Failed to add avatar attachment (\"" + e.getAttachment() + ") for group\": " + e.getMessage()); + System.err.println("Failed to add avatar attachment for group\": " + e.getMessage()); System.err.println("Aborting sending."); System.exit(1); } catch (GroupNotFoundException e) { @@ -319,7 +319,7 @@ public class Main { } private static void handleGroupNotFoundException(GroupNotFoundException e) { - System.err.println("Failed to send to group \"" + Base64.encodeBytes(e.getGroupId()) + "\": Unknown group"); + System.err.println("Failed to send to group: " + e.getMessage()); System.err.println("Aborting sending."); System.exit(1); } From fb36613402ee70e9e17d848e896689017d7ccdad Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 30 Dec 2015 17:54:48 +0100 Subject: [PATCH 0060/2005] Use File() correctly --- src/main/java/org/asamk/textsecure/Manager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/textsecure/Manager.java b/src/main/java/org/asamk/textsecure/Manager.java index f0714ce1..dfae49de 100644 --- a/src/main/java/org/asamk/textsecure/Manager.java +++ b/src/main/java/org/asamk/textsecure/Manager.java @@ -535,7 +535,7 @@ class Manager implements TextSecure { } public File getAttachmentFile(long attachmentId) { - return new File(attachmentsPath + "/" + attachmentId); + return new File(attachmentsPath, attachmentId + ""); } private File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException { From acadd90a6debfcf473a2dc813c71e4619c0af11d Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 30 Dec 2015 18:09:48 +0100 Subject: [PATCH 0061/2005] Send Signal "MessageReceived" on dbus when receiving messages --- src/main/java/org/asamk/TextSecure.java | 8 ++ src/main/java/org/asamk/textsecure/Main.java | 106 ++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/TextSecure.java b/src/main/java/org/asamk/TextSecure.java index d046c0a1..3aa514c1 100644 --- a/src/main/java/org/asamk/TextSecure.java +++ b/src/main/java/org/asamk/TextSecure.java @@ -3,6 +3,8 @@ package org.asamk; import org.asamk.textsecure.AttachmentInvalidException; import org.asamk.textsecure.GroupNotFoundException; import org.freedesktop.dbus.DBusInterface; +import org.freedesktop.dbus.DBusSignal; +import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; import java.io.IOException; @@ -16,4 +18,10 @@ public interface TextSecure extends DBusInterface { void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions; void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; + + class MessageReceived extends DBusSignal { + public MessageReceived(String objectpath, String sender, byte[] groupId, String message, List attachments) throws DBusException { + super(objectpath, sender, groupId, message, attachments); + } + } } diff --git a/src/main/java/org/asamk/textsecure/Main.java b/src/main/java/org/asamk/textsecure/Main.java index 7213a945..2e694ffd 100644 --- a/src/main/java/org/asamk/textsecure/Main.java +++ b/src/main/java/org/asamk/textsecure/Main.java @@ -295,7 +295,7 @@ public class Main { System.exit(3); } try { - m.receiveMessages(3600, false, new ReceiveMessageHandler(m)); + m.receiveMessages(3600, false, new DbusReceiveMessageHandler(m, conn)); } catch (IOException e) { System.err.println("Error while receiving messages: " + e.getMessage()); System.exit(3); @@ -542,4 +542,108 @@ public class Main { } } } + + private static class DbusReceiveMessageHandler implements Manager.ReceiveMessageHandler { + final Manager m; + final DBusConnection conn; + + public DbusReceiveMessageHandler(Manager m, DBusConnection conn) { + this.m = m; + this.conn = conn; + } + + @Override + public void handleMessage(TextSecureEnvelope envelope, TextSecureContent content, GroupInfo group) { + System.out.println("Envelope from: " + envelope.getSource()); + System.out.println("Timestamp: " + envelope.getTimestamp()); + + if (envelope.isReceipt()) { + System.out.println("Got receipt."); + } else if (envelope.isWhisperMessage() | envelope.isPreKeyWhisperMessage()) { + if (content == null) { + System.out.println("Failed to decrypt message."); + } else { + if (content.getDataMessage().isPresent()) { + TextSecureDataMessage message = content.getDataMessage().get(); + + System.out.println("Message timestamp: " + message.getTimestamp()); + + if (message.getBody().isPresent()) { + System.out.println("Body: " + message.getBody().get()); + } + + if (message.getGroupInfo().isPresent()) { + TextSecureGroup groupInfo = message.getGroupInfo().get(); + System.out.println("Group info:"); + System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); + if (groupInfo.getName().isPresent()) { + System.out.println(" Name: " + groupInfo.getName().get()); + } else if (group != null) { + System.out.println(" Name: " + group.name); + } else { + System.out.println(" Name: "); + } + System.out.println(" Type: " + groupInfo.getType()); + if (groupInfo.getMembers().isPresent()) { + for (String member : groupInfo.getMembers().get()) { + System.out.println(" Member: " + member); + } + } + if (groupInfo.getAvatar().isPresent()) { + System.out.println(" Avatar:"); + printAttachment(groupInfo.getAvatar().get()); + } + } + if (message.isEndSession()) { + System.out.println("Is end session"); + } + + List attachments = new ArrayList<>(); + if (message.getAttachments().isPresent()) { + System.out.println("Attachments: "); + for (TextSecureAttachment attachment : message.getAttachments().get()) { + if (attachment.isPointer()) { + attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); + } + printAttachment(attachment); + } + } + if (!message.isEndSession() && + !(message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() != TextSecureGroup.Type.DELIVER)) { + try { + conn.sendSignal(new TextSecure.MessageReceived( + TEXTSECURE_OBJECTPATH, + envelope.getSource(), + message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], + message.getBody().isPresent() ? message.getBody().get() : "", + attachments)); + } catch (DBusException e) { + e.printStackTrace(); + } + } + } + if (content.getSyncMessage().isPresent()) { + TextSecureSyncMessage syncMessage = content.getSyncMessage().get(); + System.out.println("Received sync message"); + } + } + } else { + System.out.println("Unknown message received."); + } + System.out.println(); + } + + private void printAttachment(TextSecureAttachment attachment) { + System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); + if (attachment.isPointer()) { + final TextSecureAttachmentPointer pointer = attachment.asPointer(); + System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); + System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); + File file = m.getAttachmentFile(pointer.getId()); + if (file.exists()) { + System.out.println(" Stored plaintext in: " + file); + } + } + } + } } From 3e013f1a8d3ddcf455da632d08816399ee136dc0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 30 Dec 2015 18:11:20 +0100 Subject: [PATCH 0062/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1eef1402..e8c24339 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.textsecure.Main' -version = '0.1.0' +version = '0.2.0' repositories { maven { From bc5b4dc76498572776c0ac608a1d824ce2e92769 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 30 Dec 2015 18:13:49 +0100 Subject: [PATCH 0063/2005] Update dependencies --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index e8c24339..0bd5b3c6 100644 --- a/build.gradle +++ b/build.gradle @@ -17,9 +17,9 @@ repositories { dependencies { compile 'org.whispersystems:textsecure-java:1.8.3' - compile 'com.madgag.spongycastle:prov:1.53.0.0' + compile 'com.madgag.spongycastle:prov:1.54.0.0' compile 'commons-io:commons-io:2.4' - compile 'net.sourceforge.argparse4j:argparse4j:0.5.0' + compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' } From ea8f62a298351ec9091d7d3d8e1d4bcdf4e5fffe Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 31 Dec 2015 11:54:18 +0100 Subject: [PATCH 0064/2005] Update gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 941144813d241db74e1bf25b6804c679fbe7f0a3..13372aef5e24af05341d49695ee84e5f9b594659 100644 GIT binary patch delta 1239 zcmZo!%-ph=nK!_jnT3mifrEoVYtz(;ylT8a%KiG(8A0|83=E+Y9fCz9nZAB1Vq`!l z6ay+;uE-3cPHTXvZw4T0vL@q05WSg~sh9~Y(9H52ERf5(1T3&wghQAOETYAC6C%PU zXut{yi zcUjN>JuPF&vh)zstp(jXSw5}N&DU7wQkEuonMJRjE$UOWu9)qmIg;~kmtXJ_YDML!#U zLG*uXy2=g~IoEZOQ4plfC?|ZI7ZU>m3s9@urNtTE2}1EY-b+8RC|S>pwuGS~kT(?0d4=5u!$8#|4PU@0}q`V2jtvpbqMPUKacAZLxh?ZvEyLuu`|X7>Q;gAM`vcm{TEI}tJz#>G zM<%a3UX2h$P<&A@cvK|3%#>7XB&eta+=Ootxw1=C9pMS$o@hvUH9#KU0j zh{Odr3HhKM`Ui~ay#mjB}b delta 1239 zcmZ9LYfO_@7{^chQmNFU2sj~{)(I-iX>DPd65F~OP%bS^Zz_UdOJN&KK`x1Acq7gm zVdZ#cC*@~N20ouzMaJ5=`egiapDHFYDaSDG+o+SI zm?U<)RN|1P7_q!iD%{Ov6&~yj2AHkJ<8KSaM>X5Nafz37PhLze{m5(<-Fa@jbvquzvuKelC$nkkT{{cdM-3hckthBr4aHiL?{g!H$LFu zMMJUl58W9QpIw`0tu6t-`Bf49;fv{#recNj6N>p8xZIiY|388|oT=fAd6feakncEbi6wgSmM?Ke{p947xt1oORo;} zyX0 Date: Thu, 31 Dec 2015 12:54:25 +0100 Subject: [PATCH 0065/2005] Handle DBusExecutionExceptions --- src/main/java/org/asamk/textsecure/Main.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/org/asamk/textsecure/Main.java b/src/main/java/org/asamk/textsecure/Main.java index 2e694ffd..8a252b99 100644 --- a/src/main/java/org/asamk/textsecure/Main.java +++ b/src/main/java/org/asamk/textsecure/Main.java @@ -23,6 +23,7 @@ import org.apache.commons.io.IOUtils; import org.asamk.TextSecure; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException; import org.whispersystems.textsecure.api.messages.*; import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage; @@ -147,6 +148,8 @@ public class Main { handleEncapsulatedExceptions(e); } catch (AssertionError e) { handleAssertionError(e); + } catch (DBusExecutionException e) { + handleDBusExecutionException(e); } } else { String messageText = ns.getString("message"); @@ -183,6 +186,8 @@ public class Main { System.err.println("Failed to add attachment: " + e.getMessage()); System.err.println("Aborting sending."); System.exit(1); + } catch (DBusExecutionException e) { + handleDBusExecutionException(e); } } @@ -324,6 +329,12 @@ public class Main { System.exit(1); } + private static void handleDBusExecutionException(DBusExecutionException e) { + System.err.println("Cannot connect to dbus: " + e.getMessage()); + System.err.println("Aborting."); + System.exit(1); + } + private static byte[] decodeGroupId(String groupId) { try { return Base64.decode(groupId); From 43ec78594c0c6b753e4578bc14184e471f3262a5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 31 Dec 2015 13:17:41 +0100 Subject: [PATCH 0066/2005] Implement fetch messages Uses a patched libtextsecure-java https://github.com/AsamK/libtextsecure-java/commits/master --- build.gradle | 2 +- src/main/java/org/asamk/textsecure/Manager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 0bd5b3c6..1b1c9e35 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ repositories { } dependencies { - compile 'org.whispersystems:textsecure-java:1.8.3' + compile 'org.whispersystems:textsecure-java:1.8.3fetchMessages' compile 'com.madgag.spongycastle:prov:1.54.0.0' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' diff --git a/src/main/java/org/asamk/textsecure/Manager.java b/src/main/java/org/asamk/textsecure/Manager.java index dfae49de..2a2b5e4f 100644 --- a/src/main/java/org/asamk/textsecure/Manager.java +++ b/src/main/java/org/asamk/textsecure/Manager.java @@ -239,7 +239,7 @@ class Manager implements TextSecure { public void verifyAccount(String verificationCode) throws IOException { verificationCode = verificationCode.replace("-", ""); signalingKey = Util.getSecret(52); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId(), false); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId(), false, true); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; From 13aafc6712943d553d0ef5971940cf1914ad1383 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 31 Dec 2015 15:17:34 +0100 Subject: [PATCH 0067/2005] Add systemd service file with instance variable --- data/textsecure-cli@.service | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 data/textsecure-cli@.service diff --git a/data/textsecure-cli@.service b/data/textsecure-cli@.service new file mode 100644 index 00000000..b8221bc2 --- /dev/null +++ b/data/textsecure-cli@.service @@ -0,0 +1,15 @@ +[Unit] +Description=Send secure messages to TextSecure/Signal clients +Requires=dbus.socket +After=dbus.socket +Wants=network.target +After=network.target + +[Service] +Type=dbus +ExecStart=%dir%/bin/textsecure-cli -u %I daemon --system +User=textsecure-cli +BusName=org.asamk.TextSecure + +[Install] +WantedBy=multi-user.target From df0ae3b8dd7d77ddaeb829b10d804e1cbcadbf8b Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 31 Dec 2015 15:21:23 +0100 Subject: [PATCH 0068/2005] Fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 184d7595..b3c93982 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ It’s advisable to run textsecure-cli as a separate unix user, the following st These steps, executed as root, should work on all distributions using systemd. ```bash -cp data/org.asamk.TextSecure.config /etc/dbus-1/system.d/ +cp data/org.asamk.TextSecure.conf /etc/dbus-1/system.d/ cp data/org.asamk.TextSecure.service /usr/share/dbus-1/system-services/ cp data/textsecure.service /etc/systemd/system/ sed -i -e "s|%dir%||" -e "s|%number%||" /etc/systemd/system/textsecure.service From 506bc5df13b35b3f9f94fc9faa099ac8a72ad5b8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 31 Dec 2015 15:21:44 +0100 Subject: [PATCH 0069/2005] Service: use user textsecure-cli --- README.md | 2 +- data/org.asamk.TextSecure.conf | 2 +- data/textsecure.service | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b3c93982..634fa30f 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ For dbus support you need jni/unix-java.so installed on your system (Debian: lib ### System bus To run on the system bus you need to take some additional steps. -It’s advisable to run textsecure-cli as a separate unix user, the following steps assume you created a user named *textsecure*. +It’s advisable to run textsecure-cli as a separate unix user, the following steps assume you created a user named *textsecure-cli*. These steps, executed as root, should work on all distributions using systemd. ```bash diff --git a/data/org.asamk.TextSecure.conf b/data/org.asamk.TextSecure.conf index 5267a799..79883753 100644 --- a/data/org.asamk.TextSecure.conf +++ b/data/org.asamk.TextSecure.conf @@ -3,7 +3,7 @@ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> - + diff --git a/data/textsecure.service b/data/textsecure.service index 2ed892ce..abdb0767 100644 --- a/data/textsecure.service +++ b/data/textsecure.service @@ -4,7 +4,7 @@ Description=Send secure messages to TextSecure/Signal clients [Service] Type=dbus ExecStart=%dir%/bin/textsecure-cli -u %number% daemon --system -User=textsecure +User=textsecure-cli BusName=org.asamk.TextSecure [Install] From 0130585355b82eb10f8d8e836182d9877aa21ab8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 31 Dec 2015 15:39:40 +0100 Subject: [PATCH 0070/2005] Make config path configurable --- data/textsecure-cli@.service | 2 +- data/textsecure.service | 2 +- src/main/java/org/asamk/textsecure/Main.java | 10 +++++++++- src/main/java/org/asamk/textsecure/Manager.java | 12 ++++++++---- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/data/textsecure-cli@.service b/data/textsecure-cli@.service index b8221bc2..002d347b 100644 --- a/data/textsecure-cli@.service +++ b/data/textsecure-cli@.service @@ -7,7 +7,7 @@ After=network.target [Service] Type=dbus -ExecStart=%dir%/bin/textsecure-cli -u %I daemon --system +ExecStart=%dir%/bin/textsecure-cli -u %I --config /var/lib/textsecure-cli daemon --system User=textsecure-cli BusName=org.asamk.TextSecure diff --git a/data/textsecure.service b/data/textsecure.service index abdb0767..b40debcb 100644 --- a/data/textsecure.service +++ b/data/textsecure.service @@ -3,7 +3,7 @@ Description=Send secure messages to TextSecure/Signal clients [Service] Type=dbus -ExecStart=%dir%/bin/textsecure-cli -u %number% daemon --system +ExecStart=%dir%/bin/textsecure-cli -u %number% --config /var/lib/textsecure-cli daemon --system User=textsecure-cli BusName=org.asamk.TextSecure diff --git a/src/main/java/org/asamk/textsecure/Main.java b/src/main/java/org/asamk/textsecure/Main.java index 8a252b99..4227e65e 100644 --- a/src/main/java/org/asamk/textsecure/Main.java +++ b/src/main/java/org/asamk/textsecure/Main.java @@ -20,6 +20,7 @@ import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; import org.apache.commons.io.IOUtils; +import org.apache.http.util.TextUtils; import org.asamk.TextSecure; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; @@ -79,7 +80,12 @@ public class Main { return; } } else { - m = new Manager(username); + String settingsPath = ns.getString("config"); + if (TextUtils.isEmpty(settingsPath)) { + settingsPath = System.getProperty("user.home") + "/.config/textsecure"; + } + + m = new Manager(username, settingsPath); ts = m; if (m.userExists()) { try { @@ -355,6 +361,8 @@ public class Main { parser.addArgument("-v", "--version") .help("Show package version.") .action(Arguments.version()); + parser.addArgument("--config") + .help("Set the path, where to store the config (Default: $HOME/.config/textsecure-cli)."); MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup(); mut.addArgument("-u", "--username") diff --git a/src/main/java/org/asamk/textsecure/Manager.java b/src/main/java/org/asamk/textsecure/Manager.java index 2a2b5e4f..e5a77dcd 100644 --- a/src/main/java/org/asamk/textsecure/Manager.java +++ b/src/main/java/org/asamk/textsecure/Manager.java @@ -59,9 +59,9 @@ class Manager implements TextSecure { public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION; - private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure"; - private final static String dataPath = settingsPath + "/data"; - private final static String attachmentsPath = settingsPath + "/attachments"; + private final String settingsPath; + private final String dataPath; + private final String attachmentsPath; private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; @@ -76,8 +76,12 @@ class Manager implements TextSecure { private TextSecureAccountManager accountManager; private JsonGroupStore groupStore; - public Manager(String username) { + public Manager(String username, String settingsPath) { this.username = username; + this.settingsPath = settingsPath; + this.dataPath = this.settingsPath + "/data"; + this.attachmentsPath = this.settingsPath + "/attachments"; + jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); From ea2b0f9d52c52545eaf61a14dafdee1409e3a480 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 31 Dec 2015 16:16:39 +0100 Subject: [PATCH 0071/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1b1c9e35..45bddee6 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.textsecure.Main' -version = '0.2.0' +version = '0.2.1' repositories { maven { From de4a1d193324174cedd0549cc219d340bc11a900 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 4 Jan 2016 12:11:23 +0100 Subject: [PATCH 0072/2005] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 634fa30f..1f4597d5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # textsecure-cli -textsecure-cli is a commandline interface for [libtextsecure-java](https://github.com/WhisperSystems/libtextsecure-java). It supports registering, verifying, sending and receiving messages. However receiving messages currently only works with a patched libtextsecure-java, because libtextsecure-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libtextsecure-java/pull/5). For registering you need a phone number where you can receive SMS or incoming calls. -It is primarily intended to be used on servers to notify admins of important events. +textsecure-cli is a commandline interface for [libtextsecure-java](https://github.com/WhisperSystems/libtextsecure-java). It supports registering, verifying, sending and receiving messages. To be able to receiving messages textsecure-cli uses a [patched libtextsecure-java](https://github.com/AsamK/libtextsecure-java), because libtextsecure-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libtextsecure-java/pull/5). For registering you need a phone number where you can receive SMS or incoming calls. +It is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings. ## Usage From 11e5dfbcf5cac40cf9eeed447e7a1229f63ef392 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 10 Feb 2016 12:47:31 +0100 Subject: [PATCH 0073/2005] Update gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 53638 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew.bat | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372aef5e24af05341d49695ee84e5f9b594659..5ccda13e9cb94678ba179b32452cf3d60dc36353 100644 GIT binary patch delta 2494 zcmaJ?2~bl<7~Z^uBtnRCgu;^}f}lt#5nC@{)WJe(6oW#cl?qW1Ml(Poo;Y$;utS+r zSgWnrB87@tMvkC(U`06s$TipaX(I0>zsY5lAl+TM7y0A-%<7A`34Qf-SYlVX-#bP!Yl0it;LN zMY;Dvb>dF*V1>8h-3Kr*%6%FiG4t*$oZkWWM7gB+<*Z1klS?w5OniMxd_$PWKXTV? z&1LPK9n^ypAv|Y!*Zx;|XY&)D=nD!SA6ATAw{(y}o%7&MqlI(#mIvBm;95MCF)CDh zZSu%m(%8B2^6%qtS1K*)s}Zfe&s%#d?Plt#9iv(8(u=Xx5gRIVvrA+z!b=N2f9PGQ zz6ZbCIPAG-``PubJF9tZNeNMg(KI6QpePrr{yEPsHW}+|A9HcCm znRjGN&Itg(5PIjruB6#;b96P~IL5r9$V?tL#Lgt!@_1Oc?5((e3?cvyS^?mPb=EER z`X|~h5s}moXpVbsvpY^VO<61lvon+M4^$zjK%lZ*^QF30`hNmDaFU` z&()T1{{{s-EhO&eLAt|6f3NHZR+3Q}!MgcPznm4jpYLF?i3{t=;su-&KlkGU&@v4G zAM7iq%>+uiWcu%G0{LR5Xzj7!L-6sHmE0y&teY)Nea`& zXkP$wiXQNL^XnEw@`Z3V2D$@K{4 z3T)nfGgDl8^p+w->_)E(dS5?PX%0M?)*ypt!o;xksNQ_{LqJ{~ zO^L-g{$jBlhSy85nO5_S0l7#!h8lpYb(A8qJv>_HL;_q!0mf(G0Hby81e;k;Q=T>$ z`@}{v8MIwDoFzkNB3=rB8^=(sEhZantzc=rgaoNULG0uhe_U@zFw+K_(k;N4PlFq- zQrS~I`v%%v7h7~dIYHB4YQrL;yM%N{{jqMO!39@%yWL{rODrz$NL9x%+o2N;$aXr30f7#zvZ z5d%%-YhYiA9czam^PxL1i3!(pZyz! zY1adZnbzwxHJlEQmO4YzR&rlLc`HrTWx?JoUuN}iV$NmDF?S>zB~~(3kbo0N{C*C? zoZ-?oHlVtVri_J9Q!^JHYO^D}3WN<5v&$@m6K$3_(Qdc?kSKI^X>`x{a1t3x(4P8D z?Pqd3O{JD1=kH8b%(=li;!J34f4%%2G_|J^uU1**^vMeSyKDv@p_ST)68mzrxAlnZ z0(&~x2H|zmJO^msslX3scmeA?FIqrF3t2|aw}C?rtU=4;$D9z{ ZNX!=^01=ow(TPLsg{!-~g;BK(_%D0s@`L~Y delta 2495 zcmZWq2~bl<7~VWWh!O%s2=L^Hpj?WCAj;u|SV0*qD0om*aJUMLP_;l+E(K}97G>4Z zmZ=Be!Jr(8wpv6GNVqS>W0Yi&(7NJWP*Z4zjQ*q?hfk z&+>{qP4x+O#_#21G2`b>w{iM$0RSp7EaKDfS+f8JsfEfLD=P#NW;{WbuwTUkV2us{ zo&e^ky_M}@^*o}oZ*8bh$gmnL7OLU!rU3KhL2KlSP`RR3F-NIz3rR^e2$3t~bN++@ zkxr8)Bj)rwhhw_oj!3&KkD@DEn&hI~wE7jsz6Z115A${%mady!^Vl{uqYGIpcldUFnRuP#fETm9UQSNu(;Ao1bz zti^6BL$ll`HA72|UXVC$y8qa-*G9YD%rfDQsF{4GqdXgxPvIV%6Fp2IS*H$(RVhv^<`HgLlC_ZZ)Z495)iIL8Y2IwlerLZf!QN(%vopqb z-_RpiUqLU6mG7${5%bDbW^ob#)Xq@8QwE=H7by1>rAY{X;>0t`n2F%u7dujne3j?| z;0{kzIV+vY&#=}8PG8^?`VW;9YvW+mQ+71JHi8ZLNV%Bh&HS~8#U?JTW_e+C#X3SJ z)R%WN0DO9NDYtSI!P?3RY?#J(MogH};I2Jqu(H8xE>n5{J;*A4_-qS+5qRhOBYI2T z8s{1u$fHbj$a!UEz^RS2Pdz&rw>2s{DdvA1M(Dlj6y^m#jOt_N5J;Ur(Tv|69b1Yx z1L$W*$oOBCV+pGp6*f%Vmb-a3O^|2`vbmJQ0g!iXwT$6B8d$6>#&H>DQ5aJ97XW zIV;HaWKHS8V=XihxPkFOvCN46I|5u|2Kt8Je1svkR zB&=-O?u3b!iPD-bq&A4G&EpU+Zf7kJ#i7ju4u2rx7Noj}L%f}aiRS4xu)c%%wB6Ab zUW_XCr~v@>9I|vL_8GiW1byKHb|&P~gS{GjZEb*FOC%#SZIrXteqAB0Atvrp>- z;KP~N0$saT5N8?PG?iJ2mOA@Gt}ZrIZ##mVRiN$S?rUzE$bFbQPGza0zK80Xo0|WjC DxnnmS diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5d7f1fee..49a5dd8d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Dec 31 11:49:58 CET 2015 +#Wed Feb 10 12:41:27 CET 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip diff --git a/gradlew.bat b/gradlew.bat index aec99730..72d362da 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -46,7 +46,7 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args From 5f4325283ae5cb7ef158984e5d070bc1ac9ac9da Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 16 Feb 2016 13:46:01 +0100 Subject: [PATCH 0074/2005] Set initial java heap size to 2MB in service files Make it more usable in memory restricted environments --- data/textsecure-cli@.service | 1 + data/textsecure.service | 1 + 2 files changed, 2 insertions(+) diff --git a/data/textsecure-cli@.service b/data/textsecure-cli@.service index 002d347b..3e39571e 100644 --- a/data/textsecure-cli@.service +++ b/data/textsecure-cli@.service @@ -7,6 +7,7 @@ After=network.target [Service] Type=dbus +Environment=TEXTSECURE_CLI_OPTS="-Xms2m" ExecStart=%dir%/bin/textsecure-cli -u %I --config /var/lib/textsecure-cli daemon --system User=textsecure-cli BusName=org.asamk.TextSecure diff --git a/data/textsecure.service b/data/textsecure.service index b40debcb..8b302d84 100644 --- a/data/textsecure.service +++ b/data/textsecure.service @@ -3,6 +3,7 @@ Description=Send secure messages to TextSecure/Signal clients [Service] Type=dbus +Environment=TEXTSECURE_CLI_OPTS="-Xms2m" ExecStart=%dir%/bin/textsecure-cli -u %number% --config /var/lib/textsecure-cli daemon --system User=textsecure-cli BusName=org.asamk.TextSecure From 78252a13df63baf83a8bb17b88bae03db708a3d0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 21 Mar 2016 16:01:10 +0100 Subject: [PATCH 0075/2005] Update gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 53638 -> 53639 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5ccda13e9cb94678ba179b32452cf3d60dc36353..2c6137b87896c8f70315ae454e00a969ef5f6019 100644 GIT binary patch delta 1762 zcmY*Z3rv$&6u$inv`UK;1cg>AAP<3I*QyZ#iC_c)5wQ50V{{aR$v}Zv1viU2n4rAw zHXk9|7`Qrh0WIp7z(AnoQJ^@Taf|a2Ky)&#+2S6eyZ^ZaY?J0Y_xrzd&i9{t+M-%+ zaV=LE7tOVri4dQUq%m2QLN7jn$jkc8K9xaR9n3lA91fb6coNBJH!cfCAAsjl7O*ep z9*a6VCYJ%?kktbqvaIWX&^huQY=H5zyG0q^Y^gOcE1W7Q(?4$`4;Zfn8yz6nFBecv z*>WdaV6@@SXF^aDdz%(4Oytq@(oKncK5-G5byoW!9(y<9ji>AU6QoPxr45a;WtU`2 z6gV_lHe()9e0DOx*@W|xJ@zjxZ^`PA3J$4Tqh=RYi36P*^Zepe8K#S-S>rwp3&X39 zuKZ}+>)vk3-r#Ei%4f$sxB9LaS)HujDXe^7zUybEDXb?bcx~Y`;brDnieS8Bhu^@# zi)Z9XTNK{gM>K{StzFB8klihJ?`O`x`sU5gV-}8QjAZ)j*LUVPyIrqWC5`6yt(%p0 z#!9U_neDrDxGwN_=a*k;wk^K$kGyU~?NHyU+9nJB^N}>+YkRTL^G?swiAc@;FTQL~ z`1XawRDG*RRQ%WZ;oFL92X>j6^@g&SuiX}TQM^~_&n2ikt^9;x11wiP1VWPf3J9HB z`a>EBcVG@Ys?C(}A?V7Ja3Of04x)i)!B5t}{HOVsivK=vg9nVMWQa0#N6s>K?2tb` z)i`&%Jwke4EG<}opXS-<4wkF!K|N7prd`c-cWH24d&vqO9X-dT&2arw`l#r_JGAtu zZWYz|es7}8M3aJQ6wR2+XS+6(y0oqhaBl8O1e~L%byfNlIQQyfrgz!Zu=cgJ-DwD62Zb99BF+ccXmEwoxIx5J zE3tII8JmOq(M($4;qUt9gR}lV5%c%} zu0H3E1x8q5>}C`(ohA5AN$}LL4-@M65lHSf${=xqP;1Hw<%16o(kqGY7cu46L2-sK*z`-)^Mgj{S93bIJ-#)}7{ zz{0)(5mR`Mcn_F*_e*UJxyMPrGh_uUZ=|?>s-Jk!o!-izh{?Y|XfYO)&SGB{JckcC zjXol?+ecbkuF)?#sBv@9N5XoObLlMC-@c~YRNFxkX96ALjV35h+ zD2{+Zvr%sKpq9kbB<)Nun7`{umQR(Dsi}T|C`9JO>Vw(zJA~TI_KVuYjpZG z+B8T*o6JW@BtrITb&jc0L_i%~`zkKSYp2zVgy#u7G$%19lCotq1Dz`XUaAwwT(i>w5|IGYWyjL<^G2gcLpdzR^1yh8|#Qoh3q7N^|BtmgcB zn+3p>`n{YFi{dRqY{1k|A!|SPd8kN4s!)f^PcFq{d;J&2YXXB+l|ib?8aGv?n@14# ziEx`o6GiTzhieZ`j&L~To$VXfBp0Vmy}5Wp^hl6PU;14cSf?F4LOr=2!c)lmPR{1u zDu|oX7Zv@Lr+RI)lv?8i#nYqH7K;7@PqaF;TsM|BDF|A<&pCZVYww=A@fnfdZ+xlzjFDU^>CNsOu?nmF*6<(c_Rciezti0&#Gq>uXKk((<6E5o#Z*5wiMSJ#WJQ>MRNPjTyoj+O%YOZ#EY@Y zxE8V(YIpUNlAf;92(9O6CQ~5$Pev)squVHg(uq1!|U1A7>LvfxWxfaC^-+{d|q|wvzPb&IvbN3|`e$ z%T+-d9<_*OKk7`6oR^AY8r5N5$y(?44abxtArU4B*)KrIi(@cgRd)as_f5BiN+~D3 ze)#SWRk(?6uIMXX&PSPF)48_qzEw&>=iDo+C#Q(aQ2$x`Orv#GZ_eiJ# zJv27Z;|K?akyk!5&^N@pf#a28S+5#w2YV&d^gVVS_br&S2D*dL{ Date: Fri, 25 Mar 2016 16:50:51 +0100 Subject: [PATCH 0076/2005] Update to signal-service-java 2.1.1 --- build.gradle | 2 +- src/main/java/org/asamk/TextSecure.java | 2 +- .../asamk/textsecure/JsonAxolotlStore.java | 26 ++-- .../textsecure/JsonIdentityKeyStore.java | 8 +- .../org/asamk/textsecure/JsonPreKeyStore.java | 6 +- .../asamk/textsecure/JsonSessionStore.java | 28 ++-- .../textsecure/JsonSignedPreKeyStore.java | 6 +- src/main/java/org/asamk/textsecure/Main.java | 48 +++---- .../java/org/asamk/textsecure/Manager.java | 124 +++++++++--------- .../asamk/textsecure/WhisperTrustStore.java | 2 +- 10 files changed, 126 insertions(+), 126 deletions(-) diff --git a/build.gradle b/build.gradle index 45bddee6..7d9d9655 100644 --- a/build.gradle +++ b/build.gradle @@ -16,7 +16,7 @@ repositories { } dependencies { - compile 'org.whispersystems:textsecure-java:1.8.3fetchMessages' + compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages' compile 'com.madgag.spongycastle:prov:1.54.0.0' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' diff --git a/src/main/java/org/asamk/TextSecure.java b/src/main/java/org/asamk/TextSecure.java index 3aa514c1..991342b7 100644 --- a/src/main/java/org/asamk/TextSecure.java +++ b/src/main/java/org/asamk/TextSecure.java @@ -5,7 +5,7 @@ import org.asamk.textsecure.GroupNotFoundException; import org.freedesktop.dbus.DBusInterface; import org.freedesktop.dbus.DBusSignal; import org.freedesktop.dbus.exceptions.DBusException; -import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import java.io.IOException; import java.util.List; diff --git a/src/main/java/org/asamk/textsecure/JsonAxolotlStore.java b/src/main/java/org/asamk/textsecure/JsonAxolotlStore.java index f054e337..afc448a2 100644 --- a/src/main/java/org/asamk/textsecure/JsonAxolotlStore.java +++ b/src/main/java/org/asamk/textsecure/JsonAxolotlStore.java @@ -3,18 +3,18 @@ package org.asamk.textsecure; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.whispersystems.libaxolotl.AxolotlAddress; -import org.whispersystems.libaxolotl.IdentityKey; -import org.whispersystems.libaxolotl.IdentityKeyPair; -import org.whispersystems.libaxolotl.InvalidKeyIdException; -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.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.InvalidKeyIdException; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SessionRecord; +import org.whispersystems.libsignal.state.SignalProtocolStore; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; import java.util.List; -class JsonAxolotlStore implements AxolotlStore { +class JsonAxolotlStore implements SignalProtocolStore { @JsonProperty("preKeys") @JsonDeserialize(using = JsonPreKeyStore.JsonPreKeyStoreDeserializer.class) @@ -94,7 +94,7 @@ class JsonAxolotlStore implements AxolotlStore { } @Override - public SessionRecord loadSession(AxolotlAddress address) { + public SessionRecord loadSession(SignalProtocolAddress address) { return sessionStore.loadSession(address); } @@ -104,17 +104,17 @@ class JsonAxolotlStore implements AxolotlStore { } @Override - public void storeSession(AxolotlAddress address, SessionRecord record) { + public void storeSession(SignalProtocolAddress address, SessionRecord record) { sessionStore.storeSession(address, record); } @Override - public boolean containsSession(AxolotlAddress address) { + public boolean containsSession(SignalProtocolAddress address) { return sessionStore.containsSession(address); } @Override - public void deleteSession(AxolotlAddress address) { + public void deleteSession(SignalProtocolAddress address) { sessionStore.deleteSession(address); } diff --git a/src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java b/src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java index 050158fc..eaf97388 100644 --- a/src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java @@ -4,10 +4,10 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; -import org.whispersystems.libaxolotl.IdentityKey; -import org.whispersystems.libaxolotl.IdentityKeyPair; -import org.whispersystems.libaxolotl.InvalidKeyException; -import org.whispersystems.libaxolotl.state.IdentityKeyStore; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.state.IdentityKeyStore; import java.io.IOException; import java.util.HashMap; diff --git a/src/main/java/org/asamk/textsecure/JsonPreKeyStore.java b/src/main/java/org/asamk/textsecure/JsonPreKeyStore.java index 9688cf3e..a522f177 100644 --- a/src/main/java/org/asamk/textsecure/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/textsecure/JsonPreKeyStore.java @@ -4,9 +4,9 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; -import org.whispersystems.libaxolotl.InvalidKeyIdException; -import org.whispersystems.libaxolotl.state.PreKeyRecord; -import org.whispersystems.libaxolotl.state.PreKeyStore; +import org.whispersystems.libsignal.InvalidKeyIdException; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.PreKeyStore; import java.io.IOException; import java.util.HashMap; diff --git a/src/main/java/org/asamk/textsecure/JsonSessionStore.java b/src/main/java/org/asamk/textsecure/JsonSessionStore.java index db352d3b..2fec85a3 100644 --- a/src/main/java/org/asamk/textsecure/JsonSessionStore.java +++ b/src/main/java/org/asamk/textsecure/JsonSessionStore.java @@ -4,28 +4,28 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; -import org.whispersystems.libaxolotl.AxolotlAddress; -import org.whispersystems.libaxolotl.state.SessionRecord; -import org.whispersystems.libaxolotl.state.SessionStore; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.state.SessionRecord; +import org.whispersystems.libsignal.state.SessionStore; import java.io.IOException; import java.util.*; class JsonSessionStore implements SessionStore { - private final Map sessions = new HashMap<>(); + private final Map sessions = new HashMap<>(); public JsonSessionStore() { } - public void addSessions(Map sessions) { + public void addSessions(Map sessions) { this.sessions.putAll(sessions); } @Override - public synchronized SessionRecord loadSession(AxolotlAddress remoteAddress) { + public synchronized SessionRecord loadSession(SignalProtocolAddress remoteAddress) { try { if (containsSession(remoteAddress)) { return new SessionRecord(sessions.get(remoteAddress)); @@ -41,7 +41,7 @@ class JsonSessionStore implements SessionStore { public synchronized List getSubDeviceSessions(String name) { List deviceIds = new LinkedList<>(); - for (AxolotlAddress key : sessions.keySet()) { + for (SignalProtocolAddress key : sessions.keySet()) { if (key.getName().equals(name) && key.getDeviceId() != 1) { deviceIds.add(key.getDeviceId()); @@ -52,23 +52,23 @@ class JsonSessionStore implements SessionStore { } @Override - public synchronized void storeSession(AxolotlAddress address, SessionRecord record) { + public synchronized void storeSession(SignalProtocolAddress address, SessionRecord record) { sessions.put(address, record.serialize()); } @Override - public synchronized boolean containsSession(AxolotlAddress address) { + public synchronized boolean containsSession(SignalProtocolAddress address) { return sessions.containsKey(address); } @Override - public synchronized void deleteSession(AxolotlAddress address) { + public synchronized void deleteSession(SignalProtocolAddress address) { sessions.remove(address); } @Override public synchronized void deleteAllSessions(String name) { - for (AxolotlAddress key : new ArrayList<>(sessions.keySet())) { + for (SignalProtocolAddress key : new ArrayList<>(sessions.keySet())) { if (key.getName().equals(name)) { sessions.remove(key); } @@ -81,12 +81,12 @@ class JsonSessionStore implements SessionStore { public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - Map sessionMap = new HashMap<>(); + Map sessionMap = new HashMap<>(); if (node.isArray()) { for (JsonNode session : node) { String sessionName = session.get("name").asText(); try { - sessionMap.put(new AxolotlAddress(sessionName, session.get("deviceId").asInt()), Base64.decode(session.get("record").asText())); + sessionMap.put(new SignalProtocolAddress(sessionName, session.get("deviceId").asInt()), Base64.decode(session.get("record").asText())); } catch (IOException e) { System.out.println(String.format("Error while decoding session for: %s", sessionName)); } @@ -106,7 +106,7 @@ class JsonSessionStore implements SessionStore { @Override public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { json.writeStartArray(); - for (Map.Entry preKey : jsonSessionStore.sessions.entrySet()) { + for (Map.Entry preKey : jsonSessionStore.sessions.entrySet()) { json.writeStartObject(); json.writeStringField("name", preKey.getKey().getName()); json.writeNumberField("deviceId", preKey.getKey().getDeviceId()); diff --git a/src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java index d8dbeb9a..f890fe88 100644 --- a/src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java @@ -4,9 +4,9 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; -import org.whispersystems.libaxolotl.InvalidKeyIdException; -import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; -import org.whispersystems.libaxolotl.state.SignedPreKeyStore; +import org.whispersystems.libsignal.InvalidKeyIdException; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyStore; import java.io.IOException; import java.util.HashMap; diff --git a/src/main/java/org/asamk/textsecure/Main.java b/src/main/java/org/asamk/textsecure/Main.java index 4227e65e..404a8988 100644 --- a/src/main/java/org/asamk/textsecure/Main.java +++ b/src/main/java/org/asamk/textsecure/Main.java @@ -25,13 +25,13 @@ import org.asamk.TextSecure; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; -import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException; -import org.whispersystems.textsecure.api.messages.*; -import org.whispersystems.textsecure.api.messages.multidevice.TextSecureSyncMessage; -import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.textsecure.api.push.exceptions.NetworkFailureException; -import org.whispersystems.textsecure.api.push.exceptions.UnregisteredUserException; -import org.whispersystems.textsecure.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.File; import java.io.IOException; @@ -486,18 +486,18 @@ public class Main { } @Override - public void handleMessage(TextSecureEnvelope envelope, TextSecureContent content, GroupInfo group) { + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) { System.out.println("Envelope from: " + envelope.getSource()); System.out.println("Timestamp: " + envelope.getTimestamp()); if (envelope.isReceipt()) { System.out.println("Got receipt."); - } else if (envelope.isWhisperMessage() | envelope.isPreKeyWhisperMessage()) { + } else if (envelope.isSignalMessage() | envelope.isPreKeySignalMessage()) { if (content == null) { System.out.println("Failed to decrypt message."); } else { if (content.getDataMessage().isPresent()) { - TextSecureDataMessage message = content.getDataMessage().get(); + SignalServiceDataMessage message = content.getDataMessage().get(); System.out.println("Message timestamp: " + message.getTimestamp()); @@ -505,7 +505,7 @@ public class Main { System.out.println("Body: " + message.getBody().get()); } if (message.getGroupInfo().isPresent()) { - TextSecureGroup groupInfo = message.getGroupInfo().get(); + SignalServiceGroup groupInfo = message.getGroupInfo().get(); System.out.println("Group info:"); System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); if (groupInfo.getName().isPresent()) { @@ -532,13 +532,13 @@ public class Main { if (message.getAttachments().isPresent()) { System.out.println("Attachments: "); - for (TextSecureAttachment attachment : message.getAttachments().get()) { + for (SignalServiceAttachment attachment : message.getAttachments().get()) { printAttachment(attachment); } } } if (content.getSyncMessage().isPresent()) { - TextSecureSyncMessage syncMessage = content.getSyncMessage().get(); + SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); System.out.println("Received sync message"); } } @@ -548,10 +548,10 @@ public class Main { System.out.println(); } - private void printAttachment(TextSecureAttachment attachment) { + private void printAttachment(SignalServiceAttachment attachment) { System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); if (attachment.isPointer()) { - final TextSecureAttachmentPointer pointer = attachment.asPointer(); + final SignalServiceAttachmentPointer pointer = attachment.asPointer(); System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); File file = m.getAttachmentFile(pointer.getId()); @@ -572,18 +572,18 @@ public class Main { } @Override - public void handleMessage(TextSecureEnvelope envelope, TextSecureContent content, GroupInfo group) { + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) { System.out.println("Envelope from: " + envelope.getSource()); System.out.println("Timestamp: " + envelope.getTimestamp()); if (envelope.isReceipt()) { System.out.println("Got receipt."); - } else if (envelope.isWhisperMessage() | envelope.isPreKeyWhisperMessage()) { + } else if (envelope.isSignalMessage() | envelope.isPreKeySignalMessage()) { if (content == null) { System.out.println("Failed to decrypt message."); } else { if (content.getDataMessage().isPresent()) { - TextSecureDataMessage message = content.getDataMessage().get(); + SignalServiceDataMessage message = content.getDataMessage().get(); System.out.println("Message timestamp: " + message.getTimestamp()); @@ -592,7 +592,7 @@ public class Main { } if (message.getGroupInfo().isPresent()) { - TextSecureGroup groupInfo = message.getGroupInfo().get(); + SignalServiceGroup groupInfo = message.getGroupInfo().get(); System.out.println("Group info:"); System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); if (groupInfo.getName().isPresent()) { @@ -620,7 +620,7 @@ public class Main { List attachments = new ArrayList<>(); if (message.getAttachments().isPresent()) { System.out.println("Attachments: "); - for (TextSecureAttachment attachment : message.getAttachments().get()) { + for (SignalServiceAttachment attachment : message.getAttachments().get()) { if (attachment.isPointer()) { attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); } @@ -628,7 +628,7 @@ public class Main { } } if (!message.isEndSession() && - !(message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() != TextSecureGroup.Type.DELIVER)) { + !(message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { try { conn.sendSignal(new TextSecure.MessageReceived( TEXTSECURE_OBJECTPATH, @@ -642,7 +642,7 @@ public class Main { } } if (content.getSyncMessage().isPresent()) { - TextSecureSyncMessage syncMessage = content.getSyncMessage().get(); + SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); System.out.println("Received sync message"); } } @@ -652,10 +652,10 @@ public class Main { System.out.println(); } - private void printAttachment(TextSecureAttachment attachment) { + private void printAttachment(SignalServiceAttachment attachment) { System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); if (attachment.isPointer()) { - final TextSecureAttachmentPointer pointer = attachment.asPointer(); + final SignalServiceAttachmentPointer pointer = attachment.asPointer(); System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); File file = m.getAttachmentFile(pointer.getId()); diff --git a/src/main/java/org/asamk/textsecure/Manager.java b/src/main/java/org/asamk/textsecure/Manager.java index e5a77dcd..77c46e9b 100644 --- a/src/main/java/org/asamk/textsecure/Manager.java +++ b/src/main/java/org/asamk/textsecure/Manager.java @@ -24,25 +24,25 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; import org.asamk.TextSecure; -import org.whispersystems.libaxolotl.*; -import org.whispersystems.libaxolotl.ecc.Curve; -import org.whispersystems.libaxolotl.ecc.ECKeyPair; -import org.whispersystems.libaxolotl.state.PreKeyRecord; -import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; -import org.whispersystems.libaxolotl.util.KeyHelper; -import org.whispersystems.libaxolotl.util.Medium; -import org.whispersystems.libaxolotl.util.guava.Optional; -import org.whispersystems.textsecure.api.TextSecureAccountManager; -import org.whispersystems.textsecure.api.TextSecureMessagePipe; -import org.whispersystems.textsecure.api.TextSecureMessageReceiver; -import org.whispersystems.textsecure.api.TextSecureMessageSender; -import org.whispersystems.textsecure.api.crypto.TextSecureCipher; -import org.whispersystems.textsecure.api.messages.*; -import org.whispersystems.textsecure.api.push.TextSecureAddress; -import org.whispersystems.textsecure.api.push.TrustStore; -import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.textsecure.api.util.InvalidNumberException; -import org.whispersystems.textsecure.api.util.PhoneNumberFormatter; +import org.whispersystems.libsignal.*; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECKeyPair; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.libsignal.util.KeyHelper; +import org.whispersystems.libsignal.util.Medium; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalServiceAccountManager; +import org.whispersystems.signalservice.api.SignalServiceMessagePipe; +import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; +import org.whispersystems.signalservice.api.SignalServiceMessageSender; +import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; +import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.TrustStore; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.*; import java.nio.file.Files; @@ -52,7 +52,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; class Manager implements TextSecure { - private final static String URL = "https://textsecure-service.whispersystems.org"; + private final static String URL = "https://SignalService-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); @@ -73,7 +73,7 @@ class Manager implements TextSecure { private boolean registered = false; private JsonAxolotlStore axolotlStore; - private TextSecureAccountManager accountManager; + private SignalServiceAccountManager accountManager; private JsonGroupStore groupStore; public Manager(String username, String settingsPath) { @@ -138,7 +138,7 @@ class Manager implements TextSecure { if (groupStore == null) { groupStore = new JsonGroupStore(); } - accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); } private void save() { @@ -175,7 +175,7 @@ class Manager implements TextSecure { public void register(boolean voiceVerication) throws IOException { password = Util.getSecret(18); - accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); if (voiceVerication) accountManager.requestVoiceVerificationCode(); @@ -259,54 +259,54 @@ class Manager implements TextSecure { } - private static List getTextSecureAttachments(List attachments) throws AttachmentInvalidException { - List textSecureAttachments = null; + private static List getSignalServiceAttachments(List attachments) throws AttachmentInvalidException { + List SignalServiceAttachments = null; if (attachments != null) { - textSecureAttachments = new ArrayList<>(attachments.size()); + SignalServiceAttachments = new ArrayList<>(attachments.size()); for (String attachment : attachments) { try { - textSecureAttachments.add(createAttachment(attachment)); + SignalServiceAttachments.add(createAttachment(attachment)); } catch (IOException e) { throw new AttachmentInvalidException(attachment, e); } } } - return textSecureAttachments; + return SignalServiceAttachments; } - private static TextSecureAttachmentStream createAttachment(String attachment) throws IOException { + private static SignalServiceAttachmentStream createAttachment(String attachment) throws IOException { File attachmentFile = new File(attachment); InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); String mime = Files.probeContentType(Paths.get(attachment)); - return new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null); } @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { - final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); + final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { - messageBuilder.withAttachments(getTextSecureAttachments(attachments)); + messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); } if (groupId != null) { - TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.DELIVER) + SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) .withId(groupId) .build(); messageBuilder.asGroupMessage(group); } - TextSecureDataMessage message = messageBuilder.build(); + SignalServiceDataMessage message = messageBuilder.build(); sendMessage(message, groupStore.getGroup(groupId).members); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { - TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.QUIT) + SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); - TextSecureDataMessage message = TextSecureDataMessage.newBuilder() + SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() .asGroupMessage(group) .build(); @@ -339,7 +339,7 @@ class Manager implements TextSecure { } } - TextSecureGroup.Builder group = TextSecureGroup.newBuilder(TextSecureGroup.Type.UPDATE) + SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) .withId(g.groupId) .withName(g.name) .withMembers(new ArrayList<>(g.members)); @@ -356,7 +356,7 @@ class Manager implements TextSecure { groupStore.updateGroup(g); - TextSecureDataMessage message = TextSecureDataMessage.newBuilder() + SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()) .build(); @@ -376,30 +376,30 @@ class Manager implements TextSecure { public void sendMessage(String messageText, List attachments, List recipients) throws IOException, EncapsulatedExceptions, AttachmentInvalidException { - final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText); + final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { - messageBuilder.withAttachments(getTextSecureAttachments(attachments)); + messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); } - TextSecureDataMessage message = messageBuilder.build(); + SignalServiceDataMessage message = messageBuilder.build(); sendMessage(message, recipients); } @Override public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { - TextSecureDataMessage message = TextSecureDataMessage.newBuilder() + SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() .asEndSessionMessage() .build(); sendMessage(message, recipients); } - private void sendMessage(TextSecureDataMessage message, Collection recipients) + private void sendMessage(SignalServiceDataMessage message, Collection recipients) throws IOException, EncapsulatedExceptions { - TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password, - axolotlStore, USER_AGENT, Optional.absent()); + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, + axolotlStore, USER_AGENT, Optional.absent()); - Set recipientsTS = new HashSet<>(recipients.size()); + Set recipientsTS = new HashSet<>(recipients.size()); for (String recipient : recipients) { try { recipientsTS.add(getPushAddress(recipient)); @@ -414,15 +414,15 @@ class Manager implements TextSecure { messageSender.sendMessage(new ArrayList<>(recipientsTS), message); if (message.isEndSession()) { - for (TextSecureAddress recipient : recipientsTS) { + for (SignalServiceAddress recipient : recipientsTS) { handleEndSession(recipient.getNumber()); } } save(); } - private TextSecureContent decryptMessage(TextSecureEnvelope envelope) { - TextSecureCipher cipher = new TextSecureCipher(new TextSecureAddress(username), axolotlStore); + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) { + SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), axolotlStore); try { return cipher.decrypt(envelope); } catch (Exception e) { @@ -437,19 +437,19 @@ class Manager implements TextSecure { } public interface ReceiveMessageHandler { - void handleMessage(TextSecureEnvelope envelope, TextSecureContent decryptedContent, GroupInfo group); + void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group); } public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { - final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); - TextSecureMessagePipe messagePipe = null; + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); + SignalServiceMessagePipe messagePipe = null; try { messagePipe = messageReceiver.createMessagePipe(); while (true) { - TextSecureEnvelope envelope; - TextSecureContent content = null; + SignalServiceEnvelope envelope; + SignalServiceContent content = null; GroupInfo group = null; try { envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS); @@ -457,9 +457,9 @@ class Manager implements TextSecure { content = decryptMessage(envelope); if (content != null) { if (content.getDataMessage().isPresent()) { - TextSecureDataMessage message = content.getDataMessage().get(); + SignalServiceDataMessage message = content.getDataMessage().get(); if (message.getGroupInfo().isPresent()) { - TextSecureGroup groupInfo = message.getGroupInfo().get(); + SignalServiceGroup groupInfo = message.getGroupInfo().get(); switch (groupInfo.getType()) { case UPDATE: try { @@ -469,7 +469,7 @@ class Manager implements TextSecure { } if (groupInfo.getAvatar().isPresent()) { - TextSecureAttachment avatar = groupInfo.getAvatar().get(); + SignalServiceAttachment avatar = groupInfo.getAvatar().get(); if (avatar.isPointer()) { long avatarId = avatar.asPointer().getId(); try { @@ -510,7 +510,7 @@ class Manager implements TextSecure { handleEndSession(envelope.getSource()); } if (message.getAttachments().isPresent()) { - for (TextSecureAttachment attachment : message.getAttachments().get()) { + for (SignalServiceAttachment attachment : message.getAttachments().get()) { if (attachment.isPointer()) { try { retrieveAttachment(attachment.asPointer()); @@ -542,8 +542,8 @@ class Manager implements TextSecure { return new File(attachmentsPath, attachmentId + ""); } - private File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException { - final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); + private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); @@ -594,9 +594,9 @@ class Manager implements TextSecure { return PhoneNumberFormatter.formatNumber(number, localNumber); } - private TextSecureAddress getPushAddress(String number) throws InvalidNumberException { + private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException { String e164number = canonicalizeNumber(number); - return new TextSecureAddress(e164number); + return new SignalServiceAddress(e164number); } @Override diff --git a/src/main/java/org/asamk/textsecure/WhisperTrustStore.java b/src/main/java/org/asamk/textsecure/WhisperTrustStore.java index 48d96e8f..1f129c5a 100644 --- a/src/main/java/org/asamk/textsecure/WhisperTrustStore.java +++ b/src/main/java/org/asamk/textsecure/WhisperTrustStore.java @@ -1,6 +1,6 @@ package org.asamk.textsecure; -import org.whispersystems.textsecure.api.push.TrustStore; +import org.whispersystems.signalservice.api.push.TrustStore; import java.io.InputStream; From 10719a443a88d06ef5734f0e17f71316b1473edf Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 25 Mar 2016 16:57:40 +0100 Subject: [PATCH 0077/2005] Rename axolotl -> signalProtocol --- ...tore.java => JsonSignalProtocolStore.java} | 8 ++--- .../java/org/asamk/textsecure/Manager.java | 35 ++++++++++--------- 2 files changed, 22 insertions(+), 21 deletions(-) rename src/main/java/org/asamk/textsecure/{JsonAxolotlStore.java => JsonSignalProtocolStore.java} (92%) diff --git a/src/main/java/org/asamk/textsecure/JsonAxolotlStore.java b/src/main/java/org/asamk/textsecure/JsonSignalProtocolStore.java similarity index 92% rename from src/main/java/org/asamk/textsecure/JsonAxolotlStore.java rename to src/main/java/org/asamk/textsecure/JsonSignalProtocolStore.java index afc448a2..f440a709 100644 --- a/src/main/java/org/asamk/textsecure/JsonAxolotlStore.java +++ b/src/main/java/org/asamk/textsecure/JsonSignalProtocolStore.java @@ -14,7 +14,7 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; import java.util.List; -class JsonAxolotlStore implements SignalProtocolStore { +class JsonSignalProtocolStore implements SignalProtocolStore { @JsonProperty("preKeys") @JsonDeserialize(using = JsonPreKeyStore.JsonPreKeyStoreDeserializer.class) @@ -36,17 +36,17 @@ class JsonAxolotlStore implements SignalProtocolStore { @JsonSerialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreSerializer.class) protected JsonIdentityKeyStore identityKeyStore; - public JsonAxolotlStore() { + public JsonSignalProtocolStore() { } - public JsonAxolotlStore(JsonPreKeyStore preKeyStore, JsonSessionStore sessionStore, JsonSignedPreKeyStore signedPreKeyStore, JsonIdentityKeyStore identityKeyStore) { + public JsonSignalProtocolStore(JsonPreKeyStore preKeyStore, JsonSessionStore sessionStore, JsonSignedPreKeyStore signedPreKeyStore, JsonIdentityKeyStore identityKeyStore) { this.preKeyStore = preKeyStore; this.sessionStore = sessionStore; this.signedPreKeyStore = signedPreKeyStore; this.identityKeyStore = identityKeyStore; } - public JsonAxolotlStore(IdentityKeyPair identityKeyPair, int registrationId) { + public JsonSignalProtocolStore(IdentityKeyPair identityKeyPair, int registrationId) { preKeyStore = new JsonPreKeyStore(); sessionStore = new JsonSessionStore(); signedPreKeyStore = new JsonSignedPreKeyStore(); diff --git a/src/main/java/org/asamk/textsecure/Manager.java b/src/main/java/org/asamk/textsecure/Manager.java index 77c46e9b..2d675170 100644 --- a/src/main/java/org/asamk/textsecure/Manager.java +++ b/src/main/java/org/asamk/textsecure/Manager.java @@ -28,6 +28,7 @@ import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.libsignal.util.Medium; @@ -72,7 +73,7 @@ class Manager implements TextSecure { private boolean registered = false; - private JsonAxolotlStore axolotlStore; + private SignalProtocolStore signalProtocolStore; private SignalServiceAccountManager accountManager; private JsonGroupStore groupStore; @@ -99,7 +100,7 @@ class Manager implements TextSecure { } public boolean userHasKeys() { - return axolotlStore != null; + return signalProtocolStore != null; } private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException { @@ -129,7 +130,7 @@ class Manager implements TextSecure { } else { nextSignedPreKeyId = 0; } - axolotlStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonAxolotlStore.class); //new JsonAxolotlStore(in.getJSONObject("axolotlStore")); + signalProtocolStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); registered = getNotNullNode(rootNode, "registered").asBoolean(); JsonNode groupStoreNode = rootNode.get("groupStore"); if (groupStoreNode != null) { @@ -149,7 +150,7 @@ class Manager implements TextSecure { .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) .put("registered", registered) - .putPOJO("axolotlStore", axolotlStore) + .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) ; try { @@ -162,7 +163,7 @@ class Manager implements TextSecure { public void createNewIdentity() { IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); int registrationId = KeyHelper.generateRegistrationId(false); - axolotlStore = new JsonAxolotlStore(identityKey, registrationId); + signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); groupStore = new JsonGroupStore(); registered = false; save(); @@ -196,7 +197,7 @@ class Manager implements TextSecure { ECKeyPair keyPair = Curve.generateKeyPair(); PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); - axolotlStore.storePreKey(preKeyId, record); + signalProtocolStore.storePreKey(preKeyId, record); records.add(record); } @@ -207,18 +208,18 @@ class Manager implements TextSecure { } private PreKeyRecord generateLastResortPreKey() { - if (axolotlStore.containsPreKey(Medium.MAX_VALUE)) { + if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) { try { - return axolotlStore.loadPreKey(Medium.MAX_VALUE); + return signalProtocolStore.loadPreKey(Medium.MAX_VALUE); } catch (InvalidKeyIdException e) { - axolotlStore.removePreKey(Medium.MAX_VALUE); + signalProtocolStore.removePreKey(Medium.MAX_VALUE); } } ECKeyPair keyPair = Curve.generateKeyPair(); PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair); - axolotlStore.storePreKey(Medium.MAX_VALUE, record); + signalProtocolStore.storePreKey(Medium.MAX_VALUE, record); save(); return record; @@ -230,7 +231,7 @@ class Manager implements TextSecure { byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize()); SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature); - axolotlStore.storeSignedPreKey(nextSignedPreKeyId, record); + signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record); nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE; save(); @@ -243,7 +244,7 @@ class Manager implements TextSecure { public void verifyAccount(String verificationCode) throws IOException { verificationCode = verificationCode.replace("-", ""); signalingKey = Util.getSecret(52); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId(), false, true); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, true); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; @@ -252,9 +253,9 @@ class Manager implements TextSecure { PreKeyRecord lastResortKey = generateLastResortPreKey(); - SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(axolotlStore.getIdentityKeyPair()); + SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair()); - accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); + accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); save(); } @@ -397,7 +398,7 @@ class Manager implements TextSecure { private void sendMessage(SignalServiceDataMessage message, Collection recipients) throws IOException, EncapsulatedExceptions { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, - axolotlStore, USER_AGENT, Optional.absent()); + signalProtocolStore, USER_AGENT, Optional.absent()); Set recipientsTS = new HashSet<>(recipients.size()); for (String recipient : recipients) { @@ -422,7 +423,7 @@ class Manager implements TextSecure { } private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) { - SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), axolotlStore); + SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore); try { return cipher.decrypt(envelope); } catch (Exception e) { @@ -433,7 +434,7 @@ class Manager implements TextSecure { } private void handleEndSession(String source) { - axolotlStore.deleteAllSessions(source); + signalProtocolStore.deleteAllSessions(source); } public interface ReceiveMessageHandler { From eabd361405a54a5b7122bf537cb299306f098e45 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 27 Mar 2016 13:53:04 +0200 Subject: [PATCH 0078/2005] Rename to signal-cli Changes experimental dbus interface from org.asamk.TextSecure to org.asamk.Signal --- README.md | 46 +++++++++--------- build.gradle | 2 +- data/org.asamk.Signal.conf | 16 ++++++ data/org.asamk.Signal.service | 4 ++ data/org.asamk.TextSecure.conf | 16 ------ data/org.asamk.TextSecure.service | 4 -- data/signal-cli@.service | 16 ++++++ data/signal.service | 12 +++++ data/textsecure-cli@.service | 16 ------ data/textsecure.service | 12 ----- settings.gradle | 2 +- .../asamk/{TextSecure.java => Signal.java} | 6 +-- .../AttachmentInvalidException.java | 2 +- .../asamk/{textsecure => signal}/Base64.java | 2 +- .../{textsecure => signal}/GroupInfo.java | 2 +- .../GroupNotFoundException.java | 2 +- .../JsonGroupStore.java | 2 +- .../JsonIdentityKeyStore.java | 2 +- .../JsonPreKeyStore.java | 2 +- .../JsonSessionStore.java | 2 +- .../JsonSignalProtocolStore.java | 2 +- .../JsonSignedPreKeyStore.java | 2 +- .../asamk/{textsecure => signal}/Main.java | 32 ++++++------ .../asamk/{textsecure => signal}/Manager.java | 8 +-- .../asamk/{textsecure => signal}/Util.java | 2 +- .../WhisperTrustStore.java | 2 +- .../{textsecure => signal}/whisper.store | Bin 27 files changed, 108 insertions(+), 108 deletions(-) create mode 100644 data/org.asamk.Signal.conf create mode 100644 data/org.asamk.Signal.service delete mode 100644 data/org.asamk.TextSecure.conf delete mode 100644 data/org.asamk.TextSecure.service create mode 100644 data/signal-cli@.service create mode 100644 data/signal.service delete mode 100644 data/textsecure-cli@.service delete mode 100644 data/textsecure.service rename src/main/java/org/asamk/{TextSecure.java => Signal.java} (87%) rename src/main/java/org/asamk/{textsecure => signal}/AttachmentInvalidException.java (92%) rename src/main/java/org/asamk/{textsecure => signal}/Base64.java (99%) rename src/main/java/org/asamk/{textsecure => signal}/GroupInfo.java (96%) rename src/main/java/org/asamk/{textsecure => signal}/GroupNotFoundException.java (91%) rename src/main/java/org/asamk/{textsecure => signal}/JsonGroupStore.java (98%) rename src/main/java/org/asamk/{textsecure => signal}/JsonIdentityKeyStore.java (99%) rename src/main/java/org/asamk/{textsecure => signal}/JsonPreKeyStore.java (99%) rename src/main/java/org/asamk/{textsecure => signal}/JsonSessionStore.java (99%) rename src/main/java/org/asamk/{textsecure => signal}/JsonSignalProtocolStore.java (99%) rename src/main/java/org/asamk/{textsecure => signal}/JsonSignedPreKeyStore.java (99%) rename src/main/java/org/asamk/{textsecure => signal}/Main.java (97%) rename src/main/java/org/asamk/{textsecure => signal}/Manager.java (99%) rename src/main/java/org/asamk/{textsecure => signal}/Util.java (95%) rename src/main/java/org/asamk/{textsecure => signal}/WhisperTrustStore.java (92%) rename src/main/resources/org/asamk/{textsecure => signal}/whisper.store (100%) diff --git a/README.md b/README.md index 1f4597d5..f951669d 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,72 @@ -# textsecure-cli +# signal-cli -textsecure-cli is a commandline interface for [libtextsecure-java](https://github.com/WhisperSystems/libtextsecure-java). It supports registering, verifying, sending and receiving messages. To be able to receiving messages textsecure-cli uses a [patched libtextsecure-java](https://github.com/AsamK/libtextsecure-java), because libtextsecure-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libtextsecure-java/pull/5). For registering you need a phone number where you can receive SMS or incoming calls. +signal-cli is a commandline interface for [libtextsecure-java](https://github.com/WhisperSystems/libtextsecure-java). It supports registering, verifying, sending and receiving messages. To be able to receiving messages signal-cli uses a [patched libtextsecure-java](https://github.com/AsamK/libtextsecure-java), because libtextsecure-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libtextsecure-java/pull/5). For registering you need a phone number where you can receive SMS or incoming calls. It is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings. ## Usage -usage: textsecure-cli [-h] [-u USERNAME] [-v] {register,verify,send,quitGroup,updateGroup,receive} ... +usage: signal-cli [-h] [-u USERNAME] [-v] {register,verify,send,quitGroup,updateGroup,receive} ... * Register a number (with SMS verification) - textsecure-cli -u USERNAME register + signal-cli -u USERNAME register * Register a number (with voice verification) - textsecure-cli -u USERNAME register -v + signal-cli -u USERNAME register -v * Verify the number using the code received via SMS or voice - textsecure-cli -u USERNAME verify CODE + signal-cli -u USERNAME verify CODE * Send a message to one or more recipients - textsecure-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] + signal-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] * Pipe the message content from another process. - uname -a | textsecure-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]] + uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]] * Groups * Create a group - textsecure-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]] + signal-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]] * Update a group - textsecure-cli -u USERNAME updateGroup -g GROUP_ID -n "New group name" + signal-cli -u USERNAME updateGroup -g GROUP_ID -n "New group name" * Send a message to a group - textsecure-cli -u USERNAME send -m "This is a message" -g GROUP_ID + signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID ## DBus service -textsecure-cli can run in daemon mode and provides an experimental dbus interface. +signal-cli can run in daemon mode and provides an experimental dbus interface. For dbus support you need jni/unix-java.so installed on your system (Debian: libunixsocket-java ArchLinux: libmatthew-unix-java (AUR)). * Run in daemon mode (dbus session bus) - textsecure-cli -u USERNAME daemon + signal-cli -u USERNAME daemon * Send a message via dbus - textsecure-cli --dbus send -m "Message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] + signal-cli --dbus send -m "Message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] ### System bus To run on the system bus you need to take some additional steps. -It’s advisable to run textsecure-cli as a separate unix user, the following steps assume you created a user named *textsecure-cli*. +It’s advisable to run signal-cli as a separate unix user, the following steps assume you created a user named *signal-cli*. These steps, executed as root, should work on all distributions using systemd. ```bash -cp data/org.asamk.TextSecure.conf /etc/dbus-1/system.d/ -cp data/org.asamk.TextSecure.service /usr/share/dbus-1/system-services/ -cp data/textsecure.service /etc/systemd/system/ -sed -i -e "s|%dir%||" -e "s|%number%||" /etc/systemd/system/textsecure.service +cp data/org.asamk.Signal.conf /etc/dbus-1/system.d/ +cp data/org.asamk.Signal.service /usr/share/dbus-1/system-services/ +cp data/signal.service /etc/systemd/system/ +sed -i -e "s|%dir%||" -e "s|%number%||" /etc/systemd/system/signal.service systemctl daemon-reload -systemctl enable textsecure.service +systemctl enable signal.service systemctl reload dbus.service ``` @@ -76,7 +76,7 @@ Then just execute the send command from above, the service will be autostarted b The password and cryptographic keys are created when registering and stored in the current users home directory: - $HOME/.config/textsecure/data/ + $HOME/.config/signal/data/ ## Building @@ -85,13 +85,13 @@ dependencies. 1. Checkout the source somewhere on your filesystem with - git clone https://github.com/AsamK/textsecure-cli.git + git clone https://github.com/AsamK/signal-cli.git 2. Execute Gradle: ./gradlew build -3. Create shell wrapper in *build/install/textsecure-cli/bin*: +3. Create shell wrapper in *build/install/signal-cli/bin*: ./gradlew installDist diff --git a/build.gradle b/build.gradle index 7d9d9655..8402bd05 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ apply plugin: 'application' sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 -mainClassName = 'org.asamk.textsecure.Main' +mainClassName = 'org.asamk.signal.Main' version = '0.2.1' diff --git a/data/org.asamk.Signal.conf b/data/org.asamk.Signal.conf new file mode 100644 index 00000000..a30c5013 --- /dev/null +++ b/data/org.asamk.Signal.conf @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + diff --git a/data/org.asamk.Signal.service b/data/org.asamk.Signal.service new file mode 100644 index 00000000..89bffd8a --- /dev/null +++ b/data/org.asamk.Signal.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.asamk.Signal +Exec=/bin/false +SystemdService=dbus-org.asamk.Signal.service diff --git a/data/org.asamk.TextSecure.conf b/data/org.asamk.TextSecure.conf deleted file mode 100644 index 79883753..00000000 --- a/data/org.asamk.TextSecure.conf +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/data/org.asamk.TextSecure.service b/data/org.asamk.TextSecure.service deleted file mode 100644 index a031626b..00000000 --- a/data/org.asamk.TextSecure.service +++ /dev/null @@ -1,4 +0,0 @@ -[D-BUS Service] -Name=org.asamk.TextSecure -Exec=/bin/false -SystemdService=dbus-org.asamk.TextSecure.service diff --git a/data/signal-cli@.service b/data/signal-cli@.service new file mode 100644 index 00000000..34409f6d --- /dev/null +++ b/data/signal-cli@.service @@ -0,0 +1,16 @@ +[Unit] +Description=Send secure messages to Signal clients +Requires=dbus.socket +After=dbus.socket +Wants=network.target +After=network.target + +[Service] +Type=dbus +Environment=SIGNAL_CLI_OPTS="-Xms2m" +ExecStart=%dir%/bin/signal-cli -u %I --config /var/lib/signal-cli daemon --system +User=signal-cli +BusName=org.asamk.Signal + +[Install] +WantedBy=multi-user.target diff --git a/data/signal.service b/data/signal.service new file mode 100644 index 00000000..126bbd2e --- /dev/null +++ b/data/signal.service @@ -0,0 +1,12 @@ +[Unit] +Description=Send secure messages to Signal clients + +[Service] +Type=dbus +Environment=SIGNAL_CLI_OPTS="-Xms2m" +ExecStart=%dir%/bin/signal-cli -u %number% --config /var/lib/signal-cli daemon --system +User=signal-cli +BusName=org.asamk.Signal + +[Install] +Alias=dbus-org.asamk.Signal.service diff --git a/data/textsecure-cli@.service b/data/textsecure-cli@.service deleted file mode 100644 index 3e39571e..00000000 --- a/data/textsecure-cli@.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Description=Send secure messages to TextSecure/Signal clients -Requires=dbus.socket -After=dbus.socket -Wants=network.target -After=network.target - -[Service] -Type=dbus -Environment=TEXTSECURE_CLI_OPTS="-Xms2m" -ExecStart=%dir%/bin/textsecure-cli -u %I --config /var/lib/textsecure-cli daemon --system -User=textsecure-cli -BusName=org.asamk.TextSecure - -[Install] -WantedBy=multi-user.target diff --git a/data/textsecure.service b/data/textsecure.service deleted file mode 100644 index 8b302d84..00000000 --- a/data/textsecure.service +++ /dev/null @@ -1,12 +0,0 @@ -[Unit] -Description=Send secure messages to TextSecure/Signal clients - -[Service] -Type=dbus -Environment=TEXTSECURE_CLI_OPTS="-Xms2m" -ExecStart=%dir%/bin/textsecure-cli -u %number% --config /var/lib/textsecure-cli daemon --system -User=textsecure-cli -BusName=org.asamk.TextSecure - -[Install] -Alias=dbus-org.asamk.TextSecure.service diff --git a/settings.gradle b/settings.gradle index a2c308d3..9f877185 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,4 +15,4 @@ include 'api' include 'services:webservice' */ -rootProject.name = 'textsecure-cli' +rootProject.name = 'signal-cli' diff --git a/src/main/java/org/asamk/TextSecure.java b/src/main/java/org/asamk/Signal.java similarity index 87% rename from src/main/java/org/asamk/TextSecure.java rename to src/main/java/org/asamk/Signal.java index 991342b7..cb2025ab 100644 --- a/src/main/java/org/asamk/TextSecure.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,7 +1,7 @@ package org.asamk; -import org.asamk.textsecure.AttachmentInvalidException; -import org.asamk.textsecure.GroupNotFoundException; +import org.asamk.signal.AttachmentInvalidException; +import org.asamk.signal.GroupNotFoundException; import org.freedesktop.dbus.DBusInterface; import org.freedesktop.dbus.DBusSignal; import org.freedesktop.dbus.exceptions.DBusException; @@ -10,7 +10,7 @@ import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptio import java.io.IOException; import java.util.List; -public interface TextSecure extends DBusInterface { +public interface Signal extends DBusInterface { void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; diff --git a/src/main/java/org/asamk/textsecure/AttachmentInvalidException.java b/src/main/java/org/asamk/signal/AttachmentInvalidException.java similarity index 92% rename from src/main/java/org/asamk/textsecure/AttachmentInvalidException.java rename to src/main/java/org/asamk/signal/AttachmentInvalidException.java index 5afa67e3..8a023f62 100644 --- a/src/main/java/org/asamk/textsecure/AttachmentInvalidException.java +++ b/src/main/java/org/asamk/signal/AttachmentInvalidException.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import org.freedesktop.dbus.exceptions.DBusExecutionException; diff --git a/src/main/java/org/asamk/textsecure/Base64.java b/src/main/java/org/asamk/signal/Base64.java similarity index 99% rename from src/main/java/org/asamk/textsecure/Base64.java rename to src/main/java/org/asamk/signal/Base64.java index f8f6b4cd..517bb7dd 100644 --- a/src/main/java/org/asamk/textsecure/Base64.java +++ b/src/main/java/org/asamk/signal/Base64.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; /** *

Encodes and decodes to and from Base64 notation.

diff --git a/src/main/java/org/asamk/textsecure/GroupInfo.java b/src/main/java/org/asamk/signal/GroupInfo.java similarity index 96% rename from src/main/java/org/asamk/textsecure/GroupInfo.java rename to src/main/java/org/asamk/signal/GroupInfo.java index dd6cacf7..4ad7003e 100644 --- a/src/main/java/org/asamk/textsecure/GroupInfo.java +++ b/src/main/java/org/asamk/signal/GroupInfo.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/textsecure/GroupNotFoundException.java b/src/main/java/org/asamk/signal/GroupNotFoundException.java similarity index 91% rename from src/main/java/org/asamk/textsecure/GroupNotFoundException.java rename to src/main/java/org/asamk/signal/GroupNotFoundException.java index 6c4cf5b1..0218c508 100644 --- a/src/main/java/org/asamk/textsecure/GroupNotFoundException.java +++ b/src/main/java/org/asamk/signal/GroupNotFoundException.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import org.freedesktop.dbus.exceptions.DBusExecutionException; diff --git a/src/main/java/org/asamk/textsecure/JsonGroupStore.java b/src/main/java/org/asamk/signal/JsonGroupStore.java similarity index 98% rename from src/main/java/org/asamk/textsecure/JsonGroupStore.java rename to src/main/java/org/asamk/signal/JsonGroupStore.java index 17a59f87..a75c5148 100644 --- a/src/main/java/org/asamk/textsecure/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/JsonGroupStore.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; diff --git a/src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java similarity index 99% rename from src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java rename to src/main/java/org/asamk/signal/JsonIdentityKeyStore.java index eaf97388..c1ef428b 100644 --- a/src/main/java/org/asamk/textsecure/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/org/asamk/textsecure/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/JsonPreKeyStore.java similarity index 99% rename from src/main/java/org/asamk/textsecure/JsonPreKeyStore.java rename to src/main/java/org/asamk/signal/JsonPreKeyStore.java index a522f177..d4c8d521 100644 --- a/src/main/java/org/asamk/textsecure/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/JsonPreKeyStore.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/org/asamk/textsecure/JsonSessionStore.java b/src/main/java/org/asamk/signal/JsonSessionStore.java similarity index 99% rename from src/main/java/org/asamk/textsecure/JsonSessionStore.java rename to src/main/java/org/asamk/signal/JsonSessionStore.java index 2fec85a3..cd4d55ad 100644 --- a/src/main/java/org/asamk/textsecure/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/JsonSessionStore.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/org/asamk/textsecure/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java similarity index 99% rename from src/main/java/org/asamk/textsecure/JsonSignalProtocolStore.java rename to src/main/java/org/asamk/signal/JsonSignalProtocolStore.java index f440a709..0d9c4b69 100644 --- a/src/main/java/org/asamk/textsecure/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; diff --git a/src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/JsonSignedPreKeyStore.java similarity index 99% rename from src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java rename to src/main/java/org/asamk/signal/JsonSignedPreKeyStore.java index f890fe88..cdcd506b 100644 --- a/src/main/java/org/asamk/textsecure/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/JsonSignedPreKeyStore.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; diff --git a/src/main/java/org/asamk/textsecure/Main.java b/src/main/java/org/asamk/signal/Main.java similarity index 97% rename from src/main/java/org/asamk/textsecure/Main.java rename to src/main/java/org/asamk/signal/Main.java index 404a8988..a4e50bb4 100644 --- a/src/main/java/org/asamk/textsecure/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -14,14 +14,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.asamk.textsecure; +package org.asamk.signal; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; import org.apache.commons.io.IOUtils; import org.apache.http.util.TextUtils; -import org.asamk.TextSecure; +import org.asamk.Signal; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; @@ -41,8 +41,8 @@ import java.util.List; public class Main { - public static final String TEXTSECURE_BUSNAME = "org.asamk.TextSecure"; - public static final String TEXTSECURE_OBJECTPATH = "/org/asamk/TextSecure"; + public static final String SIGNAL_BUSNAME = "org.asamk.Signal"; + public static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal"; public static void main(String[] args) { // Workaround for BKS truststore @@ -55,7 +55,7 @@ public class Main { final String username = ns.getString("username"); Manager m; - TextSecure ts; + Signal ts; DBusConnection dBusConn = null; try { if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) { @@ -68,9 +68,9 @@ public class Main { busType = DBusConnection.SESSION; } dBusConn = DBusConnection.getConnection(busType); - ts = (TextSecure) dBusConn.getRemoteObject( - TEXTSECURE_BUSNAME, TEXTSECURE_OBJECTPATH, - TextSecure.class); + ts = (Signal) dBusConn.getRemoteObject( + SIGNAL_BUSNAME, SIGNAL_OBJECTPATH, + Signal.class); } catch (DBusException e) { e.printStackTrace(); if (dBusConn != null) { @@ -82,7 +82,7 @@ public class Main { } else { String settingsPath = ns.getString("config"); if (TextUtils.isEmpty(settingsPath)) { - settingsPath = System.getProperty("user.home") + "/.config/textsecure"; + settingsPath = System.getProperty("user.home") + "/.config/signal"; } m = new Manager(username, settingsPath); @@ -299,8 +299,8 @@ public class Main { busType = DBusConnection.SESSION; } conn = DBusConnection.getConnection(busType); - conn.exportObject(TEXTSECURE_OBJECTPATH, m); - conn.requestBusName(TEXTSECURE_BUSNAME); + conn.exportObject(SIGNAL_OBJECTPATH, m); + conn.requestBusName(SIGNAL_BUSNAME); } catch (DBusException e) { e.printStackTrace(); System.exit(3); @@ -353,16 +353,16 @@ public class Main { } private static Namespace parseArgs(String[] args) { - ArgumentParser parser = ArgumentParsers.newArgumentParser("textsecure-cli") + ArgumentParser parser = ArgumentParsers.newArgumentParser("signal-cli") .defaultHelp(true) - .description("Commandline interface for TextSecure.") + .description("Commandline interface for Signal.") .version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION); parser.addArgument("-v", "--version") .help("Show package version.") .action(Arguments.version()); parser.addArgument("--config") - .help("Set the path, where to store the config (Default: $HOME/.config/textsecure-cli)."); + .help("Set the path, where to store the config (Default: $HOME/.config/signal-cli)."); MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup(); mut.addArgument("-u", "--username") @@ -630,8 +630,8 @@ public class Main { if (!message.isEndSession() && !(message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { try { - conn.sendSignal(new TextSecure.MessageReceived( - TEXTSECURE_OBJECTPATH, + conn.sendSignal(new Signal.MessageReceived( + SIGNAL_OBJECTPATH, envelope.getSource(), message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], message.getBody().isPresent() ? message.getBody().get() : "", diff --git a/src/main/java/org/asamk/textsecure/Manager.java b/src/main/java/org/asamk/signal/Manager.java similarity index 99% rename from src/main/java/org/asamk/textsecure/Manager.java rename to src/main/java/org/asamk/signal/Manager.java index 2d675170..94bb555a 100644 --- a/src/main/java/org/asamk/textsecure/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.asamk.textsecure; +package org.asamk.signal; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; @@ -23,7 +23,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.asamk.TextSecure; +import org.asamk.Signal; import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; @@ -52,8 +52,8 @@ import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -class Manager implements TextSecure { - private final static String URL = "https://SignalService-service.whispersystems.org"; +class Manager implements Signal { + private final static String URL = "https://textsecure-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); diff --git a/src/main/java/org/asamk/textsecure/Util.java b/src/main/java/org/asamk/signal/Util.java similarity index 95% rename from src/main/java/org/asamk/textsecure/Util.java rename to src/main/java/org/asamk/signal/Util.java index 7cbc851e..66a08731 100644 --- a/src/main/java/org/asamk/textsecure/Util.java +++ b/src/main/java/org/asamk/signal/Util.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; diff --git a/src/main/java/org/asamk/textsecure/WhisperTrustStore.java b/src/main/java/org/asamk/signal/WhisperTrustStore.java similarity index 92% rename from src/main/java/org/asamk/textsecure/WhisperTrustStore.java rename to src/main/java/org/asamk/signal/WhisperTrustStore.java index 1f129c5a..e9468c2e 100644 --- a/src/main/java/org/asamk/textsecure/WhisperTrustStore.java +++ b/src/main/java/org/asamk/signal/WhisperTrustStore.java @@ -1,4 +1,4 @@ -package org.asamk.textsecure; +package org.asamk.signal; import org.whispersystems.signalservice.api.push.TrustStore; diff --git a/src/main/resources/org/asamk/textsecure/whisper.store b/src/main/resources/org/asamk/signal/whisper.store similarity index 100% rename from src/main/resources/org/asamk/textsecure/whisper.store rename to src/main/resources/org/asamk/signal/whisper.store From 95278a95ce4cb52b0f4c42f79a8939b413c9eed1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 27 Mar 2016 14:02:28 +0200 Subject: [PATCH 0079/2005] Use the old config directory .config/textsecure as fallback --- README.md | 4 ++++ src/main/java/org/asamk/signal/Main.java | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/README.md b/README.md index f951669d..e2a3145e 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,10 @@ The password and cryptographic keys are created when registering and stored in t $HOME/.config/signal/data/ +For legacy users, the old config directory is used as a fallback: + + $HOME/.config/textsecure/data/ + ## Building This project uses [Gradle](http://gradle.org) for building and maintaining diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index a4e50bb4..5d6e419b 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -83,6 +83,12 @@ public class Main { String settingsPath = ns.getString("config"); if (TextUtils.isEmpty(settingsPath)) { settingsPath = System.getProperty("user.home") + "/.config/signal"; + if (!new File(settingsPath).exists()) { + String legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure"; + if (new File(legacySettingsPath).exists()) { + settingsPath = legacySettingsPath; + } + } } m = new Manager(username, settingsPath); From 900b648f09fa1a1c6e2d35df94c5a9d75286af01 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 27 Mar 2016 16:15:49 +0200 Subject: [PATCH 0080/2005] Refresh prekeys if there are less than 20 left on the server --- src/main/java/org/asamk/signal/Manager.java | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 94bb555a..a4a9c37a 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -60,6 +60,9 @@ class Manager implements Signal { public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION; + private final static int PREKEY_MINIMUM_COUNT = 20; + private static final int PREKEY_BATCH_SIZE = 100; + private final String settingsPath; private final String dataPath; private final String attachmentsPath; @@ -140,6 +143,10 @@ class Manager implements Signal { groupStore = new JsonGroupStore(); } accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + if (accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + save(); + } } private void save() { @@ -187,12 +194,10 @@ class Manager implements Signal { save(); } - private static final int BATCH_SIZE = 100; - private List generatePreKeys() { List records = new LinkedList<>(); - for (int i = 0; i < BATCH_SIZE; i++) { + for (int i = 0; i < PREKEY_BATCH_SIZE; i++) { int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE; ECKeyPair keyPair = Curve.generateKeyPair(); PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); @@ -201,13 +206,13 @@ class Manager implements Signal { records.add(record); } - preKeyIdOffset = (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE; + preKeyIdOffset = (preKeyIdOffset + PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE; save(); return records; } - private PreKeyRecord generateLastResortPreKey() { + private PreKeyRecord getOrGenerateLastResortPreKey() { if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) { try { return signalProtocolStore.loadPreKey(Medium.MAX_VALUE); @@ -249,14 +254,16 @@ class Manager implements Signal { //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; + refreshPreKeys(); + save(); + } + + private void refreshPreKeys() throws IOException { List oneTimePreKeys = generatePreKeys(); - - PreKeyRecord lastResortKey = generateLastResortPreKey(); - + PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey(); SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair()); accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); - save(); } From cc0177da108ca7edd2731baaffdacad98f6e7364 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 27 Mar 2016 16:49:20 +0200 Subject: [PATCH 0081/2005] Show source deviceId and relay, when receiving messages --- src/main/java/org/asamk/signal/Main.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 5d6e419b..076d3cc5 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -28,6 +28,7 @@ import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; @@ -493,7 +494,11 @@ public class Main { @Override public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) { - System.out.println("Envelope from: " + envelope.getSource()); + SignalServiceAddress source = envelope.getSourceAddress(); + System.out.println(String.format("Envelope from: %s (device: %d)", source.getNumber(), envelope.getSourceDevice())); + if (source.getRelay().isPresent()) { + System.out.println("Relayed by: " + source.getRelay().get()); + } System.out.println("Timestamp: " + envelope.getTimestamp()); if (envelope.isReceipt()) { From 083e33c4daaa7a4de0a2cd421060e60855f80378 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 27 Mar 2016 17:03:57 +0200 Subject: [PATCH 0082/2005] Use e.printStackTrace() --- src/main/java/org/asamk/signal/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 076d3cc5..f05bec19 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -463,7 +463,7 @@ public class Main { private static void handleAssertionError(AssertionError e) { System.err.println("Failed to send/receive message (Assertion): " + e.getMessage()); - System.err.println(e.getStackTrace()); + e.printStackTrace(); System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); System.exit(1); } From dd934f130471502a9a683781c5534a065c9730ca Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 27 Mar 2016 23:34:59 +0200 Subject: [PATCH 0083/2005] Reduce duplicate code --- src/main/java/org/asamk/signal/Main.java | 95 ++++++------------------ 1 file changed, 23 insertions(+), 72 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index f05bec19..a79c3c7d 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -573,94 +573,45 @@ public class Main { } } - private static class DbusReceiveMessageHandler implements Manager.ReceiveMessageHandler { - final Manager m; + private static class DbusReceiveMessageHandler extends ReceiveMessageHandler { final DBusConnection conn; public DbusReceiveMessageHandler(Manager m, DBusConnection conn) { - this.m = m; + super(m); this.conn = conn; } @Override public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) { - System.out.println("Envelope from: " + envelope.getSource()); - System.out.println("Timestamp: " + envelope.getTimestamp()); + super.handleMessage(envelope, content, group); - if (envelope.isReceipt()) { - System.out.println("Got receipt."); - } else if (envelope.isSignalMessage() | envelope.isPreKeySignalMessage()) { - if (content == null) { - System.out.println("Failed to decrypt message."); - } else { - if (content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); + if (!envelope.isReceipt() && content != null && content.getDataMessage().isPresent()) { + SignalServiceDataMessage message = content.getDataMessage().get(); - System.out.println("Message timestamp: " + message.getTimestamp()); - - if (message.getBody().isPresent()) { - System.out.println("Body: " + message.getBody().get()); - } - - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); - System.out.println("Group info:"); - System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); - if (groupInfo.getName().isPresent()) { - System.out.println(" Name: " + groupInfo.getName().get()); - } else if (group != null) { - System.out.println(" Name: " + group.name); - } else { - System.out.println(" Name: "); - } - System.out.println(" Type: " + groupInfo.getType()); - if (groupInfo.getMembers().isPresent()) { - for (String member : groupInfo.getMembers().get()) { - System.out.println(" Member: " + member); - } - } - if (groupInfo.getAvatar().isPresent()) { - System.out.println(" Avatar:"); - printAttachment(groupInfo.getAvatar().get()); - } - } - if (message.isEndSession()) { - System.out.println("Is end session"); - } - - List attachments = new ArrayList<>(); - if (message.getAttachments().isPresent()) { - System.out.println("Attachments: "); - for (SignalServiceAttachment attachment : message.getAttachments().get()) { - if (attachment.isPointer()) { - attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); - } - printAttachment(attachment); - } - } - if (!message.isEndSession() && - !(message.getGroupInfo().isPresent() && message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { - try { - conn.sendSignal(new Signal.MessageReceived( - SIGNAL_OBJECTPATH, - envelope.getSource(), - message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], - message.getBody().isPresent() ? message.getBody().get() : "", - attachments)); - } catch (DBusException e) { - e.printStackTrace(); + if (!message.isEndSession() && + !(message.getGroupInfo().isPresent() && + message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { + List attachments = new ArrayList<>(); + if (message.getAttachments().isPresent()) { + for (SignalServiceAttachment attachment : message.getAttachments().get()) { + if (attachment.isPointer()) { + attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); } } } - if (content.getSyncMessage().isPresent()) { - SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); - System.out.println("Received sync message"); + + try { + conn.sendSignal(new Signal.MessageReceived( + SIGNAL_OBJECTPATH, + envelope.getSource(), + message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], + message.getBody().isPresent() ? message.getBody().get() : "", + attachments)); + } catch (DBusException e) { + e.printStackTrace(); } } - } else { - System.out.println("Unknown message received."); } - System.out.println(); } private void printAttachment(SignalServiceAttachment attachment) { From af8a27e87f7844e733d1b42419c3976f0b41ae58 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 27 Mar 2016 23:35:36 +0200 Subject: [PATCH 0084/2005] Add timestamp to dbus MessageReceived signal --- src/main/java/org/asamk/Signal.java | 35 ++++++++++++++++++++++-- src/main/java/org/asamk/signal/Main.java | 1 + 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index cb2025ab..02fc22dd 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -20,8 +20,39 @@ public interface Signal extends DBusInterface { void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; class MessageReceived extends DBusSignal { - public MessageReceived(String objectpath, String sender, byte[] groupId, String message, List attachments) throws DBusException { - super(objectpath, sender, groupId, message, attachments); + private long timestamp; + private String sender; + private byte[] groupId; + private String message; + private List attachments; + + public MessageReceived(String objectpath, long timestamp, String sender, byte[] groupId, String message, List attachments) throws DBusException { + super(objectpath, timestamp, sender, groupId, message, attachments); + this.timestamp = timestamp; + this.sender = sender; + this.groupId = groupId; + this.message = message; + this.attachments = attachments; + } + + public long getTimestamp() { + return timestamp; + } + + public String getSender() { + return sender; + } + + public byte[] getGroupId() { + return groupId; + } + + public String getMessage() { + return message; + } + + public List getAttachments() { + return attachments; } } } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index a79c3c7d..d562c4e1 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -603,6 +603,7 @@ public class Main { try { conn.sendSignal(new Signal.MessageReceived( SIGNAL_OBJECTPATH, + message.getTimestamp(), envelope.getSource(), message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], message.getBody().isPresent() ? message.getBody().get() : "", From 0b6c09f883b4f58a58c084a5579ee6a6c62708e3 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 27 Mar 2016 23:36:03 +0200 Subject: [PATCH 0085/2005] Add rudimentary message receiving via dbus --- src/main/java/org/asamk/signal/Main.java | 32 ++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index d562c4e1..696f24ce 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -23,6 +23,7 @@ import org.apache.commons.io.IOUtils; import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.DBusSigHandler; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; @@ -207,8 +208,35 @@ public class Main { break; case "receive": if (dBusConn != null) { - System.err.println("receive is not yet implementd via dbus"); - System.exit(1); + try { + dBusConn.addSigHandler(Signal.MessageReceived.class, new DBusSigHandler() { + @Override + public void handle(Signal.MessageReceived s) { + System.out.print(String.format("Envelope from: %s\nTimestamp: %d\nBody: %s\n", + s.getSender(), s.getTimestamp(), s.getMessage())); + if (s.getGroupId().length > 0) { + System.out.println("Group info:"); + System.out.println(" Id: " + Base64.encodeBytes(s.getGroupId())); + } + if (s.getAttachments().size() > 0) { + System.out.println("Attachments: "); + for (String attachment : s.getAttachments()) { + System.out.println("- Stored plaintext in: " + attachment); + } + } + System.out.println(); + } + }); + } catch (DBusException e) { + e.printStackTrace(); + } + while (true) { + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + System.exit(0); + } + } } if (!m.isRegistered()) { System.err.println("User is not registered."); From ffc393c716ff0050ee919ba557d5221fadd8dd25 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 30 Mar 2016 23:26:39 +0200 Subject: [PATCH 0086/2005] Update README.md Rename libtextsecure-java to libsignal-service-java --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e2a3145e..796c0340 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # signal-cli -signal-cli is a commandline interface for [libtextsecure-java](https://github.com/WhisperSystems/libtextsecure-java). It supports registering, verifying, sending and receiving messages. To be able to receiving messages signal-cli uses a [patched libtextsecure-java](https://github.com/AsamK/libtextsecure-java), because libtextsecure-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libtextsecure-java/pull/5). For registering you need a phone number where you can receive SMS or incoming calls. +signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. To be able to receiving messages signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libsignal-service-java/pull/5). For registering you need a phone number where you can receive SMS or incoming calls. It is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings. ## Usage @@ -108,8 +108,8 @@ If you use a version of the Oracle JRE and get an InvalidKeyException you need t ## License -This project uses libtextsecure-java from Open Whisper Systems: +This project uses libsignal-service-java from Open Whisper Systems: -https://github.com/WhisperSystems/libtextsecure-java +https://github.com/WhisperSystems/libsignal-service-java Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html From 7ce99427e3e4aae2db94d14f919ce91a22eb41ed Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 2 Apr 2016 12:04:49 +0200 Subject: [PATCH 0087/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8402bd05..ebe9ee95 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.2.1' +version = '0.3.0' repositories { maven { From 7b8998727ef9783e064b9e01c10f90a18280a5c6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 2 Apr 2016 22:56:15 +0200 Subject: [PATCH 0088/2005] Always use utf-8 encoding for compiling --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index ebe9ee95..69d30afc 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,8 @@ mainClassName = 'org.asamk.signal.Main' version = '0.3.0' +compileJava.options.encoding = 'UTF-8' + repositories { maven { url "https://raw.github.com/AsamK/maven/master/releases/" From f479cffc9f1619f7bab11dbbac4dec5ec3f9d238 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 2 Apr 2016 22:57:13 +0200 Subject: [PATCH 0089/2005] Set user-agent null if PROJECT_NAME is null the name is only set if the code is run from a jar file --- src/main/java/org/asamk/signal/Manager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index a4a9c37a..113fb0cc 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -58,7 +58,7 @@ class Manager implements Signal { public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); - private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION; + private final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION; private final static int PREKEY_MINIMUM_COUNT = 20; private static final int PREKEY_BATCH_SIZE = 100; From edf5c9eb43917eb7231906d8d1a6b9d2b1f43627 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Apr 2016 14:08:46 +0200 Subject: [PATCH 0090/2005] Use original bouncycastle instead of spongycastle spongycastle is used by Signal-Android, because android has a crippled bouncycastle. Spongycastle seems to have a problem with Oracle JDK 8. Fixes #9 --- build.gradle | 2 +- src/main/java/org/asamk/signal/Main.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 69d30afc..2064f360 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { dependencies { compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages' - compile 'com.madgag.spongycastle:prov:1.54.0.0' + compile 'org.bouncycastle:bcprov-jdk15on:1.54' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 696f24ce..00366e5a 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -48,7 +48,7 @@ public class Main { public static void main(String[] args) { // Workaround for BKS truststore - Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1); + Security.insertProviderAt(new org.bouncycastle.jce.provider.BouncyCastleProvider(), 1); Namespace ns = parseArgs(args); if (ns == null) { From 9d18b01d85b0588a03e4640213bb27ae174e7426 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Apr 2016 14:28:24 +0200 Subject: [PATCH 0091/2005] Fix registering Only query prekeys count if registration is complete --- src/main/java/org/asamk/signal/Manager.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 113fb0cc..d1963cb5 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -41,6 +41,7 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.messages.*; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; +import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -143,9 +144,13 @@ class Manager implements Signal { groupStore = new JsonGroupStore(); } accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); - if (accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { - refreshPreKeys(); - save(); + try { + if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + save(); + } + } catch (AuthorizationFailedException e) { + System.err.println("Authorization failed, was the number registered elsewhere?"); } } From 81e94b2b72c57edbd8e3604f60ff5f9f2cc9c528 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Apr 2016 15:54:45 +0200 Subject: [PATCH 0092/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2064f360..02f8c55b 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.3.0' +version = '0.3.1' compileJava.options.encoding = 'UTF-8' From 4af9b5f0110ad05feff8c2b939d20cc69491f549 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Apr 2016 16:25:22 +0200 Subject: [PATCH 0093/2005] Fix syntax for systemd service file: Environment --- data/signal-cli@.service | 2 +- data/signal.service | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/data/signal-cli@.service b/data/signal-cli@.service index 34409f6d..a47e7eeb 100644 --- a/data/signal-cli@.service +++ b/data/signal-cli@.service @@ -7,7 +7,7 @@ After=network.target [Service] Type=dbus -Environment=SIGNAL_CLI_OPTS="-Xms2m" +Environment="SIGNAL_CLI_OPTS=-Xms2m" ExecStart=%dir%/bin/signal-cli -u %I --config /var/lib/signal-cli daemon --system User=signal-cli BusName=org.asamk.Signal diff --git a/data/signal.service b/data/signal.service index 126bbd2e..b80e2799 100644 --- a/data/signal.service +++ b/data/signal.service @@ -3,7 +3,7 @@ Description=Send secure messages to Signal clients [Service] Type=dbus -Environment=SIGNAL_CLI_OPTS="-Xms2m" +Environment="SIGNAL_CLI_OPTS=-Xms2m" ExecStart=%dir%/bin/signal-cli -u %number% --config /var/lib/signal-cli daemon --system User=signal-cli BusName=org.asamk.Signal From c2ae0c17c256329d0d4a9940e67ff1793fb86f48 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Apr 2016 22:09:52 +0200 Subject: [PATCH 0094/2005] Fix typos --- src/main/java/org/asamk/signal/Main.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 00366e5a..5dcf426a 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -109,7 +109,7 @@ public class Main { switch (ns.getString("command")) { case "register": if (dBusConn != null) { - System.err.println("register is not yet implementd via dbus"); + System.err.println("register is not yet implemented via dbus"); System.exit(1); } if (!m.userHasKeys()) { @@ -124,7 +124,7 @@ public class Main { break; case "verify": if (dBusConn != null) { - System.err.println("verify is not yet implementd via dbus"); + System.err.println("verify is not yet implemented via dbus"); System.exit(1); } if (!m.userHasKeys()) { @@ -262,7 +262,7 @@ public class Main { break; case "quitGroup": if (dBusConn != null) { - System.err.println("quitGroup is not yet implementd via dbus"); + System.err.println("quitGroup is not yet implemented via dbus"); System.exit(1); } if (!m.isRegistered()) { @@ -285,7 +285,7 @@ public class Main { break; case "updateGroup": if (dBusConn != null) { - System.err.println("updateGroup is not yet implementd via dbus"); + System.err.println("updateGroup is not yet implemented via dbus"); System.exit(1); } if (!m.isRegistered()) { From aa8a23aceb6cd9cb63723f4d783c7ef07e365610 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Apr 2016 22:09:36 +0200 Subject: [PATCH 0095/2005] Handle received sync messages --- src/main/java/org/asamk/signal/Main.java | 114 ++++++++++------ src/main/java/org/asamk/signal/Manager.java | 141 +++++++++++--------- 2 files changed, 156 insertions(+), 99 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 5dcf426a..def569df 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -28,6 +28,8 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; @@ -537,48 +539,41 @@ public class Main { } else { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - - System.out.println("Message timestamp: " + message.getTimestamp()); - - if (message.getBody().isPresent()) { - System.out.println("Body: " + message.getBody().get()); - } - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); - System.out.println("Group info:"); - System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); - if (groupInfo.getName().isPresent()) { - System.out.println(" Name: " + groupInfo.getName().get()); - } else if (group != null) { - System.out.println(" Name: " + group.name); - } else { - System.out.println(" Name: "); - } - System.out.println(" Type: " + groupInfo.getType()); - if (groupInfo.getMembers().isPresent()) { - for (String member : groupInfo.getMembers().get()) { - System.out.println(" Member: " + member); - } - } - if (groupInfo.getAvatar().isPresent()) { - System.out.println(" Avatar:"); - printAttachment(groupInfo.getAvatar().get()); - } - } - if (message.isEndSession()) { - System.out.println("Is end session"); - } - - if (message.getAttachments().isPresent()) { - System.out.println("Attachments: "); - for (SignalServiceAttachment attachment : message.getAttachments().get()) { - printAttachment(attachment); - } - } + handleSignalServiceDataMessage(message, group); } if (content.getSyncMessage().isPresent()) { SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); - System.out.println("Received sync message"); + + if (syncMessage.getContacts().isPresent()) { + System.out.println("Received sync contacts"); + printAttachment(syncMessage.getContacts().get()); + } + if (syncMessage.getGroups().isPresent()) { + System.out.println("Received sync groups"); + printAttachment(syncMessage.getGroups().get()); + } + if (syncMessage.getRead().isPresent()) { + System.out.println("Received sync read messages list"); + for (ReadMessage rm : syncMessage.getRead().get()) { + System.out.println("From: " + rm.getSender() + " Message timestamp: " + rm.getTimestamp()); + } + } + if (syncMessage.getRequest().isPresent()) { + System.out.println("Received sync request"); + if (syncMessage.getRequest().get().isContactsRequest()) { + System.out.println(" - contacts request"); + } + if (syncMessage.getRequest().get().isGroupsRequest()) { + System.out.println(" - groups request"); + } + } + if (syncMessage.getSent().isPresent()) { + System.out.println("Received sync sent message"); + final SentTranscriptMessage sentTranscriptMessage = syncMessage.getSent().get(); + System.out.println("To: " + (sentTranscriptMessage.getDestination().isPresent() ? sentTranscriptMessage.getDestination().get() : "Unknown") + " , Message timestamp: " + sentTranscriptMessage.getTimestamp()); + SignalServiceDataMessage message = sentTranscriptMessage.getMessage(); + handleSignalServiceDataMessage(message, null); + } } } } else { @@ -587,6 +582,47 @@ public class Main { System.out.println(); } + // TODO remove group parameter + private void handleSignalServiceDataMessage(SignalServiceDataMessage message, GroupInfo group) { + System.out.println("Message timestamp: " + message.getTimestamp()); + + if (message.getBody().isPresent()) { + System.out.println("Body: " + message.getBody().get()); + } + if (message.getGroupInfo().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupInfo().get(); + System.out.println("Group info:"); + System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); + if (groupInfo.getName().isPresent()) { + System.out.println(" Name: " + groupInfo.getName().get()); + } else if (group != null) { + System.out.println(" Name: " + group.name); + } else { + System.out.println(" Name: "); + } + System.out.println(" Type: " + groupInfo.getType()); + if (groupInfo.getMembers().isPresent()) { + for (String member : groupInfo.getMembers().get()) { + System.out.println(" Member: " + member); + } + } + if (groupInfo.getAvatar().isPresent()) { + System.out.println(" Avatar:"); + printAttachment(groupInfo.getAvatar().get()); + } + } + if (message.isEndSession()) { + System.out.println("Is end session"); + } + + if (message.getAttachments().isPresent()) { + System.out.println("Attachments: "); + for (SignalServiceAttachment attachment : message.getAttachments().get()) { + printAttachment(attachment); + } + } + } + private void printAttachment(SignalServiceAttachment attachment) { System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); if (attachment.isPointer()) { diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index d1963cb5..2fc54504 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -37,8 +37,10 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; -import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; +import org.whispersystems.signalservice.api.crypto.*; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; @@ -453,6 +455,73 @@ class Manager implements Signal { void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group); } + private GroupInfo handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { + GroupInfo group = null; + if (message.getGroupInfo().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupInfo().get(); + switch (groupInfo.getType()) { + case UPDATE: + try { + group = groupStore.getGroup(groupInfo.getGroupId()); + } catch (GroupNotFoundException e) { + group = new GroupInfo(groupInfo.getGroupId()); + } + + if (groupInfo.getAvatar().isPresent()) { + SignalServiceAttachment avatar = groupInfo.getAvatar().get(); + if (avatar.isPointer()) { + long avatarId = avatar.asPointer().getId(); + try { + retrieveAttachment(avatar.asPointer()); + group.avatarId = avatarId; + } catch (IOException | InvalidMessageException e) { + System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage()); + } + } + } + + if (groupInfo.getName().isPresent()) { + group.name = groupInfo.getName().get(); + } + + if (groupInfo.getMembers().isPresent()) { + group.members.addAll(groupInfo.getMembers().get()); + } + + groupStore.updateGroup(group); + break; + case DELIVER: + try { + group = groupStore.getGroup(groupInfo.getGroupId()); + } catch (GroupNotFoundException e) { + } + break; + case QUIT: + try { + group = groupStore.getGroup(groupInfo.getGroupId()); + group.members.remove(source); + } catch (GroupNotFoundException e) { + } + break; + } + } + if (message.isEndSession()) { + handleEndSession(isSync ? destination : source); + } + if (message.getAttachments().isPresent()) { + for (SignalServiceAttachment attachment : message.getAttachments().get()) { + if (attachment.isPointer()) { + try { + retrieveAttachment(attachment.asPointer()); + } catch (IOException | InvalidMessageException e) { + System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage()); + } + } + } + } + return group; + } + public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); SignalServiceMessagePipe messagePipe = null; @@ -471,67 +540,19 @@ class Manager implements Signal { if (content != null) { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); - switch (groupInfo.getType()) { - case UPDATE: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); - } catch (GroupNotFoundException e) { - group = new GroupInfo(groupInfo.getGroupId()); - } - - if (groupInfo.getAvatar().isPresent()) { - SignalServiceAttachment avatar = groupInfo.getAvatar().get(); - if (avatar.isPointer()) { - long avatarId = avatar.asPointer().getId(); - try { - retrieveAttachment(avatar.asPointer()); - group.avatarId = avatarId; - } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage()); - } - } - } - - if (groupInfo.getName().isPresent()) { - group.name = groupInfo.getName().get(); - } - - if (groupInfo.getMembers().isPresent()) { - group.members.addAll(groupInfo.getMembers().get()); - } - - groupStore.updateGroup(group); - break; - case DELIVER: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); - } catch (GroupNotFoundException e) { - } - break; - case QUIT: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); - group.members.remove(envelope.getSource()); - } catch (GroupNotFoundException e) { - } - break; - } + group = handleSignalServiceDataMessage(message, false, envelope.getSource(), username); + } + if (content.getSyncMessage().isPresent()) { + SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); + if (syncMessage.getSent().isPresent()) { + SignalServiceDataMessage message = syncMessage.getSent().get().getMessage(); + group = handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); } - if (message.isEndSession()) { - handleEndSession(envelope.getSource()); + if (syncMessage.getRequest().isPresent()) { + // TODO } - if (message.getAttachments().isPresent()) { - for (SignalServiceAttachment attachment : message.getAttachments().get()) { - if (attachment.isPointer()) { - try { - retrieveAttachment(attachment.asPointer()); - } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage()); - } - } - } + if (syncMessage.getGroups().isPresent()) { + // TODO } } } From 5c117bd863c6b01e21b2865ebffd04c5cb23003c Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 8 Apr 2016 23:34:34 +0200 Subject: [PATCH 0096/2005] Correctly use API for sending non group messages is necessary so the correct sync messages are generated for linked accounts --- src/main/java/org/asamk/Signal.java | 9 ++++---- src/main/java/org/asamk/signal/Main.java | 8 +++++++ src/main/java/org/asamk/signal/Manager.java | 23 ++++++++++++++------- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 02fc22dd..e115bb7c 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -5,19 +5,20 @@ import org.asamk.signal.GroupNotFoundException; import org.freedesktop.dbus.DBusInterface; import org.freedesktop.dbus.DBusSignal; import org.freedesktop.dbus.exceptions.DBusException; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import java.io.IOException; import java.util.List; public interface Signal extends DBusInterface { - void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; + void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException; - void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; + void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException; - void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions; + void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException; - void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; + void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException, UntrustedIdentityException; class MessageReceived extends DBusSignal { private long timestamp; diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index def569df..e5a2288f 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -166,6 +166,8 @@ public class Main { handleAssertionError(e); } catch (DBusExecutionException e) { handleDBusExecutionException(e); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); } } else { String messageText = ns.getString("message"); @@ -204,6 +206,8 @@ public class Main { System.exit(1); } catch (DBusExecutionException e) { handleDBusExecutionException(e); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); } } @@ -282,6 +286,8 @@ public class Main { handleAssertionError(e); } catch (GroupNotFoundException e) { handleGroupNotFoundException(e); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); } break; @@ -314,6 +320,8 @@ public class Main { handleGroupNotFoundException(e); } catch (EncapsulatedExceptions e) { handleEncapsulatedExceptions(e); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); } break; diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 2fc54504..d626ccaf 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -300,7 +300,7 @@ class Manager implements Signal { @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); @@ -316,7 +316,7 @@ class Manager implements Signal { sendMessage(message, groupStore.getGroup(groupId).members); } - public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); @@ -328,7 +328,7 @@ class Manager implements Signal { sendMessage(message, groupStore.getGroup(groupId).members); } - public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { GroupInfo g; if (groupId == null) { // Create new group @@ -381,7 +381,7 @@ class Manager implements Signal { @Override public void sendMessage(String message, List attachments, String recipient) - throws EncapsulatedExceptions, AttachmentInvalidException, IOException { + throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException { List recipients = new ArrayList<>(1); recipients.add(recipient); sendMessage(message, attachments, recipients); @@ -390,7 +390,7 @@ class Manager implements Signal { @Override public void sendMessage(String messageText, List attachments, List recipients) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); @@ -401,7 +401,7 @@ class Manager implements Signal { } @Override - public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { + public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() .asEndSessionMessage() .build(); @@ -410,7 +410,7 @@ class Manager implements Signal { } private void sendMessage(SignalServiceDataMessage message, Collection recipients) - throws IOException, EncapsulatedExceptions { + throws IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, signalProtocolStore, USER_AGENT, Optional.absent()); @@ -426,7 +426,14 @@ class Manager implements Signal { } } - messageSender.sendMessage(new ArrayList<>(recipientsTS), message); + if (message.getGroupInfo().isPresent()) { + messageSender.sendMessage(new ArrayList<>(recipientsTS), message); + } else { + // Send to all individually, so sync messages are sent correctly + for (SignalServiceAddress address : recipientsTS) { + messageSender.sendMessage(address, message); + } + } if (message.isEndSession()) { for (SignalServiceAddress recipient : recipientsTS) { From f6b9222edac6f72d75dd8711c13ad0902d553b2f Mon Sep 17 00:00:00 2001 From: Individual IT Services Date: Mon, 11 Apr 2016 14:34:53 +0545 Subject: [PATCH 0097/2005] Receive messages (#10) --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 796c0340..b9886cd1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ usage: signal-cli [-h] [-u USERNAME] [-v] {register,verify,send,quitGroup,update * Pipe the message content from another process. uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]] + +* Receive messages + + signal-cli -u USERNAME receive * Groups From 33956bde62d4fc7de5325bf3bb7be3b323863442 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Apr 2016 16:30:20 +0200 Subject: [PATCH 0098/2005] Implement device linking --- build.gradle | 2 +- src/main/java/org/asamk/signal/Main.java | 45 +++++++++++++++- src/main/java/org/asamk/signal/Manager.java | 54 +++++++++++++++++-- .../org/asamk/signal/UserAlreadyExists.java | 19 +++++++ 4 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/asamk/signal/UserAlreadyExists.java diff --git a/build.gradle b/build.gradle index 02f8c55b..1ee9be22 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages' + compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages_provisioning' compile 'org.bouncycastle:bcprov-jdk15on:1.54' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index e5a2288f..0d2e6f06 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -26,6 +26,7 @@ import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.DBusSigHandler; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; @@ -42,6 +43,7 @@ import java.io.IOException; import java.security.Security; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeoutException; public class Main { @@ -144,6 +146,37 @@ public class Main { System.exit(3); } break; + case "link": + if (dBusConn != null) { + System.err.println("link is not yet implemented via dbus"); + System.exit(1); + } + + // When linking, username is null and we always have to create keys + m.createNewIdentity(); + + String deviceName = ns.getString("name"); + if (deviceName == null) { + deviceName = "cli"; + } + try { + System.out.println(m.getDeviceLinkUri()); + m.finishDeviceLink(deviceName); + System.out.println("Associated with: " + m.getUsername()); + } catch (TimeoutException e) { + System.err.println("Link request timed out, please try again."); + System.exit(3); + } catch (IOException e) { + System.err.println("Link request error: " + e.getMessage()); + System.exit(3); + } catch (InvalidKeyException e) { + e.printStackTrace(); + System.exit(3); + } catch (UserAlreadyExists e) { + System.err.println("The user " + e.getUsername() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again."); + System.exit(3); + } + break; case "send": if (dBusConn == null && !m.isRegistered()) { System.err.println("User is not registered."); @@ -425,6 +458,10 @@ public class Main { .description("valid subcommands") .help("additional help"); + Subparser parserLink = subparsers.addParser("link"); + parserLink.addArgument("-n", "--name") + .help("Specify a name to describe this new device."); + Subparser parserRegister = subparsers.addParser("register"); parserRegister.addArgument("-v", "--voice") .help("The verification should be done over voice, not sms.") @@ -477,7 +514,13 @@ public class Main { try { Namespace ns = parser.parseArgs(args); - if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) { + if ("link".equals(ns.getString("command"))) { + if (ns.getString("username") != null) { + parser.printUsage(); + System.err.println("You cannot specify a username (phone number) when linking"); + System.exit(2); + } + } else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) { if (ns.getString("username") == null) { parser.printUsage(); System.err.println("You need to specify a username (phone number)"); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index d626ccaf..d442224a 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -49,6 +49,9 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; @@ -72,6 +75,7 @@ class Manager implements Signal { private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; + int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private String password; private String signalingKey; private int preKeyIdOffset; @@ -95,12 +99,19 @@ class Manager implements Signal { jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } + public String getUsername() { + return username; + } + public String getFileName() { new File(dataPath).mkdirs(); return dataPath + "/" + username; } public boolean userExists() { + if (username == null) { + return false; + } File f = new File(getFileName()); return !(!f.exists() || f.isDirectory()); } @@ -121,6 +132,10 @@ class Manager implements Signal { public void load() throws IOException, InvalidKeyException { JsonNode rootNode = jsonProcessot.readTree(new File(getFileName())); + JsonNode node = rootNode.get("deviceId"); + if (node != null) { + deviceId = node.asInt(); + } username = getNotNullNode(rootNode, "username").asText(); password = getNotNullNode(rootNode, "password").asText(); if (rootNode.has("signalingKey")) { @@ -145,7 +160,7 @@ class Manager implements Signal { if (groupStore == null) { groupStore = new JsonGroupStore(); } - accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); try { if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { refreshPreKeys(); @@ -159,6 +174,7 @@ class Manager implements Signal { private void save() { ObjectNode rootNode = jsonProcessot.createObjectNode(); rootNode.put("username", username) + .put("deviceId", deviceId) .put("password", password) .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) @@ -201,6 +217,36 @@ class Manager implements Signal { save(); } + public URI getDeviceLinkUri() throws TimeoutException, IOException { + password = Util.getSecret(18); + + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + String uuid = accountManager.getNewDeviceUuid(); + + registered = false; + try { + return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8")); + } catch (URISyntaxException e) { + // Shouldn't happen + return null; + } + } + + public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { + signalingKey = Util.getSecret(52); + SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName); + deviceId = ret.getDeviceId(); + username = ret.getNumber(); + // TODO do this check before actually registering + if (userExists()) { + throw new UserAlreadyExists(username, getFileName()); + } + signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId()); + + registered = true; + refreshPreKeys(); + } + private List generatePreKeys() { List records = new LinkedList<>(); @@ -412,7 +458,7 @@ class Manager implements Signal { private void sendMessage(SignalServiceDataMessage message, Collection recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, - signalProtocolStore, USER_AGENT, Optional.absent()); + deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); Set recipientsTS = new HashSet<>(recipients.size()); for (String recipient : recipients) { @@ -530,7 +576,7 @@ class Manager implements Signal { } public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); SignalServiceMessagePipe messagePipe = null; try { @@ -584,7 +630,7 @@ class Manager implements Signal { } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); diff --git a/src/main/java/org/asamk/signal/UserAlreadyExists.java b/src/main/java/org/asamk/signal/UserAlreadyExists.java new file mode 100644 index 00000000..2c018ed9 --- /dev/null +++ b/src/main/java/org/asamk/signal/UserAlreadyExists.java @@ -0,0 +1,19 @@ +package org.asamk.signal; + +public class UserAlreadyExists extends Exception { + private String username; + private String fileName; + + public UserAlreadyExists(String username, String fileName) { + this.username = username; + this.fileName = fileName; + } + + public String getUsername() { + return username; + } + + public String getFileName() { + return fileName; + } +} From 947818d3172a4257b6d1a160496f1504d8f514ab Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 8 Apr 2016 23:32:26 +0200 Subject: [PATCH 0099/2005] Add possiblity to add new device, as master --- src/main/java/org/asamk/signal/Main.java | 29 ++++++++++++ src/main/java/org/asamk/signal/Manager.java | 49 +++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 0d2e6f06..f2c83dfc 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -40,6 +40,8 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.security.Security; import java.util.ArrayList; import java.util.List; @@ -177,6 +179,28 @@ public class Main { System.exit(3); } break; + case "addDevice": + if (dBusConn != null) { + System.err.println("link is not yet implemented via dbus"); + System.exit(1); + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); + } + try { + m.addDeviceLink(new URI(ns.getString("uri"))); + } catch (IOException e) { + e.printStackTrace(); + System.exit(3); + } catch (InvalidKeyException e) { + e.printStackTrace(); + System.exit(2); + } catch (URISyntaxException e) { + e.printStackTrace(); + System.exit(2); + } + break; case "send": if (dBusConn == null && !m.isRegistered()) { System.err.println("User is not registered."); @@ -462,6 +486,11 @@ public class Main { parserLink.addArgument("-n", "--name") .help("Specify a name to describe this new device."); + Subparser parserAddDevice = subparsers.addParser("addDevice"); + parserAddDevice.addArgument("--uri") + .required(true) + .help("Specify the uri contained in the QR code shown by the new device."); + Subparser parserRegister = subparsers.addParser("register"); parserRegister.addArgument("-v", "--voice") .help("The verification should be done over voice, not sms.") diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index d442224a..eb883bb8 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -23,10 +23,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; +import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; @@ -40,6 +42,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.*; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; @@ -47,10 +50,12 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import java.io.*; import java.net.URI; import java.net.URISyntaxException; +import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; @@ -245,6 +250,50 @@ class Manager implements Signal { registered = true; refreshPreKeys(); + save(); + } + + + public static Map getQueryMap(String query) { + String[] params = query.split("&"); + Map map = new HashMap<>(); + for (String param : params) { + String name = null; + try { + name = URLDecoder.decode(param.split("=")[0], "utf-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + } + String value = null; + try { + value = URLDecoder.decode(param.split("=")[1], "utf-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + } + map.put(name, value); + } + return map; + } + + public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { + Map query = getQueryMap(linkUri.getQuery()); + String deviceIdentifier = query.get("uuid"); + String publicKeyEncoded = query.get("pub_key"); + + if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) { + throw new RuntimeException("Invalid device link uri"); + } + + ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); + + addDeviceLink(deviceIdentifier, deviceKey); + } + + private void addDeviceLink(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { + IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair(); + String verificationCode = accountManager.getNewDeviceVerificationCode(); + + accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, verificationCode); } private List generatePreKeys() { From 0ad42a72ab64e663733eb7ab89b6de5ec9f80a4a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 Apr 2016 11:50:52 +0200 Subject: [PATCH 0100/2005] Implement requesting/sending groups when linking device --- src/main/java/org/asamk/signal/GroupInfo.java | 3 + .../java/org/asamk/signal/JsonGroupStore.java | 6 + src/main/java/org/asamk/signal/Main.java | 1 + src/main/java/org/asamk/signal/Manager.java | 143 +++++++++++++++++- 4 files changed, 147 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/asamk/signal/GroupInfo.java b/src/main/java/org/asamk/signal/GroupInfo.java index 4ad7003e..cf5d0158 100644 --- a/src/main/java/org/asamk/signal/GroupInfo.java +++ b/src/main/java/org/asamk/signal/GroupInfo.java @@ -19,6 +19,9 @@ public class GroupInfo { @JsonProperty public long avatarId; + @JsonProperty + public boolean active; + public GroupInfo(byte[] groupId) { this.groupId = groupId; } diff --git a/src/main/java/org/asamk/signal/JsonGroupStore.java b/src/main/java/org/asamk/signal/JsonGroupStore.java index a75c5148..29dc0031 100644 --- a/src/main/java/org/asamk/signal/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/JsonGroupStore.java @@ -8,7 +8,9 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; public class JsonGroupStore { @@ -31,6 +33,10 @@ public class JsonGroupStore { return g; } + List getGroups() { + return new ArrayList<>(groups.values()); + } + public static class MapToListSerializer extends JsonSerializer> { @Override public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index f2c83dfc..9ffcb660 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -622,6 +622,7 @@ public class Main { handleSignalServiceDataMessage(message, group); } if (content.getSyncMessage().isPresent()) { + System.out.println("Received a sync message"); SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getContacts().isPresent()) { diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index eb883bb8..5bff3cf3 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -39,11 +39,10 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; -import org.whispersystems.signalservice.api.crypto.*; +import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; -import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.multidevice.*; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; @@ -250,6 +249,10 @@ class Manager implements Signal { registered = true; refreshPreKeys(); + + requestSyncGroups(); + requestSyncContacts(); + save(); } @@ -384,7 +387,7 @@ class Manager implements Signal { return SignalServiceAttachments; } - private static SignalServiceAttachmentStream createAttachment(String attachment) throws IOException { + private static SignalServiceAttachment createAttachment(String attachment) throws IOException { File attachmentFile = new File(attachment); InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); @@ -504,6 +507,37 @@ class Manager implements Signal { sendMessage(message, recipients); } + private void requestSyncGroups() throws IOException { + SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build(); + SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + try { + sendMessage(message); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + encapsulatedExceptions.printStackTrace(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } + } + + private void requestSyncContacts() throws IOException { + SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build(); + SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + try { + sendMessage(message); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + encapsulatedExceptions.printStackTrace(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } + } + + private void sendMessage(SignalServiceSyncMessage message) + throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, + deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); + messageSender.sendMessage(message); + } + private void sendMessage(SignalServiceDataMessage message, Collection recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, @@ -575,6 +609,7 @@ class Manager implements Signal { long avatarId = avatar.asPointer().getId(); try { retrieveAttachment(avatar.asPointer()); + // TODO store group avatar in /avatar/groups folder group.avatarId = avatarId; } catch (IOException | InvalidMessageException e) { System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage()); @@ -651,10 +686,68 @@ class Manager implements Signal { group = handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); } if (syncMessage.getRequest().isPresent()) { - // TODO + RequestMessage rm = syncMessage.getRequest().get(); + if (rm.isContactsRequest()) { + // TODO implement when we have contacts + } + if (rm.isGroupsRequest()) { + try { + sendGroups(); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + encapsulatedExceptions.printStackTrace(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } + } } if (syncMessage.getGroups().isPresent()) { - // TODO + try { + DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer())); + DeviceGroup g; + while ((g = s.read()) != null) { + GroupInfo syncGroup; + try { + syncGroup = groupStore.getGroup(g.getId()); + } catch (GroupNotFoundException e) { + syncGroup = new GroupInfo(g.getId()); + } + if (g.getName().isPresent()) { + syncGroup.name = g.getName().get(); + } + syncGroup.members.addAll(g.getMembers()); + syncGroup.active = g.isActive(); + + if (g.getAvatar().isPresent()) { + byte[] ava = new byte[(int) g.getAvatar().get().getLength()]; + org.whispersystems.signalservice.internal.util.Util.readFully(g.getAvatar().get().getInputStream(), ava); + // TODO store group avatar in /avatar/groups folder + } + groupStore.updateGroup(syncGroup); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + if (syncMessage.getContacts().isPresent()) { + try { + DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); + DeviceContact c; + while ((c = s.read()) != null) { + // TODO implement when we have contact storage + if (c.getName().isPresent()) { + c.getName().get(); + } + c.getNumber(); + + if (c.getAvatar().isPresent()) { + byte[] ava = new byte[(int) c.getAvatar().get().getLength()]; + org.whispersystems.signalservice.internal.util.Util.readFully(c.getAvatar().get().getInputStream(), ava); + // TODO store contact avatar in /avatar/contacts folder + } + } + } catch (Exception e) { + e.printStackTrace(); + } } } } @@ -725,6 +818,14 @@ class Manager implements Signal { return outputFile; } + private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); + File file = File.createTempFile("ts_tmp", "tmp"); + file.deleteOnExit(); + + return messageReceiver.retrieveAttachment(pointer, file); + } + private String canonicalizeNumber(String number) throws InvalidNumberException { String localNumber = username; return PhoneNumberFormatter.formatNumber(number, localNumber); @@ -739,4 +840,34 @@ class Manager implements Signal { public boolean isRemote() { return false; } + + private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); + + try { + DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(contactsFile)); + try { + for (GroupInfo record : groupStore.getGroups()) { + out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), + new ArrayList<>(record.members), Optional.of(new SignalServiceAttachmentStream(new FileInputStream("/home/sebastian/Bilder/00026_150512_14-00-18.JPG"), "octet", new File("/home/sebastian/Bilder/00026_150512_14-00-18.JPG").length(), null)), + record.active)); + } + } finally { + out.close(); + } + + if (contactsFile.exists() && contactsFile.length() > 0) { + FileInputStream contactsFileStream = new FileInputStream(contactsFile); + SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(contactsFileStream) + .withContentType("application/octet-stream") + .withLength(contactsFile.length()) + .build(); + + sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + } + } finally { + if (contactsFile != null) contactsFile.delete(); + } + } } From 3cc57044066f2cf4b28bf9e8a4d927baa92031e2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 Apr 2016 14:15:06 +0200 Subject: [PATCH 0101/2005] Remove own number when sending group messages --- src/main/java/org/asamk/signal/Manager.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 5bff3cf3..f1b1f7bb 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -411,7 +411,9 @@ class Manager implements Signal { } SignalServiceDataMessage message = messageBuilder.build(); - sendMessage(message, groupStore.getGroup(groupId).members); + Set members = groupStore.getGroup(groupId).members; + members.remove(this.username); + sendMessage(message, members); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException { @@ -473,7 +475,9 @@ class Manager implements Signal { .asGroupMessage(group.build()) .build(); - sendMessage(message, g.members); + final Set membersSend = g.members; + membersSend.remove(this.username); + sendMessage(message, membersSend); return g.groupId; } From 5c1127ced688f41d00dd61ae00bb921689891966 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 Apr 2016 14:15:25 +0200 Subject: [PATCH 0102/2005] Remove own number from group when quitting --- src/main/java/org/asamk/signal/Manager.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index f1b1f7bb..de37cc92 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -425,7 +425,11 @@ class Manager implements Signal { .asGroupMessage(group) .build(); - sendMessage(message, groupStore.getGroup(groupId).members); + final GroupInfo g = groupStore.getGroup(groupId); + g.members.remove(this.username); + groupStore.updateGroup(g); + + sendMessage(message, g.members); } public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { @@ -641,6 +645,7 @@ class Manager implements Signal { try { group = groupStore.getGroup(groupInfo.getGroupId()); group.members.remove(source); + groupStore.updateGroup(group); } catch (GroupNotFoundException e) { } break; From 800b92c4ba9f417b3a3a88929c61a66d86408d85 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 Apr 2016 14:15:36 +0200 Subject: [PATCH 0103/2005] Always save when sending messages --- src/main/java/org/asamk/signal/Manager.java | 49 +++++++++++---------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index de37cc92..a38eed5a 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -548,36 +548,39 @@ class Manager implements Signal { private void sendMessage(SignalServiceDataMessage message, Collection recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, - deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); + try { + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, + deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); - Set recipientsTS = new HashSet<>(recipients.size()); - for (String recipient : recipients) { - try { - recipientsTS.add(getPushAddress(recipient)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - save(); - return; + Set recipientsTS = new HashSet<>(recipients.size()); + for (String recipient : recipients) { + try { + recipientsTS.add(getPushAddress(recipient)); + } catch (InvalidNumberException e) { + System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + save(); + return; + } } - } - if (message.getGroupInfo().isPresent()) { - messageSender.sendMessage(new ArrayList<>(recipientsTS), message); - } else { - // Send to all individually, so sync messages are sent correctly - for (SignalServiceAddress address : recipientsTS) { - messageSender.sendMessage(address, message); + if (message.getGroupInfo().isPresent()) { + messageSender.sendMessage(new ArrayList<>(recipientsTS), message); + } else { + // Send to all individually, so sync messages are sent correctly + for (SignalServiceAddress address : recipientsTS) { + messageSender.sendMessage(address, message); + } } - } - if (message.isEndSession()) { - for (SignalServiceAddress recipient : recipientsTS) { - handleEndSession(recipient.getNumber()); + if (message.isEndSession()) { + for (SignalServiceAddress recipient : recipientsTS) { + handleEndSession(recipient.getNumber()); + } } + } finally { + save(); } - save(); } private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) { From 17ff7531d493266bca9c97fdf61290ead7262227 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 Apr 2016 14:36:56 +0200 Subject: [PATCH 0104/2005] Add method to list linked devices --- src/main/java/org/asamk/signal/Main.java | 25 +++++++++++++++++++++ src/main/java/org/asamk/signal/Manager.java | 9 +++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 9ffcb660..a6d36d0f 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -29,6 +29,7 @@ import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; @@ -201,6 +202,28 @@ public class Main { System.exit(2); } break; + case "listDevices": + if (dBusConn != null) { + System.err.println("listDevices is not yet implemented via dbus"); + System.exit(1); + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); + } + try { + List devices = m.getLinkedDevices(); + for (DeviceInfo d : devices) { + System.out.println("Device " + d.getId() + (d.getId() == m.getDeviceId() ? " (this device)" : "") + ":"); + System.out.println(" Name: " + d.getName()); + System.out.println(" Created: " + d.getCreated()); + System.out.println(" Last seen: " + d.getLastSeen()); + } + } catch (IOException e) { + e.printStackTrace(); + System.exit(3); + } + break; case "send": if (dBusConn == null && !m.isRegistered()) { System.err.println("User is not registered."); @@ -491,6 +514,8 @@ public class Main { .required(true) .help("Specify the uri contained in the QR code shown by the new device."); + Subparser parserDevices = subparsers.addParser("listDevices"); + Subparser parserRegister = subparsers.addParser("register"); parserRegister.addArgument("-v", "--voice") .help("The verification should be done over voice, not sms.") diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index a38eed5a..961be232 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -79,7 +79,7 @@ class Manager implements Signal { private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; - int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; + private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private String password; private String signalingKey; private int preKeyIdOffset; @@ -107,6 +107,10 @@ class Manager implements Signal { return username; } + public int getDeviceId() { + return deviceId; + } + public String getFileName() { new File(dataPath).mkdirs(); return dataPath + "/" + username; @@ -256,6 +260,9 @@ class Manager implements Signal { save(); } + public List getLinkedDevices() throws IOException { + return accountManager.getDevices(); + } public static Map getQueryMap(String query) { String[] params = query.split("&"); From 08a217108a9b406dfd68907766c65da42420660e Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 Apr 2016 15:03:23 +0200 Subject: [PATCH 0105/2005] Implement removing linked devices Only allowed from the master device --- src/main/java/org/asamk/signal/Main.java | 23 +++++++++++++++++++++ src/main/java/org/asamk/signal/Manager.java | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index a6d36d0f..8aede3d6 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -224,6 +224,23 @@ public class Main { System.exit(3); } break; + case "removeDevice": + if (dBusConn != null) { + System.err.println("removeDevice is not yet implemented via dbus"); + System.exit(1); + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + System.exit(1); + } + try { + int deviceId = ns.getInt("deviceId"); + m.removeLinkedDevices(deviceId); + } catch (IOException e) { + e.printStackTrace(); + System.exit(3); + } + break; case "send": if (dBusConn == null && !m.isRegistered()) { System.err.println("User is not registered."); @@ -516,6 +533,12 @@ public class Main { Subparser parserDevices = subparsers.addParser("listDevices"); + Subparser parserRemoveDevice = subparsers.addParser("removeDevice"); + parserRemoveDevice.addArgument("-d", "--deviceId") + .type(int.class) + .required(true) + .help("Specify the device you want to remove. Use listDevices to see the deviceIds."); + Subparser parserRegister = subparsers.addParser("register"); parserRegister.addArgument("-v", "--voice") .help("The verification should be done over voice, not sms.") diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 961be232..47699c3f 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -264,6 +264,10 @@ class Manager implements Signal { return accountManager.getDevices(); } + public void removeLinkedDevices(int deviceId) throws IOException { + accountManager.removeDevice(deviceId); + } + public static Map getQueryMap(String query) { String[] params = query.split("&"); Map map = new HashMap<>(); From 15568512b1400d3696e43880a674e5d63f078cd7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 Apr 2016 15:07:35 +0200 Subject: [PATCH 0106/2005] Fix addDevice --- src/main/java/org/asamk/signal/Manager.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 47699c3f..3931f3e3 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -290,7 +290,7 @@ class Manager implements Signal { } public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - Map query = getQueryMap(linkUri.getQuery()); + Map query = getQueryMap(linkUri.getRawQuery()); String deviceIdentifier = query.get("uuid"); String publicKeyEncoded = query.get("pub_key"); @@ -300,10 +300,10 @@ class Manager implements Signal { ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); - addDeviceLink(deviceIdentifier, deviceKey); + addDevice(deviceIdentifier, deviceKey); } - private void addDeviceLink(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { + private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair(); String verificationCode = accountManager.getNewDeviceVerificationCode(); From 46befdd638dba0bfa20fdfdb389692cb106630dd Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 Apr 2016 15:07:49 +0200 Subject: [PATCH 0107/2005] Don't save if username is null --- src/main/java/org/asamk/signal/Manager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 3931f3e3..be94f7f5 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -180,6 +180,9 @@ class Manager implements Signal { } private void save() { + if (username == null) { + return; + } ObjectNode rootNode = jsonProcessot.createObjectNode(); rootNode.put("username", username) .put("deviceId", deviceId) From 543dd984537c1c33e0fe622e3ffebae86d0ae793 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 Apr 2016 16:10:24 +0200 Subject: [PATCH 0108/2005] Update README.md --- README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9886cd1..a0a22a08 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # signal-cli -signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. To be able to receiving messages signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libsignal-service-java/pull/5). For registering you need a phone number where you can receive SMS or incoming calls. +signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. To be able to receiving messages signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libsignal-service-java/pull/5) nor [provisioning as a slave device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For registering you need a phone number where you can receive SMS or incoming calls. It is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings. ## Usage -usage: signal-cli [-h] [-u USERNAME] [-v] {register,verify,send,quitGroup,updateGroup,receive} ... +usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-system] {link,addDevice,listDevices,removeDevice,register,verify,send,quitGroup,updateGroup,receive,daemon} ... * Register a number (with SMS verification) @@ -45,6 +45,27 @@ usage: signal-cli [-h] [-u USERNAME] [-v] {register,verify,send,quitGroup,update signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID +* Linking other devices (Provisioning) + + * Connect to another device + + signal-cli link -n "optional device name" + + This shows a "tsdevice:/…" link, if you want to connect to another signal-cli instance, you can just use this link. If you want to link to and Android device, create a QR code with the link (e.g. with [qrencode](https://fukuchi.org/works/qrencode/)) and scan that in the Signal Android app. + + * Add another device + + signal-cli -u USERNAME addDevice --uri "tsdevice:/…" + + The "tsdevice:/…" link is the one shown by the new signal-cli instance or contained in the QR code shown in Signal-Desktop or similar apps. + Only the master device (that was registered directly, not linked) can add new devices. + + * Manage linked devices + + signal-cli -u USERNAME listDevices + + signal-cli -u USERNAME removeDevice -d DEVICE_ID + ## DBus service signal-cli can run in daemon mode and provides an experimental dbus interface. From 32beb8a0bd51d2bd13b9687168ed08cad3b8eca1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 22 Apr 2016 21:17:02 +0200 Subject: [PATCH 0109/2005] Implement a contacts store and contacts sync --- .../java/org/asamk/signal/ContactInfo.java | 11 ++++ .../org/asamk/signal/JsonContactsStore.java | 57 +++++++++++++++++ src/main/java/org/asamk/signal/Manager.java | 64 ++++++++++++++++--- 3 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/asamk/signal/ContactInfo.java create mode 100644 src/main/java/org/asamk/signal/JsonContactsStore.java diff --git a/src/main/java/org/asamk/signal/ContactInfo.java b/src/main/java/org/asamk/signal/ContactInfo.java new file mode 100644 index 00000000..89802050 --- /dev/null +++ b/src/main/java/org/asamk/signal/ContactInfo.java @@ -0,0 +1,11 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ContactInfo { + @JsonProperty + public String name; + + @JsonProperty + public String number; +} diff --git a/src/main/java/org/asamk/signal/JsonContactsStore.java b/src/main/java/org/asamk/signal/JsonContactsStore.java new file mode 100644 index 00000000..e2807d8f --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonContactsStore.java @@ -0,0 +1,57 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JsonContactsStore { + @JsonProperty("contacts") + @JsonSerialize(using = JsonContactsStore.MapToListSerializer.class) + @JsonDeserialize(using = ContactsDeserializer.class) + private Map contacts = new HashMap<>(); + + private static final ObjectMapper jsonProcessot = new ObjectMapper(); + + void updateContact(ContactInfo contact) { + contacts.put(contact.number, contact); + } + + ContactInfo getContact(String number) { + ContactInfo c = contacts.get(number); + return c; + } + + List getContacts() { + return new ArrayList<>(contacts.values()); + } + + public static class MapToListSerializer extends JsonSerializer> { + @Override + public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { + jgen.writeObject(value.values()); + } + } + + public static class ContactsDeserializer extends JsonDeserializer> { + @Override + public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + Map contacts = new HashMap<>(); + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + for (JsonNode n : node) { + ContactInfo c = jsonProcessot.treeToValue(n, ContactInfo.class); + contacts.put(c.number, c); + } + + return contacts; + } + } +} diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index be94f7f5..6295f730 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -90,6 +90,7 @@ class Manager implements Signal { private SignalProtocolStore signalProtocolStore; private SignalServiceAccountManager accountManager; private JsonGroupStore groupStore; + private JsonContactsStore contactStore; public Manager(String username, String settingsPath) { this.username = username; @@ -168,6 +169,14 @@ class Manager implements Signal { if (groupStore == null) { groupStore = new JsonGroupStore(); } + JsonNode contactStoreNode = rootNode.get("contactStore"); + if (contactStoreNode != null) { + contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class); + } + if (contactStore == null) { + contactStore = new JsonContactsStore(); + } + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); try { if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { @@ -193,6 +202,7 @@ class Manager implements Signal { .put("registered", registered) .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) + .putPOJO("contactStore", contactStore) ; try { jsonProcessot.writeValue(new File(getFileName()), rootNode); @@ -714,7 +724,13 @@ class Manager implements Signal { if (syncMessage.getRequest().isPresent()) { RequestMessage rm = syncMessage.getRequest().get(); if (rm.isContactsRequest()) { - // TODO implement when we have contacts + try { + sendContacts(); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + encapsulatedExceptions.printStackTrace(); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } } if (rm.isGroupsRequest()) { try { @@ -759,11 +775,12 @@ class Manager implements Signal { DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); DeviceContact c; while ((c = s.read()) != null) { - // TODO implement when we have contact storage + ContactInfo contact = new ContactInfo(); + contact.number = c.getNumber(); if (c.getName().isPresent()) { - c.getName().get(); + contact.name = c.getName().get(); } - c.getNumber(); + contactStore.updateContact(contact); if (c.getAvatar().isPresent()) { byte[] ava = new byte[(int) c.getAvatar().get().getLength()]; @@ -868,20 +885,49 @@ class Manager implements Signal { } private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { - File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); + File groupsFile = File.createTempFile("multidevice-group-update", ".tmp"); try { - DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(contactsFile)); + DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile)); try { for (GroupInfo record : groupStore.getGroups()) { out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), - new ArrayList<>(record.members), Optional.of(new SignalServiceAttachmentStream(new FileInputStream("/home/sebastian/Bilder/00026_150512_14-00-18.JPG"), "octet", new File("/home/sebastian/Bilder/00026_150512_14-00-18.JPG").length(), null)), + new ArrayList<>(record.members), Optional.absent(), // TODO record.active)); } } finally { out.close(); } + if (groupsFile.exists() && groupsFile.length() > 0) { + FileInputStream contactsFileStream = new FileInputStream(groupsFile); + SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(contactsFileStream) + .withContentType("application/octet-stream") + .withLength(groupsFile.length()) + .build(); + + sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + } + } finally { + groupsFile.delete(); + } + } + + private void sendContacts() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); + + try { + DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile)); + try { + for (ContactInfo record : contactStore.getContacts()) { + out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), + Optional.absent())); // TODO + } + } finally { + out.close(); + } + if (contactsFile.exists() && contactsFile.length() > 0) { FileInputStream contactsFileStream = new FileInputStream(contactsFile); SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() @@ -890,10 +936,10 @@ class Manager implements Signal { .withLength(contactsFile.length()) .build(); - sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); } } finally { - if (contactsFile != null) contactsFile.delete(); + contactsFile.delete(); } } } From a46af40b6185546ac857b714f34c38e49df86578 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 25 Apr 2016 22:42:56 +0200 Subject: [PATCH 0110/2005] Update dependency --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 1ee9be22..5bd59f55 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { dependencies { compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages_provisioning' compile 'org.bouncycastle:bcprov-jdk15on:1.54' - compile 'commons-io:commons-io:2.4' + compile 'commons-io:commons-io:2.5' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' } From e59ceef6e341393e187dc1dfb5024c76a5b9d16e Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 6 May 2016 12:18:53 +0200 Subject: [PATCH 0111/2005] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 53639 -> 53556 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 46 ++++++++++++----------- gradlew.bat | 6 +-- 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 2c6137b87896c8f70315ae454e00a969ef5f6019..ca78035ef0501d802d4fc55381ef2d5c3ce0ec6e 100644 GIT binary patch delta 3268 zcmZWr2|SePAAjc^S7GF)QG>~mbDWWljWQ!gNJ2{F`n#{flx1jXU8N_BVo{_+EaTXA z?dBYeyAnn=i$qE~eNx%~JJ0;L{m<@vKJ)qg<~!f-?|Pp1d7q4{_=YQZac6tHz(xcS z5kcY&it@zOMKD7XerYC~XW=FNCgxQP4*8oBiczks;JKz>6Pi^D*8nZtA6&~Mps^?f z&Imk(K!jig1emfS(n1i?CQP7`7?&>`bp6F7n-)myc4=v$ROUpxNMq z0!mMI(kk`FV<*+Qhiw1)_ngqAQ!g6tHjX*1uYEjcg=;w*nY7SVk-DAqjI!8q!a=+6 zRU0`c@(S~oyBtYwI(7-wl6?tPlG+~mGv7a& zyCxrM`H1z4Yl}wLN2#^lFLRP~FW&3l zqd7mGUL+!F?C2TD8gU5PYaeiev9_VZxHuH8X>H9+ImPCbH6{*H-X#a03_a3Z(D?46 zUYcjLZsV1S^kVs(2#x>v3#32Qv5?$1&avGm5kBd)^Y&!USs7JXXxiUj|1* z7?`~n6q__t_*-Fq<1C$V_J=CBuQUs8{&ZGe!0X&<(Ws%_a;0*OUozhNu}jtlMopEQ zmu<@}RYt`|8&*WEOAKRqwzHuA+VQYOwZR(p0ms0c2k8-_-i1x!eall$&jKUa_e>X- zcGVnyd1OB3V3+!`U7l;5-~o028?;%nU{j`;lAx#TYWb~k7D3;3{%l!U>JeGeth8+D zjFD{VEF)8Yr82eF?R0WZq=3-=-(6AN`~r`!bt`kiBV=l|CqJ1kIVa-$m-=Ei7Mq{U zf1kf6VP?BFIjuL&!HtWCg5T=q|g>(JI^4=-~D^*;k~wh{Qf%MGx@O zk`A(Z70^`DfAf!RAJRuv3mR|jUktQHJ=h{2cY-5BGGp;VswSQ>YIEP|eU|u0Ei%df zRmgiVR5_}W^;%l6^NraM$uOlLi%#kmEgo$fsy#1(f1>7Ptt3btnzrnGnMkt=Ncw$& zn&PU9P*Qg1iYv5Jl3l!YeoMPdn{KvwaQj7v$&SLbkJN1oi}1_AS27ovu)uM{&=a=gJ( z`KpBCJK=TT4}!JRhqCAGy!{$1ESJz1uKTzN2AiTsf%ij z4Y|>8XM()jLUq1XCq0fGqu5hibe!7a^Er*DaXwY=ZI7OMp$%x#fGuEf~jl_RGzvyD>o9`nC?^06V z=aw2|-@D`Wz@r^M&X7v$8eKnFl_gGQ5bIw=#5Cn+FWBy1aelra@%MnSCIQ`72aj3i z4d1gU*8#@sA(h#;c!|EPY>*Fp3XU@e- zLH2t2D^!wO4Rs{&`30<+VnGBk0>0rF7 z!b|v+xtqt){%M$ptlx3taSpZsyc9Rh@RO@Isn|Bzi+xE5a2`=%kN+%LAR z8hGq&4x(C(zr58!b?Z*FOrdpavxGLk%?aB1wn%6_+8v?2-tGY{zT+sg z{v93!{<5!F5fL)RW6 z0MTwSDE;!pVlpzHG!2+`8~)#;8r?06lLZ;w8r&TK>bi+IMKI9475-Z8CZaT7U@)i% zw)Ln($gzir-aifug2^DKM_QS??5@@uwp>RMWEE@YE52NJ{ULF-hZkJDmx#6nVUQJg z*)ImQ3uN)vRIEjNOmD!ay&ZxPm4JDuJn-z}k*(x||LhhZKF9o$( zyG_DQV_H#6yOS?LR3@0t{0dn1h(T>;KM~!N4GU!V8$f6sgI?!ikSf?UzzgRiVR`Q= z*4%ciIU~MsCFM{{xgw?GY>z`_@Vtxmzm5w&QCef1tXI7+>)F)$V{9Q-+)Zqm_}9~@7S3tQkSZLCh5$`W5gSSnw-dhj z>h%~=354|%VUnUDBAU*GjZF+`KrKKlins&)glWzBwCrY}JWPeUkYOTPa)&pNpC!4g k4PiD0zx{;=Ul9ZnBPyhSpL2i5Y+z)9{Ue4#gjO#0UthZVsQ>@~ delta 3282 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then diff --git a/gradlew.bat b/gradlew.bat index 72d362da..f6d5974e 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome From 39687f9d87774a824be4765d80a635475f8215f1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 19 May 2016 18:00:16 +0200 Subject: [PATCH 0112/2005] Update Readme and fix help bug in Main --- README.md | 4 ++++ src/main/java/org/asamk/signal/Main.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a0a22a08..dea9d145 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-sys signal-cli -u USERNAME updateGroup -g GROUP_ID -n "New group name" + * Leave a group + + signal-cli -u USERNAME quitGroup -g GROUP_ID + * Send a message to a group signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 8aede3d6..b3664f24 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -504,7 +504,7 @@ public class Main { .help("Show package version.") .action(Arguments.version()); parser.addArgument("--config") - .help("Set the path, where to store the config (Default: $HOME/.config/signal-cli)."); + .help("Set the path, where to store the config (Default: $HOME/.config/signal)."); MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup(); mut.addArgument("-u", "--username") From fb5f2ca5fa8ec5a445565df08d041f68879ded23 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 19 May 2016 18:22:52 +0200 Subject: [PATCH 0113/2005] Update systemd service files --- data/signal-cli@.service | 4 ++-- data/signal.service | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/data/signal-cli@.service b/data/signal-cli@.service index a47e7eeb..4cc6e2cf 100644 --- a/data/signal-cli@.service +++ b/data/signal-cli@.service @@ -2,8 +2,8 @@ Description=Send secure messages to Signal clients Requires=dbus.socket After=dbus.socket -Wants=network.target -After=network.target +Wants=network-online.target +After=network-online.target [Service] Type=dbus diff --git a/data/signal.service b/data/signal.service index b80e2799..089b0428 100644 --- a/data/signal.service +++ b/data/signal.service @@ -1,5 +1,9 @@ [Unit] Description=Send secure messages to Signal clients +Requires=dbus.socket +After=dbus.socket +Wants=network-online.target +After=network-online.target [Service] Type=dbus From 4608fb433b1af1753bdf73584d5c0f5f9e70f1bf Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 19 Jun 2016 15:08:49 +0200 Subject: [PATCH 0114/2005] Remove dependency on apache commons-io --- build.gradle | 1 - src/main/java/org/asamk/signal/Main.java | 18 ++++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 5bd59f55..b38e4d47 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,6 @@ repositories { dependencies { compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages_provisioning' compile 'org.bouncycastle:bcprov-jdk15on:1.54' - compile 'commons-io:commons-io:2.5' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index b3664f24..4b22605c 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -19,7 +19,6 @@ package org.asamk.signal; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; -import org.apache.commons.io.IOUtils; import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.freedesktop.dbus.DBusConnection; @@ -41,8 +40,11 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.Charset; import java.security.Security; import java.util.ArrayList; import java.util.List; @@ -270,7 +272,7 @@ public class Main { String messageText = ns.getString("message"); if (messageText == null) { try { - messageText = IOUtils.toString(System.in); + messageText = readAll(System.in); } catch (IOException e) { System.err.println("Failed to read message from stdin: " + e.getMessage()); System.err.println("Aborting sending."); @@ -643,6 +645,18 @@ public class Main { System.err.println("Failed to send message: " + e.getMessage()); } + private static String readAll(InputStream in) throws IOException { + StringWriter output = new StringWriter(); + byte[] buffer = new byte[4096]; + long count = 0; + int n; + while (-1 != (n = System.in.read(buffer))) { + output.write(new String(buffer, 0, n, Charset.defaultCharset())); + count += n; + } + return output.toString(); + } + private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final Manager m; From 54558ae7fbaa53998520ed64ff21af5125ef1363 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 19 Jun 2016 15:08:57 +0200 Subject: [PATCH 0115/2005] Remove unused method --- src/main/java/org/asamk/signal/Main.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 4b22605c..d276329f 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -822,17 +822,5 @@ public class Main { } } - private void printAttachment(SignalServiceAttachment attachment) { - System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); - if (attachment.isPointer()) { - final SignalServiceAttachmentPointer pointer = attachment.asPointer(); - System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); - System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); - File file = m.getAttachmentFile(pointer.getId()); - if (file.exists()) { - System.out.println(" Stored plaintext in: " + file); - } - } - } } } From 03c6f84fc216e396643bf01c0a4be2623aab280f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 19 Jun 2016 15:09:24 +0200 Subject: [PATCH 0116/2005] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 53556 -> 53319 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ca78035ef0501d802d4fc55381ef2d5c3ce0ec6e..d3b83982b9b1bccad955349d702be9b884c6e049 100644 GIT binary patch delta 3698 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 Kt!+{*O!R*iz94e| delta 4032 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 Date: Sun, 19 Jun 2016 15:40:32 +0200 Subject: [PATCH 0117/2005] Show the contact name when receiving messages Works only if the contact is stored in the config file already --- src/main/java/org/asamk/signal/Main.java | 16 +++++++++++++--- src/main/java/org/asamk/signal/Manager.java | 4 ++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index d276329f..5f716e3c 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -667,7 +667,8 @@ public class Main { @Override public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) { SignalServiceAddress source = envelope.getSourceAddress(); - System.out.println(String.format("Envelope from: %s (device: %d)", source.getNumber(), envelope.getSourceDevice())); + ContactInfo sourceContact = m.getContact(source.getNumber()); + System.out.println(String.format("Envelope from: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + source.getNumber(), envelope.getSourceDevice())); if (source.getRelay().isPresent()) { System.out.println("Relayed by: " + source.getRelay().get()); } @@ -698,7 +699,8 @@ public class Main { if (syncMessage.getRead().isPresent()) { System.out.println("Received sync read messages list"); for (ReadMessage rm : syncMessage.getRead().get()) { - System.out.println("From: " + rm.getSender() + " Message timestamp: " + rm.getTimestamp()); + ContactInfo fromContact = m.getContact(rm.getSender()); + System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender() + " Message timestamp: " + rm.getTimestamp()); } } if (syncMessage.getRequest().isPresent()) { @@ -713,7 +715,15 @@ public class Main { if (syncMessage.getSent().isPresent()) { System.out.println("Received sync sent message"); final SentTranscriptMessage sentTranscriptMessage = syncMessage.getSent().get(); - System.out.println("To: " + (sentTranscriptMessage.getDestination().isPresent() ? sentTranscriptMessage.getDestination().get() : "Unknown") + " , Message timestamp: " + sentTranscriptMessage.getTimestamp()); + String to; + if (sentTranscriptMessage.getDestination().isPresent()) { + String dest = sentTranscriptMessage.getDestination().get(); + ContactInfo destContact = m.getContact(dest); + to = (destContact == null ? "" : "“" + destContact.name + "” ") + dest; + } else { + to = "Unknown"; + } + System.out.println("To: " + to + " , Message timestamp: " + sentTranscriptMessage.getTimestamp()); SignalServiceDataMessage message = sentTranscriptMessage.getMessage(); handleSignalServiceDataMessage(message, null); } diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 6295f730..bd8d8a5f 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -942,4 +942,8 @@ class Manager implements Signal { contactsFile.delete(); } } + + public ContactInfo getContact(String number) { + return contactStore.getContact(number); + } } From 9427616906ffaea99effde23278cbc073cffdd8a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 19 Jun 2016 18:33:24 +0200 Subject: [PATCH 0118/2005] Improve internal group handling for receiving --- .../java/org/asamk/signal/JsonGroupStore.java | 5 +- src/main/java/org/asamk/signal/Main.java | 22 ++++---- src/main/java/org/asamk/signal/Manager.java | 54 ++++++++++--------- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonGroupStore.java b/src/main/java/org/asamk/signal/JsonGroupStore.java index 29dc0031..7bcf22c2 100644 --- a/src/main/java/org/asamk/signal/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/JsonGroupStore.java @@ -25,11 +25,8 @@ public class JsonGroupStore { groups.put(Base64.encodeBytes(group.groupId), group); } - GroupInfo getGroup(byte[] groupId) throws GroupNotFoundException { + GroupInfo getGroup(byte[] groupId) { GroupInfo g = groups.get(Base64.encodeBytes(groupId)); - if (g == null) { - throw new GroupNotFoundException(groupId); - } return g; } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 5f716e3c..5e6bfadb 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -665,7 +665,7 @@ public class Main { } @Override - public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) { + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content) { SignalServiceAddress source = envelope.getSourceAddress(); ContactInfo sourceContact = m.getContact(source.getNumber()); System.out.println(String.format("Envelope from: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + source.getNumber(), envelope.getSourceDevice())); @@ -682,7 +682,7 @@ public class Main { } else { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - handleSignalServiceDataMessage(message, group); + handleSignalServiceDataMessage(message); } if (content.getSyncMessage().isPresent()) { System.out.println("Received a sync message"); @@ -725,7 +725,7 @@ public class Main { } System.out.println("To: " + to + " , Message timestamp: " + sentTranscriptMessage.getTimestamp()); SignalServiceDataMessage message = sentTranscriptMessage.getMessage(); - handleSignalServiceDataMessage(message, null); + handleSignalServiceDataMessage(message); } } } @@ -735,8 +735,7 @@ public class Main { System.out.println(); } - // TODO remove group parameter - private void handleSignalServiceDataMessage(SignalServiceDataMessage message, GroupInfo group) { + private void handleSignalServiceDataMessage(SignalServiceDataMessage message) { System.out.println("Message timestamp: " + message.getTimestamp()); if (message.getBody().isPresent()) { @@ -748,10 +747,13 @@ public class Main { System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); if (groupInfo.getName().isPresent()) { System.out.println(" Name: " + groupInfo.getName().get()); - } else if (group != null) { - System.out.println(" Name: " + group.name); } else { - System.out.println(" Name: "); + GroupInfo group = m.getGroup(groupInfo.getGroupId()); + if (group != null) { + System.out.println(" Name: " + group.name); + } else { + System.out.println(" Name: "); + } } System.out.println(" Type: " + groupInfo.getType()); if (groupInfo.getMembers().isPresent()) { @@ -799,8 +801,8 @@ public class Main { } @Override - public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, GroupInfo group) { - super.handleMessage(envelope, content, group); + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content) { + super.handleMessage(envelope, content); if (!envelope.isReceipt() && content != null && content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index bd8d8a5f..1b460a77 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -435,7 +435,11 @@ class Manager implements Signal { } SignalServiceDataMessage message = messageBuilder.build(); - Set members = groupStore.getGroup(groupId).members; + GroupInfo g = groupStore.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } + Set members = g.members; members.remove(this.username); sendMessage(message, members); } @@ -450,6 +454,9 @@ class Manager implements Signal { .build(); final GroupInfo g = groupStore.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } g.members.remove(this.username); groupStore.updateGroup(g); @@ -464,6 +471,9 @@ class Manager implements Signal { g.members.add(username); } else { g = groupStore.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } } if (name != null) { @@ -623,18 +633,17 @@ class Manager implements Signal { } public interface ReceiveMessageHandler { - void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group); + void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent); } - private GroupInfo handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { - GroupInfo group = null; + private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { if (message.getGroupInfo().isPresent()) { SignalServiceGroup groupInfo = message.getGroupInfo().get(); switch (groupInfo.getType()) { case UPDATE: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); - } catch (GroupNotFoundException e) { + GroupInfo group; + group = groupStore.getGroup(groupInfo.getGroupId()); + if (group == null) { group = new GroupInfo(groupInfo.getGroupId()); } @@ -663,17 +672,12 @@ class Manager implements Signal { groupStore.updateGroup(group); break; case DELIVER: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); - } catch (GroupNotFoundException e) { - } break; case QUIT: - try { - group = groupStore.getGroup(groupInfo.getGroupId()); + group = groupStore.getGroup(groupInfo.getGroupId()); + if (group != null) { group.members.remove(source); groupStore.updateGroup(group); - } catch (GroupNotFoundException e) { } break; } @@ -692,7 +696,6 @@ class Manager implements Signal { } } } - return group; } public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { @@ -705,7 +708,6 @@ class Manager implements Signal { while (true) { SignalServiceEnvelope envelope; SignalServiceContent content = null; - GroupInfo group = null; try { envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS); if (!envelope.isReceipt()) { @@ -713,13 +715,13 @@ class Manager implements Signal { if (content != null) { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - group = handleSignalServiceDataMessage(message, false, envelope.getSource(), username); + handleSignalServiceDataMessage(message, false, envelope.getSource(), username); } if (content.getSyncMessage().isPresent()) { SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { SignalServiceDataMessage message = syncMessage.getSent().get().getMessage(); - group = handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); + handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); } if (syncMessage.getRequest().isPresent()) { RequestMessage rm = syncMessage.getRequest().get(); @@ -747,10 +749,8 @@ class Manager implements Signal { DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer())); DeviceGroup g; while ((g = s.read()) != null) { - GroupInfo syncGroup; - try { - syncGroup = groupStore.getGroup(g.getId()); - } catch (GroupNotFoundException e) { + GroupInfo syncGroup = groupStore.getGroup(g.getId()); + if (syncGroup == null) { syncGroup = new GroupInfo(g.getId()); } if (g.getName().isPresent()) { @@ -796,7 +796,7 @@ class Manager implements Signal { } } save(); - handler.handleMessage(envelope, content, group); + handler.handleMessage(envelope, content); } catch (TimeoutException e) { if (returnOnTimeout) return; @@ -892,7 +892,7 @@ class Manager implements Signal { try { for (GroupInfo record : groupStore.getGroups()) { out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), - new ArrayList<>(record.members), Optional.absent(), // TODO + new ArrayList<>(record.members), Optional.absent(), // TODO add avatar record.active)); } } finally { @@ -922,7 +922,7 @@ class Manager implements Signal { try { for (ContactInfo record : contactStore.getContacts()) { out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), - Optional.absent())); // TODO + Optional.absent())); // TODO add avatar } } finally { out.close(); @@ -946,4 +946,8 @@ class Manager implements Signal { public ContactInfo getContact(String number) { return contactStore.getContact(number); } + + public GroupInfo getGroup(byte[] groupId) { + return groupStore.getGroup(groupId); + } } From 3e2024ff0a581c730c2ddd4b80e50cb12d721730 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 19 Jun 2016 20:58:01 +0200 Subject: [PATCH 0119/2005] Add avatar image storage Group and contact avatars are now stored in the avatars subfolder of the settings path: - contact-NUMBER - group-GROUP_ID --- src/main/java/org/asamk/signal/GroupInfo.java | 9 +- .../java/org/asamk/signal/JsonGroupStore.java | 6 + src/main/java/org/asamk/signal/Manager.java | 154 ++++++++++++++---- 3 files changed, 136 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/asamk/signal/GroupInfo.java b/src/main/java/org/asamk/signal/GroupInfo.java index cf5d0158..610e8f9f 100644 --- a/src/main/java/org/asamk/signal/GroupInfo.java +++ b/src/main/java/org/asamk/signal/GroupInfo.java @@ -1,5 +1,6 @@ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Collection; @@ -16,8 +17,12 @@ public class GroupInfo { @JsonProperty public Set members = new HashSet<>(); - @JsonProperty - public long avatarId; + private long avatarId; + + @JsonIgnore + public long getAvatarId() { + return avatarId; + } @JsonProperty public boolean active; diff --git a/src/main/java/org/asamk/signal/JsonGroupStore.java b/src/main/java/org/asamk/signal/JsonGroupStore.java index 7bcf22c2..da512706 100644 --- a/src/main/java/org/asamk/signal/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/JsonGroupStore.java @@ -19,6 +19,8 @@ public class JsonGroupStore { @JsonDeserialize(using = JsonGroupStore.GroupsDeserializer.class) private Map groups = new HashMap<>(); + public static List groupsWithLegacyAvatarId = new ArrayList<>(); + private static final ObjectMapper jsonProcessot = new ObjectMapper(); void updateGroup(GroupInfo group) { @@ -48,6 +50,10 @@ public class JsonGroupStore { JsonNode node = jsonParser.getCodec().readTree(jsonParser); for (JsonNode n : node) { GroupInfo g = jsonProcessot.treeToValue(n, GroupInfo.class); + // Check if a legacy avatarId exists + if (g.getAvatarId() != 0) { + groupsWithLegacyAvatarId.add(g); + } groups.put(Base64.encodeBytes(g.groupId), g); } diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 1b460a77..29e75ee9 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -58,6 +58,7 @@ import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -76,6 +77,7 @@ class Manager implements Signal { private final String settingsPath; private final String dataPath; private final String attachmentsPath; + private final String avatarsPath; private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; @@ -97,6 +99,7 @@ class Manager implements Signal { this.settingsPath = settingsPath; this.dataPath = this.settingsPath + "/data"; this.attachmentsPath = this.settingsPath + "/attachments"; + this.avatarsPath = this.settingsPath + "/avatars"; jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. @@ -169,6 +172,25 @@ class Manager implements Signal { if (groupStore == null) { groupStore = new JsonGroupStore(); } + // Copy group avatars that were previously stored in the attachments folder + // to the new avatar folder + if (groupStore.groupsWithLegacyAvatarId.size() > 0) { + for (GroupInfo g : groupStore.groupsWithLegacyAvatarId) { + File avatarFile = getGroupAvatarFile(g.groupId); + File attachmentFile = getAttachmentFile(g.getAvatarId()); + if (!avatarFile.exists() && attachmentFile.exists()) { + try { + new File(avatarsPath).mkdirs(); + Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + // Ignore + } + } + } + groupStore.groupsWithLegacyAvatarId.clear(); + save(); + } + JsonNode contactStoreNode = rootNode.get("contactStore"); if (contactStoreNode != null) { contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class); @@ -402,7 +424,7 @@ class Manager implements Signal { SignalServiceAttachments = new ArrayList<>(attachments.size()); for (String attachment : attachments) { try { - SignalServiceAttachments.add(createAttachment(attachment)); + SignalServiceAttachments.add(createAttachment(new File(attachment))); } catch (IOException e) { throw new AttachmentInvalidException(attachment, e); } @@ -411,14 +433,31 @@ class Manager implements Signal { return SignalServiceAttachments; } - private static SignalServiceAttachment createAttachment(String attachment) throws IOException { - File attachmentFile = new File(attachment); + private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); - String mime = Files.probeContentType(Paths.get(attachment)); + String mime = Files.probeContentType(attachmentFile.toPath()); return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null); } + private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { + File file = getGroupAvatarFile(groupId); + if (!file.exists()) { + return Optional.absent(); + } + + return Optional.of(createAttachment(file)); + } + + private Optional createContactAvatarAttachment(String number) throws IOException { + File file = getContactAvatarFile(number); + if (!file.exists()) { + return Optional.absent(); + } + + return Optional.of(createAttachment(file)); + } + @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) @@ -497,11 +536,14 @@ class Manager implements Signal { .withName(g.name) .withMembers(new ArrayList<>(g.members)); + File aFile = getGroupAvatarFile(g.groupId); if (avatarFile != null) { + new File(avatarsPath).mkdirs(); + Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + if (aFile.exists()) { try { - group.withAvatar(createAttachment(avatarFile)); - // TODO - g.avatarId = 0; + group.withAvatar(createAttachment(aFile)); } catch (IOException e) { throw new AttachmentInvalidException(avatarFile, e); } @@ -650,13 +692,10 @@ class Manager implements Signal { if (groupInfo.getAvatar().isPresent()) { SignalServiceAttachment avatar = groupInfo.getAvatar().get(); if (avatar.isPointer()) { - long avatarId = avatar.asPointer().getId(); try { - retrieveAttachment(avatar.asPointer()); - // TODO store group avatar in /avatar/groups folder - group.avatarId = avatarId; + retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId); } catch (IOException | InvalidMessageException e) { - System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage()); + System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage()); } } } @@ -760,9 +799,7 @@ class Manager implements Signal { syncGroup.active = g.isActive(); if (g.getAvatar().isPresent()) { - byte[] ava = new byte[(int) g.getAvatar().get().getLength()]; - org.whispersystems.signalservice.internal.util.Util.readFully(g.getAvatar().get().getInputStream(), ava); - // TODO store group avatar in /avatar/groups folder + retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); } groupStore.updateGroup(syncGroup); } @@ -783,9 +820,7 @@ class Manager implements Signal { contactStore.updateContact(contact); if (c.getAvatar().isPresent()) { - byte[] ava = new byte[(int) c.getAvatar().get().getLength()]; - org.whispersystems.signalservice.internal.util.Util.readFully(c.getAvatar().get().getInputStream(), ava); - // TODO store contact avatar in /avatar/contacts folder + retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); } } } catch (Exception e) { @@ -810,18 +845,48 @@ class Manager implements Signal { } } + public File getContactAvatarFile(String number) { + return new File(avatarsPath, "contact-" + number); + } + + private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { + new File(avatarsPath).mkdirs(); + if (attachment.isPointer()) { + SignalServiceAttachmentPointer pointer = attachment.asPointer(); + return retrieveAttachment(pointer, getContactAvatarFile(number), false); + } else { + SignalServiceAttachmentStream stream = attachment.asStream(); + return retrieveAttachment(stream, getContactAvatarFile(number)); + } + } + + public File getGroupAvatarFile(byte[] groupId) { + return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_")); + } + + private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { + new File(avatarsPath).mkdirs(); + if (attachment.isPointer()) { + SignalServiceAttachmentPointer pointer = attachment.asPointer(); + return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); + } else { + SignalServiceAttachmentStream stream = attachment.asStream(); + return retrieveAttachment(stream, getGroupAvatarFile(groupId)); + } + } + public File getAttachmentFile(long attachmentId) { return new File(attachmentsPath, attachmentId + ""); } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); - - File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); - InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); - new File(attachmentsPath).mkdirs(); - File outputFile = getAttachmentFile(pointer.getId()); + return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); + } + + private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException { + InputStream input = stream.getInputStream(); + OutputStream output = null; try { output = new FileOutputStream(outputFile); @@ -837,14 +902,15 @@ class Manager implements Signal { } finally { if (output != null) { output.close(); - output = null; - } - if (!tmpFile.delete()) { - System.err.println("Failed to delete temp file: " + tmpFile); } } - if (pointer.getPreview().isPresent()) { + return outputFile; + } + + private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException { + if (storePreview && pointer.getPreview().isPresent()) { File previewFile = new File(outputFile + ".preview"); + OutputStream output = null; try { output = new FileOutputStream(previewFile); byte[] preview = pointer.getPreview().get(); @@ -858,6 +924,32 @@ class Manager implements Signal { } } } + + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); + + File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); + InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); + + OutputStream output = null; + try { + output = new FileOutputStream(outputFile); + byte[] buffer = new byte[4096]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } finally { + if (output != null) { + output.close(); + } + if (!tmpFile.delete()) { + System.err.println("Failed to delete temp file: " + tmpFile); + } + } return outputFile; } @@ -892,7 +984,7 @@ class Manager implements Signal { try { for (GroupInfo record : groupStore.getGroups()) { out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), - new ArrayList<>(record.members), Optional.absent(), // TODO add avatar + new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId), record.active)); } } finally { @@ -922,7 +1014,7 @@ class Manager implements Signal { try { for (ContactInfo record : contactStore.getContacts()) { out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), - Optional.absent())); // TODO add avatar + createContactAvatarAttachment(record.number))); } } finally { out.close(); From 2972dd27c11aa745b72fb3c6e0a37c44ac95c3f1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 19 Jun 2016 21:13:24 +0200 Subject: [PATCH 0120/2005] Use name in groupInfo only if it's a group update Signal-Android send an empty name instead of absent, with group quit messages --- src/main/java/org/asamk/signal/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 5e6bfadb..16df90d8 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -745,7 +745,7 @@ public class Main { SignalServiceGroup groupInfo = message.getGroupInfo().get(); System.out.println("Group info:"); System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); - if (groupInfo.getName().isPresent()) { + if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) { System.out.println(" Name: " + groupInfo.getName().get()); } else { GroupInfo group = m.getGroup(groupInfo.getGroupId()); From c04a21be3da7e9f840a96a147fea3219223c81cb Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 19 Jun 2016 21:25:22 +0200 Subject: [PATCH 0121/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b38e4d47..873ee41a 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.3.1' +version = '0.4.0' compileJava.options.encoding = 'UTF-8' From b1f0d40d4423b93cbf4d0ed2d24bf3010ca8cca3 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Jun 2016 00:14:15 +0200 Subject: [PATCH 0122/2005] Fix creating groups Fixes #16 --- src/main/java/org/asamk/signal/Manager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 29e75ee9..d96537fe 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -555,7 +555,7 @@ class Manager implements Signal { .asGroupMessage(group.build()) .build(); - final Set membersSend = g.members; + final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); sendMessage(message, membersSend); return g.groupId; From 7ce080b6db28909a7749682c01b91be1778bf277 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Jun 2016 12:35:18 +0200 Subject: [PATCH 0123/2005] =?UTF-8?q?Don=E2=80=99t=20remove=20self=20from?= =?UTF-8?q?=20group=20when=20sending=20group=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/asamk/signal/Manager.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index d96537fe..90785824 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -478,9 +478,11 @@ class Manager implements Signal { if (g == null) { throw new GroupNotFoundException(groupId); } - Set members = g.members; - members.remove(this.username); - sendMessage(message, members); + + // Don't send group message to ourself + final List membersSend = new ArrayList<>(g.members); + membersSend.remove(this.username); + sendMessage(message, membersSend); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException { @@ -555,6 +557,7 @@ class Manager implements Signal { .asGroupMessage(group.build()) .build(); + // Don't send group message to ourself final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); sendMessage(message, membersSend); From 24a9398cd73ded2db873998d098e387918b28b3b Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Jun 2016 12:53:10 +0200 Subject: [PATCH 0124/2005] Update README.md --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dea9d145..256445bf 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,11 @@ usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-sys * Update a group - signal-cli -u USERNAME updateGroup -g GROUP_ID -n "New group name" + signal-cli -u USERNAME updateGroup -g GROUP_ID -n "New group name" -a "AVATAR_IMAGE_FILE" + + * Add member to a group + + signal-cli -u USERNAME updateGroup -g GROUP_ID -m "NEW_MEMBER" * Leave a group @@ -114,7 +118,7 @@ For legacy users, the old config directory is used as a fallback: ## Building This project uses [Gradle](http://gradle.org) for building and maintaining -dependencies. +dependencies. If you have a recent gradle version installed, you can replace `./gradlew` with `gradle` in the following steps. 1. Checkout the source somewhere on your filesystem with From bc17f9317e09c97907123da06a44b42b67f16b0d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Jul 2016 12:43:30 +0200 Subject: [PATCH 0125/2005] Update README.md Add Installation section Closes #18 --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 256445bf..cc9bde5f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,18 @@ signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. To be able to receiving messages signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libsignal-service-java/pull/5) nor [provisioning as a slave device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For registering you need a phone number where you can receive SMS or incoming calls. It is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings. +## Installation + +You can [build signal-cli](#building) yourself, or use the provided binary files, which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/). You need to have at least JRE 7 installed, to run signal-cli. + +### Install system-wide on Linux +```sh +export VERSION= +wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz +sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt +sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ +``` + ## Usage usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-system] {link,addDevice,listDevices,removeDevice,register,verify,send,quitGroup,updateGroup,receive,daemon} ... From d5797ebb6941a63d783114916ff3cab95290ee48 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 4 Jul 2016 09:46:55 +0200 Subject: [PATCH 0126/2005] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc9bde5f..6da7b477 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ It is primarily intended to be used on servers to notify admins of important eve ## Installation -You can [build signal-cli](#building) yourself, or use the provided binary files, which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/). You need to have at least JRE 7 installed, to run signal-cli. +You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/). You need to have at least JRE 7 installed, to run signal-cli. ### Install system-wide on Linux ```sh From c5ac72a9a592bf9e7a6ce1c5e2bc5c540b48787e Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Jul 2016 01:06:08 +0200 Subject: [PATCH 0127/2005] Lock config file Fixes #19 --- src/main/java/org/asamk/signal/Manager.java | 32 +++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 90785824..69800a82 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -18,6 +18,8 @@ package org.asamk.signal; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -56,6 +58,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URLDecoder; import java.net.URLEncoder; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; @@ -79,6 +84,9 @@ class Manager implements Signal { private final String attachmentsPath; private final String avatarsPath; + private FileChannel fileChannel; + private FileLock lock; + private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; @@ -105,6 +113,8 @@ class Manager implements Signal { jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + jsonProcessot.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); + jsonProcessot.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } public String getUsername() { @@ -141,8 +151,22 @@ class Manager implements Signal { return node; } + private void openFileChannel() throws IOException { + if (fileChannel != null) + return; + + fileChannel = new RandomAccessFile(new File(getFileName()), "rw").getChannel(); + lock = fileChannel.tryLock(); + if (lock == null) { + System.err.println("Config file is in use by another instance, waiting…"); + lock = fileChannel.lock(); + System.err.println("Config file lock acquired."); + } + } + public void load() throws IOException, InvalidKeyException { - JsonNode rootNode = jsonProcessot.readTree(new File(getFileName())); + openFileChannel(); + JsonNode rootNode = jsonProcessot.readTree(Channels.newInputStream(fileChannel)); JsonNode node = rootNode.get("deviceId"); if (node != null) { @@ -227,7 +251,11 @@ class Manager implements Signal { .putPOJO("contactStore", contactStore) ; try { - jsonProcessot.writeValue(new File(getFileName()), rootNode); + openFileChannel(); + fileChannel.position(0); + jsonProcessot.writeValue(Channels.newOutputStream(fileChannel), rootNode); + fileChannel.truncate(fileChannel.position()); + fileChannel.force(false); } catch (Exception e) { System.err.println(String.format("Error saving file: %s", e.getMessage())); } From 74fb7d937777562996ffe9a745b377e8a2b62894 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 8 Jul 2016 11:32:50 +0200 Subject: [PATCH 0128/2005] Fix typo --- src/main/java/org/asamk/signal/Manager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 69800a82..7672e19c 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -274,12 +274,12 @@ class Manager implements Signal { return registered; } - public void register(boolean voiceVerication) throws IOException { + public void register(boolean voiceVerification) throws IOException { password = Util.getSecret(18); accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); - if (voiceVerication) + if (voiceVerification) accountManager.requestVoiceVerificationCode(); else accountManager.requestSmsVerificationCode(); From c0a0f89896c3f685f8016ac853fb93c7cee75863 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 8 Jul 2016 11:31:41 +0200 Subject: [PATCH 0129/2005] Improve exception handling --- src/main/java/org/asamk/Signal.java | 8 +- src/main/java/org/asamk/signal/Main.java | 8 -- src/main/java/org/asamk/signal/Manager.java | 89 ++++++++++++--------- 3 files changed, 53 insertions(+), 52 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index e115bb7c..9a868109 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -12,13 +12,13 @@ import java.io.IOException; import java.util.List; public interface Signal extends DBusInterface { - void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException; + void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; - void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException; + void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; - void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException; + void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions; - void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException, UntrustedIdentityException; + void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; class MessageReceived extends DBusSignal { private long timestamp; diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 16df90d8..14eb985a 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -265,8 +265,6 @@ public class Main { handleAssertionError(e); } catch (DBusExecutionException e) { handleDBusExecutionException(e); - } catch (UntrustedIdentityException e) { - e.printStackTrace(); } } else { String messageText = ns.getString("message"); @@ -305,8 +303,6 @@ public class Main { System.exit(1); } catch (DBusExecutionException e) { handleDBusExecutionException(e); - } catch (UntrustedIdentityException e) { - e.printStackTrace(); } } @@ -385,8 +381,6 @@ public class Main { handleAssertionError(e); } catch (GroupNotFoundException e) { handleGroupNotFoundException(e); - } catch (UntrustedIdentityException e) { - e.printStackTrace(); } break; @@ -419,8 +413,6 @@ public class Main { handleGroupNotFoundException(e); } catch (EncapsulatedExceptions e) { handleEncapsulatedExceptions(e); - } catch (UntrustedIdentityException e) { - e.printStackTrace(); } break; diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 7672e19c..2edeb921 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -47,8 +47,7 @@ import org.whispersystems.signalservice.api.messages.*; import org.whispersystems.signalservice.api.messages.multidevice.*; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; -import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; -import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.push.exceptions.*; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; @@ -489,7 +488,7 @@ class Manager implements Signal { @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) - throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { + throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); @@ -513,7 +512,7 @@ class Manager implements Signal { sendMessage(message, membersSend); } - public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException { + public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupId) .build(); @@ -532,7 +531,7 @@ class Manager implements Signal { sendMessage(message, g.members); } - public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException { + public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { GroupInfo g; if (groupId == null) { // Create new group @@ -594,7 +593,7 @@ class Manager implements Signal { @Override public void sendMessage(String message, List attachments, String recipient) - throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException { + throws EncapsulatedExceptions, AttachmentInvalidException, IOException { List recipients = new ArrayList<>(1); recipients.add(recipient); sendMessage(message, attachments, recipients); @@ -603,7 +602,7 @@ class Manager implements Signal { @Override public void sendMessage(String messageText, List attachments, List recipients) - throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException { + throws IOException, EncapsulatedExceptions, AttachmentInvalidException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); @@ -614,7 +613,7 @@ class Manager implements Signal { } @Override - public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() .asEndSessionMessage() .build(); @@ -627,8 +626,6 @@ class Manager implements Signal { SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { sendMessage(message); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -639,65 +636,74 @@ class Manager implements Signal { SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { sendMessage(message); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } } private void sendMessage(SignalServiceSyncMessage message) - throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + throws IOException, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); messageSender.sendMessage(message); } private void sendMessage(SignalServiceDataMessage message, Collection recipients) - throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + throws EncapsulatedExceptions, IOException { + Set recipientsTS = new HashSet<>(recipients.size()); + for (String recipient : recipients) { + try { + recipientsTS.add(getPushAddress(recipient)); + } catch (InvalidNumberException e) { + System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + save(); + return; + } + } + try { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); - Set recipientsTS = new HashSet<>(recipients.size()); - for (String recipient : recipients) { - try { - recipientsTS.add(getPushAddress(recipient)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - save(); - return; - } - } - if (message.getGroupInfo().isPresent()) { messageSender.sendMessage(new ArrayList<>(recipientsTS), message); } else { // Send to all individually, so sync messages are sent correctly + List untrustedIdentities = new LinkedList<>(); + List unregisteredUsers = new LinkedList<>(); + List networkExceptions = new LinkedList<>(); for (SignalServiceAddress address : recipientsTS) { - messageSender.sendMessage(address, message); + try { + messageSender.sendMessage(address, message); + } catch (UntrustedIdentityException e) { + untrustedIdentities.add(e); + } catch (UnregisteredUserException e) { + unregisteredUsers.add(e); + } catch (PushNetworkException e) { + networkExceptions.add(new NetworkFailureException(address.getNumber(), e)); + } + } + if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { + throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions); } } - + } finally { if (message.isEndSession()) { for (SignalServiceAddress recipient : recipientsTS) { handleEndSession(recipient.getNumber()); } } - } finally { save(); } } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) { + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException { SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore); try { return cipher.decrypt(envelope); } catch (Exception e) { - // TODO handle all exceptions - e.printStackTrace(); - return null; + throw e; } } @@ -781,7 +787,14 @@ class Manager implements Signal { try { envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS); if (!envelope.isReceipt()) { - content = decryptMessage(envelope); + Exception exception; + try { + content = decryptMessage(envelope); + } catch (Exception e) { + exception = e; + // TODO pass exception to handler instead + e.printStackTrace(); + } if (content != null) { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); @@ -798,8 +811,6 @@ class Manager implements Signal { if (rm.isContactsRequest()) { try { sendContacts(); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -807,8 +818,6 @@ class Manager implements Signal { if (rm.isGroupsRequest()) { try { sendGroups(); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - encapsulatedExceptions.printStackTrace(); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -1007,7 +1016,7 @@ class Manager implements Signal { return false; } - private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + private void sendGroups() throws IOException, UntrustedIdentityException { File groupsFile = File.createTempFile("multidevice-group-update", ".tmp"); try { @@ -1037,7 +1046,7 @@ class Manager implements Signal { } } - private void sendContacts() throws IOException, EncapsulatedExceptions, UntrustedIdentityException { + private void sendContacts() throws IOException, UntrustedIdentityException { File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); try { From 9f075da269ff21a1396e4415fb46490d90f228ca Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 14 Jul 2016 16:07:34 +0200 Subject: [PATCH 0130/2005] Prevent NullPointerException when sending sync groups ContentType was null, if it could not be determined --- src/main/java/org/asamk/signal/Manager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 2edeb921..2ea9249e 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -464,6 +464,9 @@ class Manager implements Signal { InputStream attachmentStream = new FileInputStream(attachmentFile); final long attachmentSize = attachmentFile.length(); String mime = Files.probeContentType(attachmentFile.toPath()); + if (mime == null) { + mime = "application/octet-stream"; + } return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null); } From 0f0d8a873a8bddb2cd6bc5738e477fa9d14807b9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 14 Jul 2016 16:20:14 +0200 Subject: [PATCH 0131/2005] Improve return codes Always return non-zero code, when sending failed Fixes #22 --- src/main/java/org/asamk/signal/Main.java | 111 +++++++++++++---------- 1 file changed, 65 insertions(+), 46 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 14eb985a..3b76a2b8 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -64,6 +64,11 @@ public class Main { System.exit(1); } + int res = handleCommands(ns); + System.exit(res); + } + + private static int handleCommands(Namespace ns) { final String username = ns.getString("username"); Manager m; Signal ts; @@ -87,8 +92,7 @@ public class Main { if (dBusConn != null) { dBusConn.disconnect(); } - System.exit(3); - return; + return 3; } } else { String settingsPath = ns.getString("config"); @@ -109,8 +113,7 @@ public class Main { m.load(); } catch (Exception e) { System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage()); - System.exit(2); - return; + return 2; } } } @@ -119,7 +122,7 @@ public class Main { case "register": if (dBusConn != null) { System.err.println("register is not yet implemented via dbus"); - System.exit(1); + return 1; } if (!m.userHasKeys()) { m.createNewIdentity(); @@ -128,33 +131,33 @@ public class Main { m.register(ns.getBoolean("voice")); } catch (IOException e) { System.err.println("Request verify error: " + e.getMessage()); - System.exit(3); + return 3; } break; case "verify": if (dBusConn != null) { System.err.println("verify is not yet implemented via dbus"); - System.exit(1); + return 1; } if (!m.userHasKeys()) { System.err.println("User has no keys, first call register."); - System.exit(1); + return 1; } if (m.isRegistered()) { System.err.println("User registration is already verified"); - System.exit(1); + return 1; } try { m.verifyAccount(ns.getString("verificationCode")); } catch (IOException e) { System.err.println("Verify error: " + e.getMessage()); - System.exit(3); + return 3; } break; case "link": if (dBusConn != null) { System.err.println("link is not yet implemented via dbus"); - System.exit(1); + return 1; } // When linking, username is null and we always have to create keys @@ -170,48 +173,48 @@ public class Main { System.out.println("Associated with: " + m.getUsername()); } catch (TimeoutException e) { System.err.println("Link request timed out, please try again."); - System.exit(3); + return 3; } catch (IOException e) { System.err.println("Link request error: " + e.getMessage()); - System.exit(3); + return 3; } catch (InvalidKeyException e) { e.printStackTrace(); - System.exit(3); + return 2; } catch (UserAlreadyExists e) { System.err.println("The user " + e.getUsername() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again."); - System.exit(3); + return 1; } break; case "addDevice": if (dBusConn != null) { System.err.println("link is not yet implemented via dbus"); - System.exit(1); + return 1; } if (!m.isRegistered()) { System.err.println("User is not registered."); - System.exit(1); + return 1; } try { m.addDeviceLink(new URI(ns.getString("uri"))); } catch (IOException e) { e.printStackTrace(); - System.exit(3); + return 3; } catch (InvalidKeyException e) { e.printStackTrace(); - System.exit(2); + return 2; } catch (URISyntaxException e) { e.printStackTrace(); - System.exit(2); + return 2; } break; case "listDevices": if (dBusConn != null) { System.err.println("listDevices is not yet implemented via dbus"); - System.exit(1); + return 1; } if (!m.isRegistered()) { System.err.println("User is not registered."); - System.exit(1); + return 1; } try { List devices = m.getLinkedDevices(); @@ -223,48 +226,52 @@ public class Main { } } catch (IOException e) { e.printStackTrace(); - System.exit(3); + return 3; } break; case "removeDevice": if (dBusConn != null) { System.err.println("removeDevice is not yet implemented via dbus"); - System.exit(1); + return 1; } if (!m.isRegistered()) { System.err.println("User is not registered."); - System.exit(1); + return 1; } try { int deviceId = ns.getInt("deviceId"); m.removeLinkedDevices(deviceId); } catch (IOException e) { e.printStackTrace(); - System.exit(3); + return 3; } break; case "send": if (dBusConn == null && !m.isRegistered()) { System.err.println("User is not registered."); - System.exit(1); + return 1; } if (ns.getBoolean("endsession")) { if (ns.getList("recipient") == null) { System.err.println("No recipients given"); System.err.println("Aborting sending."); - System.exit(1); + return 1; } try { ts.sendEndSessionMessage(ns.getList("recipient")); } catch (IOException e) { handleIOException(e); + return 3; } catch (EncapsulatedExceptions e) { handleEncapsulatedExceptions(e); + return 3; } catch (AssertionError e) { handleAssertionError(e); + return 1; } catch (DBusExecutionException e) { handleDBusExecutionException(e); + return 1; } } else { String messageText = ns.getString("message"); @@ -274,7 +281,7 @@ public class Main { } catch (IOException e) { System.err.println("Failed to read message from stdin: " + e.getMessage()); System.err.println("Aborting sending."); - System.exit(1); + return 1; } } @@ -291,18 +298,23 @@ public class Main { } } catch (IOException e) { handleIOException(e); + return 3; } catch (EncapsulatedExceptions e) { handleEncapsulatedExceptions(e); + return 3; } catch (AssertionError e) { handleAssertionError(e); + return 1; } catch (GroupNotFoundException e) { handleGroupNotFoundException(e); + return 1; } catch (AttachmentInvalidException e) { System.err.println("Failed to add attachment: " + e.getMessage()); System.err.println("Aborting sending."); - System.exit(1); + return 1; } catch (DBusExecutionException e) { handleDBusExecutionException(e); + return 1; } } @@ -330,18 +342,19 @@ public class Main { }); } catch (DBusException e) { e.printStackTrace(); + return 1; } while (true) { try { Thread.sleep(10000); } catch (InterruptedException e) { - System.exit(0); + return 0; } } } if (!m.isRegistered()) { System.err.println("User is not registered."); - System.exit(1); + return 1; } int timeout = 5; if (ns.getInt("timeout") != null) { @@ -356,42 +369,47 @@ public class Main { m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m)); } catch (IOException e) { System.err.println("Error while receiving messages: " + e.getMessage()); - System.exit(3); + return 3; } catch (AssertionError e) { handleAssertionError(e); + return 1; } break; case "quitGroup": if (dBusConn != null) { System.err.println("quitGroup is not yet implemented via dbus"); - System.exit(1); + return 1; } if (!m.isRegistered()) { System.err.println("User is not registered."); - System.exit(1); + return 1; } try { m.sendQuitGroupMessage(decodeGroupId(ns.getString("group"))); } catch (IOException e) { handleIOException(e); + return 3; } catch (EncapsulatedExceptions e) { handleEncapsulatedExceptions(e); + return 3; } catch (AssertionError e) { handleAssertionError(e); + return 1; } catch (GroupNotFoundException e) { handleGroupNotFoundException(e); + return 1; } break; case "updateGroup": if (dBusConn != null) { System.err.println("updateGroup is not yet implemented via dbus"); - System.exit(1); + return 1; } if (!m.isRegistered()) { System.err.println("User is not registered."); - System.exit(1); + return 1; } try { @@ -405,25 +423,28 @@ public class Main { } } catch (IOException e) { handleIOException(e); + return 3; } catch (AttachmentInvalidException e) { System.err.println("Failed to add avatar attachment for group\": " + e.getMessage()); System.err.println("Aborting sending."); - System.exit(1); + return 1; } catch (GroupNotFoundException e) { handleGroupNotFoundException(e); + return 1; } catch (EncapsulatedExceptions e) { handleEncapsulatedExceptions(e); + return 3; } break; case "daemon": if (dBusConn != null) { System.err.println("Stop it."); - System.exit(1); + return 1; } if (!m.isRegistered()) { System.err.println("User is not registered."); - System.exit(1); + return 1; } DBusConnection conn = null; try { @@ -439,15 +460,16 @@ public class Main { conn.requestBusName(SIGNAL_BUSNAME); } catch (DBusException e) { e.printStackTrace(); - System.exit(3); + return 2; } try { m.receiveMessages(3600, false, new DbusReceiveMessageHandler(m, conn)); } catch (IOException e) { System.err.println("Error while receiving messages: " + e.getMessage()); - System.exit(3); + return 3; } catch (AssertionError e) { handleAssertionError(e); + return 1; } } finally { if (conn != null) { @@ -457,7 +479,7 @@ public class Main { break; } - System.exit(0); + return 0; } finally { if (dBusConn != null) { dBusConn.disconnect(); @@ -468,13 +490,11 @@ public class Main { private static void handleGroupNotFoundException(GroupNotFoundException e) { System.err.println("Failed to send to group: " + e.getMessage()); System.err.println("Aborting sending."); - System.exit(1); } private static void handleDBusExecutionException(DBusExecutionException e) { System.err.println("Cannot connect to dbus: " + e.getMessage()); System.err.println("Aborting."); - System.exit(1); } private static byte[] decodeGroupId(String groupId) { @@ -617,7 +637,6 @@ public class Main { System.err.println("Failed to send/receive message (Assertion): " + e.getMessage()); e.printStackTrace(); System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); - System.exit(1); } private static void handleEncapsulatedExceptions(EncapsulatedExceptions e) { From f2c2597379280e0aea56932647eded1c5f7cc72c Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Jul 2016 15:57:17 +0200 Subject: [PATCH 0132/2005] Implement trustLevel for IdentityKeys --- .../asamk/signal/JsonIdentityKeyStore.java | 96 +++++++++++++++---- 1 file changed, 77 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java index c1ef428b..332d8712 100644 --- a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java @@ -10,12 +10,14 @@ import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.state.IdentityKeyStore; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; class JsonIdentityKeyStore implements IdentityKeyStore { - private final Map trustedKeys = new HashMap<>(); + private final Map> trustedKeys = new HashMap<>(); private final IdentityKeyPair identityKeyPair; private final int localRegistrationId; @@ -26,10 +28,6 @@ class JsonIdentityKeyStore implements IdentityKeyStore { this.localRegistrationId = localRegistrationId; } - public void addTrustedKeys(Map keyMap) { - trustedKeys.putAll(keyMap); - } - @Override public IdentityKeyPair getIdentityKeyPair() { return identityKeyPair; @@ -42,13 +40,41 @@ class JsonIdentityKeyStore implements IdentityKeyStore { @Override public void saveIdentity(String name, IdentityKey identityKey) { - trustedKeys.put(name, identityKey); + saveIdentity(name, identityKey, TrustLevel.TRUSTED_UNVERIFIED); + } + + public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) { + List identities = trustedKeys.get(name); + if (identities == null) { + identities = new ArrayList<>(); + trustedKeys.put(name, identities); + } else { + for (Identity id : identities) { + if (!id.identityKey.equals(identityKey)) + continue; + + id.trustLevel = trustLevel; + return; + } + } + identities.add(new Identity(identityKey, trustLevel)); } @Override public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - IdentityKey trusted = trustedKeys.get(name); - return (trusted == null || trusted.equals(identityKey)); + List identities = trustedKeys.get(name); + if (identities == null) { + // Trust on first use + return true; + } + + for (Identity id : identities) { + if (id.identityKey.equals(identityKey)) { + return id.isTrusted(); + } + } + + return false; } public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer { @@ -62,24 +88,23 @@ class JsonIdentityKeyStore implements IdentityKeyStore { IdentityKeyPair identityKeyPair = new IdentityKeyPair(Base64.decode(node.get("identityKey").asText())); - Map trustedKeyMap = new HashMap<>(); + JsonIdentityKeyStore keyStore = new JsonIdentityKeyStore(identityKeyPair, localRegistrationId); + JsonNode trustedKeysNode = node.get("trustedKeys"); if (trustedKeysNode.isArray()) { for (JsonNode trustedKey : trustedKeysNode) { String trustedKeyName = trustedKey.get("name").asText(); try { - trustedKeyMap.put(trustedKeyName, new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0)); + IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0); + TrustLevel trustLevel = trustedKey.has("trustLevel") ? TrustLevel.fromInt(trustedKey.get("trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED; + keyStore.saveIdentity(trustedKeyName, id, trustLevel); } catch (InvalidKeyException | IOException e) { System.out.println(String.format("Error while decoding key for: %s", trustedKeyName)); } } } - JsonIdentityKeyStore keyStore = new JsonIdentityKeyStore(identityKeyPair, localRegistrationId); - keyStore.addTrustedKeys(trustedKeyMap); - return keyStore; - } catch (InvalidKeyException e) { throw new IOException(e); } @@ -94,14 +119,47 @@ class JsonIdentityKeyStore implements IdentityKeyStore { json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId()); json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize())); json.writeArrayFieldStart("trustedKeys"); - for (Map.Entry trustedKey : jsonIdentityKeyStore.trustedKeys.entrySet()) { - json.writeStartObject(); - json.writeStringField("name", trustedKey.getKey()); - json.writeStringField("identityKey", Base64.encodeBytes(trustedKey.getValue().serialize())); - json.writeEndObject(); + for (Map.Entry> trustedKey : jsonIdentityKeyStore.trustedKeys.entrySet()) { + for (Identity id : trustedKey.getValue()) { + json.writeStartObject(); + json.writeStringField("name", trustedKey.getKey()); + json.writeStringField("identityKey", Base64.encodeBytes(id.identityKey.serialize())); + json.writeNumberField("trustLevel", id.trustLevel.ordinal()); + json.writeEndObject(); + } } json.writeEndArray(); json.writeEndObject(); } } + + private enum TrustLevel { + UNTRUSTED, + TRUSTED_UNVERIFIED, + TRUSTED_VERIFIED; + + private static TrustLevel[] cachedValues = null; + + public static TrustLevel fromInt(int i) { + if (TrustLevel.cachedValues == null) { + TrustLevel.cachedValues = TrustLevel.values(); + } + return TrustLevel.cachedValues[i]; + } + } + + private class Identity { + IdentityKey identityKey; + TrustLevel trustLevel; + + public Identity(IdentityKey identityKey, TrustLevel trustLevel) { + this.identityKey = identityKey; + this.trustLevel = trustLevel; + } + + public boolean isTrusted() { + return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || + trustLevel == TrustLevel.TRUSTED_VERIFIED; + } + } } From 55d485de88bf2f67f64be3978cf7c5649f1cc36f Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 8 Jul 2016 10:14:53 +0200 Subject: [PATCH 0133/2005] Add added timestamp to Identities --- src/main/java/org/asamk/signal/Hex.java | 22 ++++++++ .../asamk/signal/JsonIdentityKeyStore.java | 55 +++++++++++-------- .../java/org/asamk/signal/TrustLevel.java | 16 ++++++ 3 files changed, 69 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/asamk/signal/Hex.java create mode 100644 src/main/java/org/asamk/signal/TrustLevel.java diff --git a/src/main/java/org/asamk/signal/Hex.java b/src/main/java/org/asamk/signal/Hex.java new file mode 100644 index 00000000..43d77cc5 --- /dev/null +++ b/src/main/java/org/asamk/signal/Hex.java @@ -0,0 +1,22 @@ +package org.asamk.signal; + +public class Hex { + + private final static char[] HEX_DIGITS = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + public static String toStringCondensed(byte[] bytes) { + StringBuffer buf = new StringBuffer(); + for (int i = 0; i < bytes.length; i++) { + appendHexChar(buf, bytes[i]); + } + return buf.toString(); + } + + private static void appendHexChar(StringBuffer buf, int b) { + buf.append(HEX_DIGITS[(b >> 4) & 0xf]); + buf.append(HEX_DIGITS[b & 0xf]); + buf.append(" "); + } +} diff --git a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java index 332d8712..d4d0ea3e 100644 --- a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java @@ -10,10 +10,7 @@ import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.state.IdentityKeyStore; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; class JsonIdentityKeyStore implements IdentityKeyStore { @@ -40,10 +37,18 @@ class JsonIdentityKeyStore implements IdentityKeyStore { @Override public void saveIdentity(String name, IdentityKey identityKey) { - saveIdentity(name, identityKey, TrustLevel.TRUSTED_UNVERIFIED); + saveIdentity(name, identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); } - public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) { + /** + * Adds or updates the given identityKey for the user name and sets the trustLevel and added timestamp. + * + * @param name User name, i.e. phone number + * @param identityKey The user's public key + * @param trustLevel + * @param added Added timestamp, if null and the key is newly added, the current time is used. + */ + public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel, Date added) { List identities = trustedKeys.get(name); if (identities == null) { identities = new ArrayList<>(); @@ -54,10 +59,13 @@ class JsonIdentityKeyStore implements IdentityKeyStore { continue; id.trustLevel = trustLevel; + if (added != null) { + id.added = added; + } return; } } - identities.add(new Identity(identityKey, trustLevel)); + identities.add(new Identity(identityKey, trustLevel, added != null ? added : new Date())); } @Override @@ -97,7 +105,8 @@ class JsonIdentityKeyStore implements IdentityKeyStore { try { IdentityKey id = new IdentityKey(Base64.decode(trustedKey.get("identityKey").asText()), 0); TrustLevel trustLevel = trustedKey.has("trustLevel") ? TrustLevel.fromInt(trustedKey.get("trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED; - keyStore.saveIdentity(trustedKeyName, id, trustLevel); + Date added = trustedKey.has("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp").asLong()) : new Date(); + keyStore.saveIdentity(trustedKeyName, id, trustLevel, added); } catch (InvalidKeyException | IOException e) { System.out.println(String.format("Error while decoding key for: %s", trustedKeyName)); } @@ -125,6 +134,7 @@ class JsonIdentityKeyStore implements IdentityKeyStore { json.writeStringField("name", trustedKey.getKey()); json.writeStringField("identityKey", Base64.encodeBytes(id.identityKey.serialize())); json.writeNumberField("trustLevel", id.trustLevel.ordinal()); + json.writeNumberField("addedTimestamp", id.added.getTime()); json.writeEndObject(); } } @@ -133,33 +143,30 @@ class JsonIdentityKeyStore implements IdentityKeyStore { } } - private enum TrustLevel { - UNTRUSTED, - TRUSTED_UNVERIFIED, - TRUSTED_VERIFIED; - - private static TrustLevel[] cachedValues = null; - - public static TrustLevel fromInt(int i) { - if (TrustLevel.cachedValues == null) { - TrustLevel.cachedValues = TrustLevel.values(); - } - return TrustLevel.cachedValues[i]; - } - } - - private class Identity { + public class Identity { IdentityKey identityKey; TrustLevel trustLevel; + Date added; public Identity(IdentityKey identityKey, TrustLevel trustLevel) { this.identityKey = identityKey; this.trustLevel = trustLevel; + this.added = new Date(); + } + + public Identity(IdentityKey identityKey, TrustLevel trustLevel, Date added) { + this.identityKey = identityKey; + this.trustLevel = trustLevel; + this.added = added; } public boolean isTrusted() { return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; } + + public String getFingerprint() { + return Hex.toStringCondensed(identityKey.getPublicKey().serialize()); + } } } diff --git a/src/main/java/org/asamk/signal/TrustLevel.java b/src/main/java/org/asamk/signal/TrustLevel.java new file mode 100644 index 00000000..e9e7796d --- /dev/null +++ b/src/main/java/org/asamk/signal/TrustLevel.java @@ -0,0 +1,16 @@ +package org.asamk.signal; + +public enum TrustLevel { + UNTRUSTED, + TRUSTED_UNVERIFIED, + TRUSTED_VERIFIED; + + private static TrustLevel[] cachedValues = null; + + public static TrustLevel fromInt(int i) { + if (TrustLevel.cachedValues == null) { + TrustLevel.cachedValues = TrustLevel.values(); + } + return TrustLevel.cachedValues[i]; + } +} From bfb51e414b42c69538498e1aa5cbb7421138d5e2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 8 Jul 2016 11:32:41 +0200 Subject: [PATCH 0134/2005] Store untrusted identities in identityKeyStore --- .../asamk/signal/JsonSignalProtocolStore.java | 4 ++++ src/main/java/org/asamk/signal/Manager.java | 23 +++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java index 0d9c4b69..015707ae 100644 --- a/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java @@ -68,6 +68,10 @@ class JsonSignalProtocolStore implements SignalProtocolStore { identityKeyStore.saveIdentity(name, identityKey); } + public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) { + identityKeyStore.saveIdentity(name, identityKey, trustLevel, null); + } + @Override public boolean isTrustedIdentity(String name, IdentityKey identityKey) { return identityKeyStore.isTrustedIdentity(name, identityKey); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 2ea9249e..3ce9dfbe 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -32,7 +32,6 @@ import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.KeyHelper; import org.whispersystems.libsignal.util.Medium; @@ -96,7 +95,7 @@ class Manager implements Signal { private boolean registered = false; - private SignalProtocolStore signalProtocolStore; + private JsonSignalProtocolStore signalProtocolStore; private SignalServiceAccountManager accountManager; private JsonGroupStore groupStore; private JsonContactsStore contactStore; @@ -648,7 +647,12 @@ class Manager implements Signal { throws IOException, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); - messageSender.sendMessage(message); + try { + messageSender.sendMessage(message); + } catch (UntrustedIdentityException e) { + signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + throw e; + } } private void sendMessage(SignalServiceDataMessage message, Collection recipients) @@ -670,7 +674,13 @@ class Manager implements Signal { deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); if (message.getGroupInfo().isPresent()) { - messageSender.sendMessage(new ArrayList<>(recipientsTS), message); + try { + messageSender.sendMessage(new ArrayList<>(recipientsTS), message); + } catch (EncapsulatedExceptions encapsulatedExceptions) { + for (UntrustedIdentityException e : encapsulatedExceptions.getUntrustedIdentityExceptions()) { + signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + } + } } else { // Send to all individually, so sync messages are sent correctly List untrustedIdentities = new LinkedList<>(); @@ -680,6 +690,7 @@ class Manager implements Signal { try { messageSender.sendMessage(address, message); } catch (UntrustedIdentityException e) { + signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); untrustedIdentities.add(e); } catch (UnregisteredUserException e) { unregisteredUsers.add(e); @@ -705,6 +716,10 @@ class Manager implements Signal { SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore); try { return cipher.decrypt(envelope); + } catch (org.whispersystems.libsignal.UntrustedIdentityException e) { + // TODO temporarily store message, until user has accepted the key + signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); + throw e; } catch (Exception e) { throw e; } From f095d947f892830e1ced4af450df77a8f7ae6d5d Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 14 Jul 2016 15:35:59 +0200 Subject: [PATCH 0135/2005] Implement listIdentities and trust commands Print the fingerprints of all known phone numbers and can set their trust --- src/main/java/org/asamk/signal/Hex.java | 9 +++ .../asamk/signal/JsonIdentityKeyStore.java | 14 +++- .../asamk/signal/JsonSignalProtocolStore.java | 9 +++ src/main/java/org/asamk/signal/Main.java | 76 +++++++++++++++++++ src/main/java/org/asamk/signal/Manager.java | 50 ++++++++++++ 5 files changed, 156 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/Hex.java b/src/main/java/org/asamk/signal/Hex.java index 43d77cc5..696ca62b 100644 --- a/src/main/java/org/asamk/signal/Hex.java +++ b/src/main/java/org/asamk/signal/Hex.java @@ -19,4 +19,13 @@ public class Hex { buf.append(HEX_DIGITS[b & 0xf]); buf.append(" "); } + + public static byte[] toByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } } diff --git a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java index d4d0ea3e..7cde350c 100644 --- a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java @@ -85,6 +85,16 @@ class JsonIdentityKeyStore implements IdentityKeyStore { return false; } + public Map> getIdentities() { + // TODO deep copy + return trustedKeys; + } + + public List getIdentities(String name) { + // TODO deep copy + return trustedKeys.get(name); + } + public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer { @Override @@ -165,8 +175,8 @@ class JsonIdentityKeyStore implements IdentityKeyStore { trustLevel == TrustLevel.TRUSTED_VERIFIED; } - public String getFingerprint() { - return Hex.toStringCondensed(identityKey.getPublicKey().serialize()); + public byte[] getFingerprint() { + return identityKey.getPublicKey().serialize(); } } } diff --git a/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java index 015707ae..a3159e48 100644 --- a/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java @@ -13,6 +13,7 @@ import org.whispersystems.libsignal.state.SignalProtocolStore; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import java.util.List; +import java.util.Map; class JsonSignalProtocolStore implements SignalProtocolStore { @@ -72,6 +73,14 @@ class JsonSignalProtocolStore implements SignalProtocolStore { identityKeyStore.saveIdentity(name, identityKey, trustLevel, null); } + public Map> getIdentities() { + return identityKeyStore.getIdentities(); + } + + public List getIdentities(String name) { + return identityKeyStore.getIdentities(name); + } + @Override public boolean isTrustedIdentity(String name, IdentityKey identityKey) { return identityKeyStore.isTrustedIdentity(name, identityKey); diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 3b76a2b8..25807a6b 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -48,6 +48,8 @@ import java.nio.charset.Charset; import java.security.Security; import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.concurrent.TimeoutException; public class Main { @@ -436,6 +438,65 @@ public class Main { return 3; } + break; + case "listIdentities": + if (dBusConn != null) { + System.err.println("listIdentities is not yet implemented via dbus"); + return 1; + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + if (ns.get("number") == null) { + for (Map.Entry> keys : m.getIdentities().entrySet()) { + for (JsonIdentityKeyStore.Identity id : keys.getValue()) { + System.out.println(String.format("%s: %s Added: %s Fingerprint: %s", keys.getKey(), id.trustLevel, id.added, Hex.toStringCondensed(id.getFingerprint()))); + } + } + } else { + String number = ns.getString("number"); + for (JsonIdentityKeyStore.Identity id : m.getIdentities(number)) { + System.out.println(String.format("%s: %s Added: %s Fingerprint: %s", number, id.trustLevel, id.added, Hex.toStringCondensed(id.getFingerprint()))); + } + } + break; + case "trust": + if (dBusConn != null) { + System.err.println("trust is not yet implemented via dbus"); + return 1; + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + String number = ns.getString("number"); + if (ns.getBoolean("trust_all_known_keys")) { + boolean res = m.trustIdentityAllKeys(number); + if (!res) { + System.err.println("Failed to set the trust for this number, make sure the number is correct."); + return 1; + } + } else { + String fingerprint = ns.getString("verified_fingerprint"); + if (fingerprint != null) { + byte[] fingerprintBytes; + try { + fingerprintBytes = Hex.toByteArray(fingerprint.replaceAll(" ", "").toLowerCase(Locale.ROOT)); + } catch (Exception e) { + System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); + return 1; + } + boolean res = m.trustIdentityVerified(number, fingerprintBytes); + if (!res) { + System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); + return 1; + } + } else { + System.err.println("You need to specify the fingerprint you have verified with -v FINGERPRINT"); + return 1; + } + } break; case "daemon": if (dBusConn != null) { @@ -593,6 +654,21 @@ public class Main { .nargs("*") .help("Specify one or more members to add to the group"); + Subparser parserListIdentities = subparsers.addParser("listIdentities"); + parserListIdentities.addArgument("-n", "--number") + .help("Only show identity keys for the given phone number."); + + Subparser parserTrust = subparsers.addParser("trust"); + parserTrust.addArgument("number") + .help("Specify the phone number, for which to set the trust.") + .required(true); + MutuallyExclusiveGroup mutTrust = parserTrust.addMutuallyExclusiveGroup(); + mutTrust.addArgument("-a", "--trust-all-known-keys") + .help("Trust all known keys of this user, only use this for testing.") + .action(Arguments.storeTrue()); + mutTrust.addArgument("-v", "--verified-fingerprint") + .help("Specify the fingerprint of the key, only use this option if you have verified the fingerprint."); + Subparser parserReceive = subparsers.addParser("receive"); parserReceive.addArgument("-t", "--timeout") .type(int.class) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 3ce9dfbe..3e36e319 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1100,4 +1100,54 @@ class Manager implements Signal { public GroupInfo getGroup(byte[] groupId) { return groupStore.getGroup(groupId); } + + public Map> getIdentities() { + return signalProtocolStore.getIdentities(); + } + + public List getIdentities(String number) { + return signalProtocolStore.getIdentities(number); + } + + /** + * Trust this the identity with this fingerprint + * + * @param name username of the identity + * @param fingerprint Fingerprint + */ + public boolean trustIdentityVerified(String name, byte[] fingerprint) { + List ids = signalProtocolStore.getIdentities(name); + if (ids == null) { + return false; + } + for (JsonIdentityKeyStore.Identity id : ids) { + if (!Arrays.equals(id.identityKey.serialize(), fingerprint)) { + continue; + } + + signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED); + save(); + return true; + } + return false; + } + + /** + * Trust all keys of this identity without verification + * + * @param name username of the identity + */ + public boolean trustIdentityAllKeys(String name) { + List ids = signalProtocolStore.getIdentities(name); + if (ids == null) { + return false; + } + for (JsonIdentityKeyStore.Identity id : ids) { + if (id.trustLevel == TrustLevel.UNTRUSTED) { + signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_UNVERIFIED); + } + } + save(); + return true; + } } From d78551564b8ce87615cfb575aa2eaab61644c142 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 18 Jul 2016 13:32:11 +0200 Subject: [PATCH 0136/2005] Bump version --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 873ee41a..dd27f3fd 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.4.0' +version = '0.4.1' compileJava.options.encoding = 'UTF-8' @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages_provisioning' + compile 'org.whispersystems:signal-service-java:2.1.1b_fetchMessages_provisioning' compile 'org.bouncycastle:bcprov-jdk15on:1.54' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' From ee5062a2cc83078d1d1d33cba32bbaa89e96f52e Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 20 Jul 2016 23:11:52 +0200 Subject: [PATCH 0137/2005] Create config directory/files as only user readable Directories are created with mode 700, files with 600 Fixes #21 --- src/main/java/org/asamk/signal/Manager.java | 40 +++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 3e36e319..68f66455 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -60,12 +60,17 @@ import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static java.nio.file.attribute.PosixFilePermission.*; + class Manager implements Signal { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); @@ -124,10 +129,29 @@ class Manager implements Signal { } public String getFileName() { - new File(dataPath).mkdirs(); return dataPath + "/" + username; } + private static void createPrivateDirectories(String path) throws IOException { + final Path file = new File(path).toPath(); + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); + Files.createDirectories(file, PosixFilePermissions.asFileAttribute(perms)); + } catch (UnsupportedOperationException e) { + Files.createDirectories(file); + } + } + + private static void createPrivateFile(String path) throws IOException { + final Path file = new File(path).toPath(); + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); + Files.createFile(file, PosixFilePermissions.asFileAttribute(perms)); + } catch (UnsupportedOperationException e) { + Files.createFile(file); + } + } + public boolean userExists() { if (username == null) { return false; @@ -153,6 +177,10 @@ class Manager implements Signal { if (fileChannel != null) return; + createPrivateDirectories(dataPath); + if (!new File(getFileName()).exists()) { + createPrivateFile(getFileName()); + } fileChannel = new RandomAccessFile(new File(getFileName()), "rw").getChannel(); lock = fileChannel.tryLock(); if (lock == null) { @@ -202,7 +230,7 @@ class Manager implements Signal { File attachmentFile = getAttachmentFile(g.getAvatarId()); if (!avatarFile.exists() && attachmentFile.exists()) { try { - new File(avatarsPath).mkdirs(); + createPrivateDirectories(avatarsPath); Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { // Ignore @@ -569,7 +597,7 @@ class Manager implements Signal { File aFile = getGroupAvatarFile(g.groupId); if (avatarFile != null) { - new File(avatarsPath).mkdirs(); + createPrivateDirectories(avatarsPath); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } if (aFile.exists()) { @@ -908,7 +936,7 @@ class Manager implements Signal { } private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { - new File(avatarsPath).mkdirs(); + createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getContactAvatarFile(number), false); @@ -923,7 +951,7 @@ class Manager implements Signal { } private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { - new File(avatarsPath).mkdirs(); + createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); @@ -938,7 +966,7 @@ class Manager implements Signal { } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - new File(attachmentsPath).mkdirs(); + createPrivateDirectories(attachmentsPath); return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); } From 1efdf04394d8f5daf919e3ba739d5b32186093ce Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 12 Aug 2016 18:24:30 +0200 Subject: [PATCH 0138/2005] Prevent sending to groups that the user has quit Fixes #23 --- src/main/java/org/asamk/Signal.java | 1 - src/main/java/org/asamk/signal/Main.java | 16 +++++++++++ src/main/java/org/asamk/signal/Manager.java | 28 +++++++++++-------- .../signal/NotAGroupMemberException.java | 14 ++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/asamk/signal/NotAGroupMemberException.java diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 9a868109..02fc22dd 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -5,7 +5,6 @@ import org.asamk.signal.GroupNotFoundException; import org.freedesktop.dbus.DBusInterface; import org.freedesktop.dbus.DBusSignal; import org.freedesktop.dbus.exceptions.DBusException; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 25807a6b..4b95d58e 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -310,6 +310,9 @@ public class Main { } catch (GroupNotFoundException e) { handleGroupNotFoundException(e); return 1; + } catch (NotAGroupMemberException e) { + handleNotAGroupMemberException(e); + return 1; } catch (AttachmentInvalidException e) { System.err.println("Failed to add attachment: " + e.getMessage()); System.err.println("Aborting sending."); @@ -401,6 +404,9 @@ public class Main { } catch (GroupNotFoundException e) { handleGroupNotFoundException(e); return 1; + } catch (NotAGroupMemberException e) { + handleNotAGroupMemberException(e); + return 1; } break; @@ -433,6 +439,9 @@ public class Main { } catch (GroupNotFoundException e) { handleGroupNotFoundException(e); return 1; + } catch (NotAGroupMemberException e) { + handleNotAGroupMemberException(e); + return 1; } catch (EncapsulatedExceptions e) { handleEncapsulatedExceptions(e); return 3; @@ -553,6 +562,13 @@ public class Main { System.err.println("Aborting sending."); } + private static void handleNotAGroupMemberException(NotAGroupMemberException e) { + System.err.println("Failed to send to group: " + e.getMessage()); + System.err.println("Update the group on another device to readd the user to this group."); + System.err.println("Aborting sending."); + } + + private static void handleDBusExecutionException(DBusExecutionException e) { System.err.println("Cannot connect to dbus: " + e.getMessage()); System.err.println("Aborting."); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 68f66455..987a5cac 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -515,6 +515,19 @@ class Manager implements Signal { return Optional.of(createAttachment(file)); } + private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException { + GroupInfo g = groupStore.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } + for (String member : g.members) { + if (member.equals(this.username)) { + return g; + } + } + throw new NotAGroupMemberException(groupId, g.name); + } + @Override public void sendGroupMessage(String messageText, List attachments, byte[] groupId) @@ -531,10 +544,7 @@ class Manager implements Signal { } SignalServiceDataMessage message = messageBuilder.build(); - GroupInfo g = groupStore.getGroup(groupId); - if (g == null) { - throw new GroupNotFoundException(groupId); - } + final GroupInfo g = getGroupForSending(groupId); // Don't send group message to ourself final List membersSend = new ArrayList<>(g.members); @@ -551,10 +561,7 @@ class Manager implements Signal { .asGroupMessage(group) .build(); - final GroupInfo g = groupStore.getGroup(groupId); - if (g == null) { - throw new GroupNotFoundException(groupId); - } + final GroupInfo g = getGroupForSending(groupId); g.members.remove(this.username); groupStore.updateGroup(g); @@ -568,10 +575,7 @@ class Manager implements Signal { g = new GroupInfo(Util.getSecretBytes(16)); g.members.add(username); } else { - g = groupStore.getGroup(groupId); - if (g == null) { - throw new GroupNotFoundException(groupId); - } + g = getGroupForSending(groupId); } if (name != null) { diff --git a/src/main/java/org/asamk/signal/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/NotAGroupMemberException.java new file mode 100644 index 00000000..52ba4238 --- /dev/null +++ b/src/main/java/org/asamk/signal/NotAGroupMemberException.java @@ -0,0 +1,14 @@ +package org.asamk.signal; + +import org.freedesktop.dbus.exceptions.DBusExecutionException; + +public class NotAGroupMemberException extends DBusExecutionException { + + public NotAGroupMemberException(String message) { + super(message); + } + + public NotAGroupMemberException(byte[] groupId, String groupName) { + super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")"); + } +} From 6a9f791f0d91bfd8edd224f31e69be780b196648 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 13 Aug 2016 13:42:56 +0200 Subject: [PATCH 0139/2005] Check if number is registered on Signal before adding to group Fixes #15 --- src/main/java/org/asamk/signal/Manager.java | 31 ++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 987a5cac..117c1ba3 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -44,6 +44,7 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; import org.whispersystems.signalservice.api.messages.multidevice.*; +import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.exceptions.*; @@ -568,6 +569,18 @@ class Manager implements Signal { sendMessage(message, g.members); } + private static String join(CharSequence separator, Iterable list) { + StringBuilder buf = new StringBuilder(); + for (CharSequence str : list) { + if (buf.length() > 0) { + buf.append(separator); + } + buf.append(str); + } + + return buf.toString(); + } + public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { GroupInfo g; if (groupId == null) { @@ -583,14 +596,30 @@ class Manager implements Signal { } if (members != null) { + Set newMembers = new HashSet<>(); for (String member : members) { try { - g.members.add(canonicalizeNumber(member)); + member = canonicalizeNumber(member); } catch (InvalidNumberException e) { System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage()); System.err.println("Aborting…"); System.exit(1); } + if (g.members.contains(member)) { + continue; + } + newMembers.add(member); + g.members.add(member); + } + final List contacts = accountManager.getContacts(newMembers); + if (contacts.size() != newMembers.size()) { + // Some of the new members are not registered on Signal + for (ContactTokenDetails contact : contacts) { + newMembers.remove(contact.getNumber()); + } + System.err.println("Failed to add members " + join(", ", newMembers) + " to group: Not registered on Signal"); + System.err.println("Aborting…"); + System.exit(1); } } From 5ee375c74d13fa18ed353221c403772511abcbb1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 20 Aug 2016 16:01:31 +0200 Subject: [PATCH 0140/2005] Store encrypted messages on disk when receiving them - Acknowledge to the server only after the message is stored. - Delete the message when decrypting was successful --- src/main/java/org/asamk/signal/Main.java | 16 +- src/main/java/org/asamk/signal/Manager.java | 239 ++++++++++++-------- 2 files changed, 163 insertions(+), 92 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 4b95d58e..40e28d25 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -768,7 +768,7 @@ public class Main { } @Override - public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content) { + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { SignalServiceAddress source = envelope.getSourceAddress(); ContactInfo sourceContact = m.getContact(source.getNumber()); System.out.println(String.format("Envelope from: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + source.getNumber(), envelope.getSourceDevice())); @@ -780,6 +780,16 @@ public class Main { if (envelope.isReceipt()) { System.out.println("Got receipt."); } else if (envelope.isSignalMessage() | envelope.isPreKeySignalMessage()) { + if (exception != null) { + if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) { + org.whispersystems.libsignal.UntrustedIdentityException e = (org.whispersystems.libsignal.UntrustedIdentityException) exception; + System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message."); + System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getName() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getName() + "' to mark it as trusted"); + System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getName() + "' to trust it without verification"); + } else { + System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")"); + } + } if (content == null) { System.out.println("Failed to decrypt message."); } else { @@ -904,8 +914,8 @@ public class Main { } @Override - public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content) { - super.handleMessage(envelope, content); + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { + super.handleMessage(envelope, content, exception); if (!envelope.isReceipt() && content != null && content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 117c1ba3..315eac13 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -133,6 +133,20 @@ class Manager implements Signal { return dataPath + "/" + username; } + private String getMessageCachePath() { + return this.dataPath + "/" + username + ".d/msg-cache"; + } + + private String getMessageCachePath(String sender) { + return getMessageCachePath() + "/" + sender.replace("/", "_"); + } + + private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException { + String cachePath = getMessageCachePath(sender); + createPrivateDirectories(cachePath); + return new File(cachePath + "/" + now + "_" + timestamp); + } + private static void createPrivateDirectories(String path) throws IOException { final Path file = new File(path).toPath(); try { @@ -778,11 +792,8 @@ class Manager implements Signal { try { return cipher.decrypt(envelope); } catch (org.whispersystems.libsignal.UntrustedIdentityException e) { - // TODO temporarily store message, until user has accepted the key signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); throw e; - } catch (Exception e) { - throw e; } } @@ -791,7 +802,7 @@ class Manager implements Signal { } public interface ReceiveMessageHandler { - void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent); + void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e); } private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { @@ -863,99 +874,47 @@ class Manager implements Signal { while (true) { SignalServiceEnvelope envelope; SignalServiceContent content = null; + Exception exception = null; + final long now = new Date().getTime(); try { - envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS); - if (!envelope.isReceipt()) { - Exception exception; - try { - content = decryptMessage(envelope); - } catch (Exception e) { - exception = e; - // TODO pass exception to handler instead - e.printStackTrace(); - } - if (content != null) { - if (content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); - handleSignalServiceDataMessage(message, false, envelope.getSource(), username); - } - if (content.getSyncMessage().isPresent()) { - SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); - if (syncMessage.getSent().isPresent()) { - SignalServiceDataMessage message = syncMessage.getSent().get().getMessage(); - handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); - } - if (syncMessage.getRequest().isPresent()) { - RequestMessage rm = syncMessage.getRequest().get(); - if (rm.isContactsRequest()) { - try { - sendContacts(); - } catch (UntrustedIdentityException e) { - e.printStackTrace(); - } - } - if (rm.isGroupsRequest()) { - try { - sendGroups(); - } catch (UntrustedIdentityException e) { - e.printStackTrace(); - } - } - } - if (syncMessage.getGroups().isPresent()) { - try { - DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer())); - DeviceGroup g; - while ((g = s.read()) != null) { - GroupInfo syncGroup = groupStore.getGroup(g.getId()); - if (syncGroup == null) { - syncGroup = new GroupInfo(g.getId()); - } - if (g.getName().isPresent()) { - syncGroup.name = g.getName().get(); - } - syncGroup.members.addAll(g.getMembers()); - syncGroup.active = g.isActive(); - - if (g.getAvatar().isPresent()) { - retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); - } - groupStore.updateGroup(syncGroup); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - if (syncMessage.getContacts().isPresent()) { - try { - DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); - DeviceContact c; - while ((c = s.read()) != null) { - ContactInfo contact = new ContactInfo(); - contact.number = c.getNumber(); - if (c.getName().isPresent()) { - contact.name = c.getName().get(); - } - contactStore.updateContact(contact); - - if (c.getAvatar().isPresent()) { - retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } + envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS, new SignalServiceMessagePipe.MessagePipeCallback() { + @Override + public void onMessage(SignalServiceEnvelope envelope) { + // store message on disk, before acknowledging receipt to the server + try { + File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp()); + storeEnvelope(envelope, cacheFile); + } catch (IOException e) { + System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage()); } } - } - save(); - handler.handleMessage(envelope, content); + }); } catch (TimeoutException e) { if (returnOnTimeout) return; + continue; } catch (InvalidVersionException e) { System.err.println("Ignoring error: " + e.getMessage()); + continue; + } + if (!envelope.isReceipt()) { + try { + content = decryptMessage(envelope); + } catch (Exception e) { + exception = e; + } + handleMessage(envelope, content); + } + save(); + handler.handleMessage(envelope, content, exception); + if (exception == null || !(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { + try { + File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp()); + cacheFile.delete(); + } catch (IOException e) { + // Ignoring + return; + } } } } finally { @@ -964,6 +923,108 @@ class Manager implements Signal { } } + private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content) { + if (content != null) { + if (content.getDataMessage().isPresent()) { + SignalServiceDataMessage message = content.getDataMessage().get(); + handleSignalServiceDataMessage(message, false, envelope.getSource(), username); + } + if (content.getSyncMessage().isPresent()) { + SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); + if (syncMessage.getSent().isPresent()) { + SignalServiceDataMessage message = syncMessage.getSent().get().getMessage(); + handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); + } + if (syncMessage.getRequest().isPresent()) { + RequestMessage rm = syncMessage.getRequest().get(); + if (rm.isContactsRequest()) { + try { + sendContacts(); + } catch (UntrustedIdentityException | IOException e) { + e.printStackTrace(); + } + } + if (rm.isGroupsRequest()) { + try { + sendGroups(); + } catch (UntrustedIdentityException | IOException e) { + e.printStackTrace(); + } + } + } + if (syncMessage.getGroups().isPresent()) { + try { + DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer())); + DeviceGroup g; + while ((g = s.read()) != null) { + GroupInfo syncGroup = groupStore.getGroup(g.getId()); + if (syncGroup == null) { + syncGroup = new GroupInfo(g.getId()); + } + if (g.getName().isPresent()) { + syncGroup.name = g.getName().get(); + } + syncGroup.members.addAll(g.getMembers()); + syncGroup.active = g.isActive(); + + if (g.getAvatar().isPresent()) { + retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); + } + groupStore.updateGroup(syncGroup); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + if (syncMessage.getContacts().isPresent()) { + try { + DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); + DeviceContact c; + while ((c = s.read()) != null) { + ContactInfo contact = new ContactInfo(); + contact.number = c.getNumber(); + if (c.getName().isPresent()) { + contact.name = c.getName().get(); + } + contactStore.updateContact(contact); + + if (c.getAvatar().isPresent()) { + retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + } + + private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { + try (FileOutputStream f = new FileOutputStream(file)) { + DataOutputStream out = new DataOutputStream(f); + out.writeInt(1); // version + out.writeInt(envelope.getType()); + out.writeUTF(envelope.getSource()); + out.writeInt(envelope.getSourceDevice()); + out.writeUTF(envelope.getRelay()); + out.writeLong(envelope.getTimestamp()); + if (envelope.hasContent()) { + out.writeInt(envelope.getContent().length); + out.write(envelope.getContent()); + } else { + out.writeInt(0); + } + if (envelope.hasLegacyMessage()) { + out.writeInt(envelope.getLegacyMessage().length); + out.write(envelope.getLegacyMessage()); + } else { + out.writeInt(0); + } + out.close(); + } + } + public File getContactAvatarFile(String number) { return new File(avatarsPath, "contact-" + number); } From b2289568efb9f9c0fed3fac019ae082a05cd39f5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 20 Aug 2016 16:04:00 +0200 Subject: [PATCH 0141/2005] Retry decrypting of messages from previously untrusted keys Decrypts messages from untrusted keys, if they are trusted now --- src/main/java/org/asamk/signal/Manager.java | 70 +++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 315eac13..6bd8c657 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -864,7 +864,49 @@ class Manager implements Signal { } } + public void retryFailedReceivedMessages(ReceiveMessageHandler handler) { + final File cachePath = new File(getMessageCachePath()); + if (!cachePath.exists()) { + return; + } + for (final File dir : cachePath.listFiles()) { + if (!dir.isDirectory()) { + continue; + } + + String sender = dir.getName(); + for (final File fileEntry : dir.listFiles()) { + if (!fileEntry.isFile()) { + continue; + } + SignalServiceEnvelope envelope; + try { + envelope = loadEnvelope(fileEntry); + if (envelope == null) { + continue; + } + } catch (IOException e) { + e.printStackTrace(); + continue; + } + SignalServiceContent content = null; + if (!envelope.isReceipt()) { + try { + content = decryptMessage(envelope); + } catch (Exception e) { + continue; + } + handleMessage(envelope, content); + } + save(); + handler.handleMessage(envelope, content, null); + fileEntry.delete(); + } + } + } + public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { + retryFailedReceivedMessages(handler); final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); SignalServiceMessagePipe messagePipe = null; @@ -1000,6 +1042,34 @@ class Manager implements Signal { } } + private SignalServiceEnvelope loadEnvelope(File file) throws IOException { + try (FileInputStream f = new FileInputStream(file)) { + DataInputStream in = new DataInputStream(f); + int version = in.readInt(); + if (version != 1) { + return null; + } + int type = in.readInt(); + String source = in.readUTF(); + int sourceDevice = in.readInt(); + String relay = in.readUTF(); + long timestamp = in.readLong(); + byte[] content = null; + int contentLen = in.readInt(); + if (contentLen > 0) { + content = new byte[contentLen]; + in.readFully(content); + } + byte[] legacyMessage = null; + int legacyMessageLen = in.readInt(); + if (legacyMessageLen > 0) { + legacyMessage = new byte[legacyMessageLen]; + in.readFully(legacyMessage); + } + return new SignalServiceEnvelope(type, source, sourceDevice, relay, timestamp, legacyMessage, content); + } + } + private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { try (FileOutputStream f = new FileOutputStream(file)) { DataOutputStream out = new DataOutputStream(f); From a724251f8d9a629ae7e59a6eb7518c6059911946 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 20 Aug 2016 16:04:43 +0200 Subject: [PATCH 0142/2005] Bugfix: don't decrease trustLevel when receiving messages --- src/main/java/org/asamk/signal/JsonIdentityKeyStore.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java index 7cde350c..14c0d11e 100644 --- a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java @@ -58,7 +58,9 @@ class JsonIdentityKeyStore implements IdentityKeyStore { if (!id.identityKey.equals(identityKey)) continue; - id.trustLevel = trustLevel; + if (id.trustLevel.compareTo(trustLevel) < 0) { + id.trustLevel = trustLevel; + } if (added != null) { id.added = added; } From e89e656b45d64681415c5c0723e4743184496fd8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 20 Aug 2016 16:26:14 +0200 Subject: [PATCH 0143/2005] Update README.md Fixes #17 --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6da7b477..61fa8870 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ ## Usage -usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-system] {link,addDevice,listDevices,removeDevice,register,verify,send,quitGroup,updateGroup,receive,daemon} ... +usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-system] {link,addDevice,listDevices,removeDevice,register,verify,send,quitGroup,updateGroup,listIdentities,trust,receive,daemon} ... * Register a number (with SMS verification) @@ -86,6 +86,24 @@ usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-sys signal-cli -u USERNAME removeDevice -d DEVICE_ID +* Manage trusted keys + + * View all known keys + + signal-cli -u USERNAME listIdentities + + * View known keys of one number + + signal-cli -u USERNAME listIdentities -n NUMBER + + * Trust new key, after having verified it + + signal-cli -u USERNAME trust -v FINGER_PRINT NUMBER + + * Trust new key, without having verified it. Only use this if you don't care about security + + signal-cli -u USERNAME trust -a NUMBER + ## DBus service signal-cli can run in daemon mode and provides an experimental dbus interface. From 6597d48eccdaf4945924086447100a214a2c40f2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 27 Aug 2016 13:19:21 +0200 Subject: [PATCH 0144/2005] Update dependencies --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index dd27f3fd..d2950257 100644 --- a/build.gradle +++ b/build.gradle @@ -18,8 +18,8 @@ repositories { } dependencies { - compile 'org.whispersystems:signal-service-java:2.1.1b_fetchMessages_provisioning' - compile 'org.bouncycastle:bcprov-jdk15on:1.54' + compile 'org.whispersystems:signal-service-java:2.3.0_fetchMessages_provisioning' + compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' } From e4618456a1551bfa06914a23d545f8540963db79 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 27 Aug 2016 13:22:11 +0200 Subject: [PATCH 0145/2005] Add support for contact color sync and receiving blocklists and expiring messages --- .../java/org/asamk/signal/ContactInfo.java | 3 +++ src/main/java/org/asamk/signal/Main.java | 22 +++++++++++++++---- src/main/java/org/asamk/signal/Manager.java | 8 ++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/asamk/signal/ContactInfo.java b/src/main/java/org/asamk/signal/ContactInfo.java index 89802050..c607238f 100644 --- a/src/main/java/org/asamk/signal/ContactInfo.java +++ b/src/main/java/org/asamk/signal/ContactInfo.java @@ -8,4 +8,7 @@ public class ContactInfo { @JsonProperty public String number; + + @JsonProperty + public String color; } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 40e28d25..8a1dd860 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -28,10 +28,7 @@ import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; -import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.multidevice.*; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; @@ -837,9 +834,20 @@ public class Main { to = "Unknown"; } System.out.println("To: " + to + " , Message timestamp: " + sentTranscriptMessage.getTimestamp()); + if (sentTranscriptMessage.getExpirationStartTimestamp() > 0) { + System.out.println("Expiration started at: " + sentTranscriptMessage.getExpirationStartTimestamp()); + } SignalServiceDataMessage message = sentTranscriptMessage.getMessage(); handleSignalServiceDataMessage(message); } + if (syncMessage.getBlockedList().isPresent()) { + System.out.println("Received sync message with block list"); + System.out.println("Blocked numbers:"); + final BlockedListMessage blockedList = syncMessage.getBlockedList().get(); + for (String number : blockedList.getNumbers()) { + System.out.println(" - " + number); + } + } } } } else { @@ -882,6 +890,12 @@ public class Main { if (message.isEndSession()) { System.out.println("Is end session"); } + if (message.isExpirationUpdate()) { + System.out.println("Is Expiration update: " + message.isExpirationUpdate()); + } + if (message.getExpiresInSeconds() > 0) { + System.out.println("Expires in: " + message.getExpiresInSeconds() + " seconds"); + } if (message.getAttachments().isPresent()) { System.out.println("Attachments: "); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 6bd8c657..6ed8b045 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1017,6 +1017,9 @@ class Manager implements Signal { } catch (Exception e) { e.printStackTrace(); } + if (syncMessage.getBlockedList().isPresent()) { + // TODO store list of blocked numbers + } } if (syncMessage.getContacts().isPresent()) { try { @@ -1028,6 +1031,9 @@ class Manager implements Signal { if (c.getName().isPresent()) { contact.name = c.getName().get(); } + if (c.getColor().isPresent()) { + contact.color = c.getColor().get(); + } contactStore.updateContact(contact); if (c.getAvatar().isPresent()) { @@ -1264,7 +1270,7 @@ class Manager implements Signal { try { for (ContactInfo record : contactStore.getContacts()) { out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), - createContactAvatarAttachment(record.number))); + createContactAvatarAttachment(record.number), Optional.fromNullable(record.color))); } } finally { out.close(); From 293c176831d9acc4b44c30e2fa13119152d08d3a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 27 Aug 2016 13:41:28 +0200 Subject: [PATCH 0146/2005] Format timestamps as ISO 8601 in UTC --- src/main/java/org/asamk/signal/Main.java | 30 +++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 8a1dd860..d70b00cb 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -43,10 +43,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.security.Security; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; import java.util.concurrent.TimeoutException; public class Main { @@ -54,6 +53,8 @@ public class Main { public static final String SIGNAL_BUSNAME = "org.asamk.Signal"; public static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal"; + private static final TimeZone tzUTC = TimeZone.getTimeZone("UTC"); + public static void main(String[] args) { // Workaround for BKS truststore Security.insertProviderAt(new org.bouncycastle.jce.provider.BouncyCastleProvider(), 1); @@ -327,8 +328,8 @@ public class Main { dBusConn.addSigHandler(Signal.MessageReceived.class, new DBusSigHandler() { @Override public void handle(Signal.MessageReceived s) { - System.out.print(String.format("Envelope from: %s\nTimestamp: %d\nBody: %s\n", - s.getSender(), s.getTimestamp(), s.getMessage())); + System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n", + s.getSender(), formatTimestamp(s.getTimestamp()), s.getMessage())); if (s.getGroupId().length > 0) { System.out.println("Group info:"); System.out.println(" Id: " + Base64.encodeBytes(s.getGroupId())); @@ -772,7 +773,7 @@ public class Main { if (source.getRelay().isPresent()) { System.out.println("Relayed by: " + source.getRelay().get()); } - System.out.println("Timestamp: " + envelope.getTimestamp()); + System.out.println("Timestamp: " + formatTimestamp(envelope.getTimestamp())); if (envelope.isReceipt()) { System.out.println("Got receipt."); @@ -810,7 +811,7 @@ public class Main { System.out.println("Received sync read messages list"); for (ReadMessage rm : syncMessage.getRead().get()) { ContactInfo fromContact = m.getContact(rm.getSender()); - System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender() + " Message timestamp: " + rm.getTimestamp()); + System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender() + " Message timestamp: " + formatTimestamp(rm.getTimestamp())); } } if (syncMessage.getRequest().isPresent()) { @@ -833,9 +834,9 @@ public class Main { } else { to = "Unknown"; } - System.out.println("To: " + to + " , Message timestamp: " + sentTranscriptMessage.getTimestamp()); + System.out.println("To: " + to + " , Message timestamp: " + formatTimestamp(sentTranscriptMessage.getTimestamp())); if (sentTranscriptMessage.getExpirationStartTimestamp() > 0) { - System.out.println("Expiration started at: " + sentTranscriptMessage.getExpirationStartTimestamp()); + System.out.println("Expiration started at: " + formatTimestamp(sentTranscriptMessage.getExpirationStartTimestamp())); } SignalServiceDataMessage message = sentTranscriptMessage.getMessage(); handleSignalServiceDataMessage(message); @@ -857,7 +858,7 @@ public class Main { } private void handleSignalServiceDataMessage(SignalServiceDataMessage message) { - System.out.println("Message timestamp: " + message.getTimestamp()); + System.out.println("Message timestamp: " + formatTimestamp(message.getTimestamp())); if (message.getBody().isPresent()) { System.out.println("Body: " + message.getBody().get()); @@ -962,4 +963,11 @@ public class Main { } } + + private static String formatTimestamp(long timestamp) { + Date date = new Date(timestamp); + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); // Quoted "Z" to indicate UTC, no timezone offset + df.setTimeZone(tzUTC); + return timestamp + " (" + df.format(date) + ")"; + } } From adbc08b2f76c80c8613ade367159af9f40f03099 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 29 Aug 2016 10:53:47 +0200 Subject: [PATCH 0147/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d2950257..7a583b44 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.4.1' +version = '0.5.0' compileJava.options.encoding = 'UTF-8' From fccb28fdf4b363a0e3f134740926c814d12b17e3 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 13 Sep 2016 21:18:05 +0200 Subject: [PATCH 0148/2005] Update dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7a583b44..d747ef73 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'org.whispersystems:signal-service-java:2.3.0_fetchMessages_provisioning' + compile 'org.whispersystems:signal-service-java:2.3.1_fetchMessages_provisioning' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' From 7ceddf24dfb3c1661c9e8b695a80f7ce97f57c53 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 13 Sep 2016 21:18:23 +0200 Subject: [PATCH 0149/2005] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 61fa8870..77c4c8c2 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ It is primarily intended to be used on servers to notify admins of important eve You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/). You need to have at least JRE 7 installed, to run signal-cli. ### Install system-wide on Linux +See [latest version](https://github.com/AsamK/signal-cli/releases). ```sh -export VERSION= +export VERSION= wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal-cli-"${VERSION}".tar.gz sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ From 2e19cd09f49147adcdff3b494ade805e4937aa04 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 27 Oct 2016 10:46:22 +0200 Subject: [PATCH 0150/2005] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 53319 -> 52928 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 5 +++++ gradlew.bat | 6 ------ 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d3b83982b9b1bccad955349d702be9b884c6e049..6ffa237849ef3607e39c3b334a92a65367962071 100644 GIT binary patch delta 10333 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

^E)wG5GY4Y}eYb379qrHV3RUboZP$6IRibjJ* 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

-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 delta 10713 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�jSA52!a>JJ=1FUj zQ4g)bjy%qDNKrk36*d4c09}7iA02^MD3J;if0{&2=$~58tJ&6 z7;ZN|w3^xU- Date: Thu, 27 Oct 2016 10:46:30 +0200 Subject: [PATCH 0151/2005] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77c4c8c2..617742c2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # signal-cli -signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. To be able to receiving messages signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libsignal-service-java/pull/5) nor [provisioning as a slave device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For registering you need a phone number where you can receive SMS or incoming calls. +signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. To be able to receive messages signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libsignal-service-java/pull/5) nor [provisioning as a slave device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For registering you need a phone number where you can receive SMS or incoming calls. It is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings. ## Installation From f97b0c0faad04f9475d4248a1f09c775d1c3df3f Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 27 Oct 2016 14:09:22 +0200 Subject: [PATCH 0152/2005] Add support for new safety numbers, that replace the hex fingerprint --- src/main/java/org/asamk/signal/Main.java | 52 ++++++++++++++++----- src/main/java/org/asamk/signal/Manager.java | 34 ++++++++++++++ 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index d70b00cb..fbdc0ce1 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -458,13 +458,13 @@ public class Main { if (ns.get("number") == null) { for (Map.Entry> keys : m.getIdentities().entrySet()) { for (JsonIdentityKeyStore.Identity id : keys.getValue()) { - System.out.println(String.format("%s: %s Added: %s Fingerprint: %s", keys.getKey(), id.trustLevel, id.added, Hex.toStringCondensed(id.getFingerprint()))); + printIdentityFingerprint(m, keys.getKey(), id); } } } else { String number = ns.getString("number"); for (JsonIdentityKeyStore.Identity id : m.getIdentities(number)) { - System.out.println(String.format("%s: %s Added: %s Fingerprint: %s", number, id.trustLevel, id.added, Hex.toStringCondensed(id.getFingerprint()))); + printIdentityFingerprint(m, number, id); } } break; @@ -487,16 +487,28 @@ public class Main { } else { String fingerprint = ns.getString("verified_fingerprint"); if (fingerprint != null) { - byte[] fingerprintBytes; - try { - fingerprintBytes = Hex.toByteArray(fingerprint.replaceAll(" ", "").toLowerCase(Locale.ROOT)); - } catch (Exception e) { - System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); - return 1; - } - boolean res = m.trustIdentityVerified(number, fingerprintBytes); - if (!res) { - System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); + fingerprint = fingerprint.replaceAll(" ", ""); + if (fingerprint.length() == 66) { + byte[] fingerprintBytes; + try { + fingerprintBytes = Hex.toByteArray(fingerprint.toLowerCase(Locale.ROOT)); + } catch (Exception e) { + System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); + return 1; + } + boolean res = m.trustIdentityVerified(number, fingerprintBytes); + if (!res) { + System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); + return 1; + } + } else if (fingerprint.length() == 60) { + boolean res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint); + if (!res) { + System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); + return 1; + } + } else { + System.err.println("Fingerprint has invalid format, either specify the old hex fingerprint or the new safety number"); return 1; } } else { @@ -555,6 +567,22 @@ public class Main { } } + private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) { + String digits = formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.identityKey)); + System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, + theirId.trustLevel, theirId.added, Hex.toStringCondensed(theirId.getFingerprint()), digits)); + } + + private static String formatSafetyNumber(String digits) { + final int partCount = 12; + int partSize = digits.length() / partCount; + StringBuilder f = new StringBuilder(digits.length() + partCount); + for (int i = 0; i < partCount; i++) { + f.append(digits.substring(i * partSize, (i * partSize) + partSize)).append(" "); + } + return f.toString(); + } + private static void handleGroupNotFoundException(GroupNotFoundException e) { System.err.println("Failed to send to group: " + e.getMessage()); System.err.println("Aborting sending."); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 6ed8b045..e02258f1 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -31,6 +31,8 @@ import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; +import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.KeyHelper; @@ -125,6 +127,10 @@ class Manager implements Signal { return username; } + private IdentityKey getIdentity() { + return signalProtocolStore.getIdentityKeyPair().getPublicKey(); + } + public int getDeviceId() { return deviceId; } @@ -1330,6 +1336,29 @@ class Manager implements Signal { return false; } + /** + * Trust this the identity with this safety number + * + * @param name username of the identity + * @param safetyNumber Safety number + */ + public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) { + List ids = signalProtocolStore.getIdentities(name); + if (ids == null) { + return false; + } + for (JsonIdentityKeyStore.Identity id : ids) { + if (!safetyNumber.equals(computeSafetyNumber(name, id.identityKey))) { + continue; + } + + signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED); + save(); + return true; + } + return false; + } + /** * Trust all keys of this identity without verification * @@ -1348,4 +1377,9 @@ class Manager implements Signal { save(); return true; } + + public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) { + Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey); + return fingerprint.getDisplayableFingerprint().getDisplayText(); + } } From 6c9f26f49b46d39421e822cf0be6b299f161c5bf Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 27 Oct 2016 16:03:20 +0200 Subject: [PATCH 0153/2005] Split load function --- src/main/java/org/asamk/signal/Main.java | 2 +- src/main/java/org/asamk/signal/Manager.java | 53 ++++++++++++--------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index fbdc0ce1..5dd471f9 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -110,7 +110,7 @@ public class Main { ts = m; if (m.userExists()) { try { - m.load(); + m.init(); } catch (Exception e) { System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage()); return 2; diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index e02258f1..34bb5068 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -211,7 +211,23 @@ class Manager implements Signal { } } - public void load() throws IOException, InvalidKeyException { + public void init() throws IOException { + load(); + + migrateLegacyConfigs(); + + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); + try { + if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + save(); + } + } catch (AuthorizationFailedException e) { + System.err.println("Authorization failed, was the number registered elsewhere?"); + } + } + + private void load() throws IOException { openFileChannel(); JsonNode rootNode = jsonProcessot.readTree(Channels.newInputStream(fileChannel)); @@ -243,10 +259,21 @@ class Manager implements Signal { if (groupStore == null) { groupStore = new JsonGroupStore(); } + + JsonNode contactStoreNode = rootNode.get("contactStore"); + if (contactStoreNode != null) { + contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class); + } + if (contactStore == null) { + contactStore = new JsonContactsStore(); + } + } + + private void migrateLegacyConfigs() { // Copy group avatars that were previously stored in the attachments folder // to the new avatar folder - if (groupStore.groupsWithLegacyAvatarId.size() > 0) { - for (GroupInfo g : groupStore.groupsWithLegacyAvatarId) { + if (JsonGroupStore.groupsWithLegacyAvatarId.size() > 0) { + for (GroupInfo g : JsonGroupStore.groupsWithLegacyAvatarId) { File avatarFile = getGroupAvatarFile(g.groupId); File attachmentFile = getAttachmentFile(g.getAvatarId()); if (!avatarFile.exists() && attachmentFile.exists()) { @@ -258,27 +285,9 @@ class Manager implements Signal { } } } - groupStore.groupsWithLegacyAvatarId.clear(); + JsonGroupStore.groupsWithLegacyAvatarId.clear(); save(); } - - JsonNode contactStoreNode = rootNode.get("contactStore"); - if (contactStoreNode != null) { - contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class); - } - if (contactStore == null) { - contactStore = new JsonContactsStore(); - } - - accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); - try { - if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { - refreshPreKeys(); - save(); - } - } catch (AuthorizationFailedException e) { - System.err.println("Authorization failed, was the number registered elsewhere?"); - } } private void save() { From 93e2c58fcfd1777c80193bbd7ea017fd78371b6c Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 31 Oct 2016 17:51:05 +0100 Subject: [PATCH 0154/2005] Fix typo --- .../org/asamk/signal/JsonContactsStore.java | 4 +-- .../java/org/asamk/signal/JsonGroupStore.java | 4 +-- src/main/java/org/asamk/signal/Manager.java | 26 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonContactsStore.java b/src/main/java/org/asamk/signal/JsonContactsStore.java index e2807d8f..e288eca6 100644 --- a/src/main/java/org/asamk/signal/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/JsonContactsStore.java @@ -19,7 +19,7 @@ public class JsonContactsStore { @JsonDeserialize(using = ContactsDeserializer.class) private Map contacts = new HashMap<>(); - private static final ObjectMapper jsonProcessot = new ObjectMapper(); + private static final ObjectMapper jsonProcessor = new ObjectMapper(); void updateContact(ContactInfo contact) { contacts.put(contact.number, contact); @@ -47,7 +47,7 @@ public class JsonContactsStore { Map contacts = new HashMap<>(); JsonNode node = jsonParser.getCodec().readTree(jsonParser); for (JsonNode n : node) { - ContactInfo c = jsonProcessot.treeToValue(n, ContactInfo.class); + ContactInfo c = jsonProcessor.treeToValue(n, ContactInfo.class); contacts.put(c.number, c); } diff --git a/src/main/java/org/asamk/signal/JsonGroupStore.java b/src/main/java/org/asamk/signal/JsonGroupStore.java index da512706..aa7e3a45 100644 --- a/src/main/java/org/asamk/signal/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/JsonGroupStore.java @@ -21,7 +21,7 @@ public class JsonGroupStore { public static List groupsWithLegacyAvatarId = new ArrayList<>(); - private static final ObjectMapper jsonProcessot = new ObjectMapper(); + private static final ObjectMapper jsonProcessor = new ObjectMapper(); void updateGroup(GroupInfo group) { groups.put(Base64.encodeBytes(group.groupId), group); @@ -49,7 +49,7 @@ public class JsonGroupStore { Map groups = new HashMap<>(); JsonNode node = jsonParser.getCodec().readTree(jsonParser); for (JsonNode n : node) { - GroupInfo g = jsonProcessot.treeToValue(n, GroupInfo.class); + GroupInfo g = jsonProcessor.treeToValue(n, GroupInfo.class); // Check if a legacy avatarId exists if (g.getAvatarId() != 0) { groupsWithLegacyAvatarId.add(g); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 34bb5068..6c73d2c4 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -93,7 +93,7 @@ class Manager implements Signal { private FileChannel fileChannel; private FileLock lock; - private final ObjectMapper jsonProcessot = new ObjectMapper(); + private final ObjectMapper jsonProcessor = new ObjectMapper(); private String username; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private String password; @@ -115,12 +115,12 @@ class Manager implements Signal { this.attachmentsPath = this.settingsPath + "/attachments"; this.avatarsPath = this.settingsPath + "/avatars"; - jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect - jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. - jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); - jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - jsonProcessot.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); - jsonProcessot.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect + jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. + jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); + jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); + jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } public String getUsername() { @@ -229,7 +229,7 @@ class Manager implements Signal { private void load() throws IOException { openFileChannel(); - JsonNode rootNode = jsonProcessot.readTree(Channels.newInputStream(fileChannel)); + JsonNode rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel)); JsonNode node = rootNode.get("deviceId"); if (node != null) { @@ -250,11 +250,11 @@ class Manager implements Signal { } else { nextSignedPreKeyId = 0; } - signalProtocolStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); + signalProtocolStore = jsonProcessor.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); registered = getNotNullNode(rootNode, "registered").asBoolean(); JsonNode groupStoreNode = rootNode.get("groupStore"); if (groupStoreNode != null) { - groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class); + groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class); } if (groupStore == null) { groupStore = new JsonGroupStore(); @@ -262,7 +262,7 @@ class Manager implements Signal { JsonNode contactStoreNode = rootNode.get("contactStore"); if (contactStoreNode != null) { - contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class); + contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class); } if (contactStore == null) { contactStore = new JsonContactsStore(); @@ -294,7 +294,7 @@ class Manager implements Signal { if (username == null) { return; } - ObjectNode rootNode = jsonProcessot.createObjectNode(); + ObjectNode rootNode = jsonProcessor.createObjectNode(); rootNode.put("username", username) .put("deviceId", deviceId) .put("password", password) @@ -309,7 +309,7 @@ class Manager implements Signal { try { openFileChannel(); fileChannel.position(0); - jsonProcessot.writeValue(Channels.newOutputStream(fileChannel), rootNode); + jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode); fileChannel.truncate(fileChannel.position()); fileChannel.force(false); } catch (Exception e) { From a4e22539a3d87262f43d399fbd79823c4dc2fde0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 31 Oct 2016 20:52:32 +0100 Subject: [PATCH 0155/2005] Cleanup --- src/main/java/org/asamk/signal/JsonContactsStore.java | 3 +-- src/main/java/org/asamk/signal/Manager.java | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonContactsStore.java b/src/main/java/org/asamk/signal/JsonContactsStore.java index e288eca6..500684fe 100644 --- a/src/main/java/org/asamk/signal/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/JsonContactsStore.java @@ -26,8 +26,7 @@ public class JsonContactsStore { } ContactInfo getContact(String number) { - ContactInfo c = contacts.get(number); - return c; + return contacts.get(number); } List getContacts() { diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 6c73d2c4..42f5ba85 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1041,8 +1041,11 @@ class Manager implements Signal { DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); DeviceContact c; while ((c = s.read()) != null) { - ContactInfo contact = new ContactInfo(); - contact.number = c.getNumber(); + ContactInfo contact = contactStore.getContact(c.getNumber()); + if (contact == null) { + contact = new ContactInfo(); + contact.number = c.getNumber(); + } if (c.getName().isPresent()) { contact.name = c.getName().get(); } From 82cecfff85ef29e627d4238e3237222b492d5248 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 31 Oct 2016 20:52:06 +0100 Subject: [PATCH 0156/2005] Implement support for sending disappearing messages Stores the expiration timeout received from contacts in the config file Fixes #27 --- .../org/asamk/signal/JsonThreadStore.java | 56 +++++++++ src/main/java/org/asamk/signal/Manager.java | 111 ++++++++++++------ .../java/org/asamk/signal/ThreadInfo.java | 11 ++ 3 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 src/main/java/org/asamk/signal/JsonThreadStore.java create mode 100644 src/main/java/org/asamk/signal/ThreadInfo.java diff --git a/src/main/java/org/asamk/signal/JsonThreadStore.java b/src/main/java/org/asamk/signal/JsonThreadStore.java new file mode 100644 index 00000000..3a8eb830 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonThreadStore.java @@ -0,0 +1,56 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JsonThreadStore { + @JsonProperty("threads") + @JsonSerialize(using = JsonThreadStore.MapToListSerializer.class) + @JsonDeserialize(using = ThreadsDeserializer.class) + private Map threads = new HashMap<>(); + + private static final ObjectMapper jsonProcessor = new ObjectMapper(); + + void updateThread(ThreadInfo thread) { + threads.put(thread.id, thread); + } + + ThreadInfo getThread(String id) { + return threads.get(id); + } + + List getThreads() { + return new ArrayList<>(threads.values()); + } + + public static class MapToListSerializer extends JsonSerializer> { + @Override + public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { + jgen.writeObject(value.values()); + } + } + + public static class ThreadsDeserializer extends JsonDeserializer> { + @Override + public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + Map threads = new HashMap<>(); + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + for (JsonNode n : node) { + ThreadInfo t = jsonProcessor.treeToValue(n, ThreadInfo.class); + threads.put(t.id, t); + } + + return threads; + } + } +} diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 42f5ba85..1aefc252 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -107,6 +107,7 @@ class Manager implements Signal { private SignalServiceAccountManager accountManager; private JsonGroupStore groupStore; private JsonContactsStore contactStore; + private JsonThreadStore threadStore; public Manager(String username, String settingsPath) { this.username = username; @@ -267,6 +268,13 @@ class Manager implements Signal { if (contactStore == null) { contactStore = new JsonContactsStore(); } + JsonNode threadStoreNode = rootNode.get("threadStore"); + if (threadStoreNode != null) { + threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class); + } + if (threadStore == null) { + threadStore = new JsonThreadStore(); + } } private void migrateLegacyConfigs() { @@ -305,6 +313,7 @@ class Manager implements Signal { .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) .putPOJO("contactStore", contactStore) + .putPOJO("threadStore", threadStore) ; try { openFileChannel(); @@ -572,14 +581,17 @@ class Manager implements Signal { .build(); messageBuilder.asGroupMessage(group); } - SignalServiceDataMessage message = messageBuilder.build(); + ThreadInfo thread = threadStore.getThread(Base64.encodeBytes(groupId)); + if (thread != null) { + messageBuilder.withExpiration(thread.messageExpirationTime); + } final GroupInfo g = getGroupForSending(groupId); // Don't send group message to ourself final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); - sendMessage(message, membersSend); + sendMessage(messageBuilder, membersSend); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { @@ -587,15 +599,14 @@ class Manager implements Signal { .withId(groupId) .build(); - SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() - .asGroupMessage(group) - .build(); + SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .asGroupMessage(group); final GroupInfo g = getGroupForSending(groupId); g.members.remove(this.username); groupStore.updateGroup(g); - sendMessage(message, g.members); + sendMessage(messageBuilder, g.members); } private static String join(CharSequence separator, Iterable list) { @@ -672,14 +683,13 @@ class Manager implements Signal { groupStore.updateGroup(g); - SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .build(); + SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()); // Don't send group message to ourself final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); - sendMessage(message, membersSend); + sendMessage(messageBuilder, membersSend); return g.groupId; } @@ -699,25 +709,22 @@ class Manager implements Signal { if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); } - SignalServiceDataMessage message = messageBuilder.build(); - - sendMessage(message, recipients); + sendMessage(messageBuilder, recipients); } @Override public void sendEndSessionMessage(List recipients) throws IOException, EncapsulatedExceptions { - SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder() - .asEndSessionMessage() - .build(); + SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .asEndSessionMessage(); - sendMessage(message, recipients); + sendMessage(messageBuilder, recipients); } private void requestSyncGroups() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { - sendMessage(message); + sendSyncMessage(message); } catch (UntrustedIdentityException e) { e.printStackTrace(); } @@ -727,13 +734,13 @@ class Manager implements Signal { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); try { - sendMessage(message); + sendSyncMessage(message); } catch (UntrustedIdentityException e) { e.printStackTrace(); } } - private void sendMessage(SignalServiceSyncMessage message) + private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); @@ -745,24 +752,17 @@ class Manager implements Signal { } } - private void sendMessage(SignalServiceDataMessage message, Collection recipients) + private void sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) throws EncapsulatedExceptions, IOException { - Set recipientsTS = new HashSet<>(recipients.size()); - for (String recipient : recipients) { - try { - recipientsTS.add(getPushAddress(recipient)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - save(); - return; - } - } + Set recipientsTS = getSignalServiceAddresses(recipients); + if (recipientsTS == null) return; + SignalServiceDataMessage message = null; try { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); + message = messageBuilder.build(); if (message.getGroupInfo().isPresent()) { try { messageSender.sendMessage(new ArrayList<>(recipientsTS), message); @@ -777,6 +777,13 @@ class Manager implements Signal { List unregisteredUsers = new LinkedList<>(); List networkExceptions = new LinkedList<>(); for (SignalServiceAddress address : recipientsTS) { + ThreadInfo thread = threadStore.getThread(address.getNumber()); + if (thread != null) { + messageBuilder.withExpiration(thread.messageExpirationTime); + } else { + messageBuilder.withExpiration(0); + } + message = messageBuilder.build(); try { messageSender.sendMessage(address, message); } catch (UntrustedIdentityException e) { @@ -793,7 +800,7 @@ class Manager implements Signal { } } } finally { - if (message.isEndSession()) { + if (message != null && message.isEndSession()) { for (SignalServiceAddress recipient : recipientsTS) { handleEndSession(recipient.getNumber()); } @@ -802,6 +809,21 @@ class Manager implements Signal { } } + private Set getSignalServiceAddresses(Collection recipients) { + Set recipientsTS = new HashSet<>(recipients.size()); + for (String recipient : recipients) { + try { + recipientsTS.add(getPushAddress(recipient)); + } catch (InvalidNumberException e) { + System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + save(); + return null; + } + } + return recipientsTS; + } + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException { SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore); try { @@ -821,8 +843,10 @@ class Manager implements Signal { } private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { + String threadId; if (message.getGroupInfo().isPresent()) { SignalServiceGroup groupInfo = message.getGroupInfo().get(); + threadId = Base64.encodeBytes(groupInfo.getGroupId()); switch (groupInfo.getType()) { case UPDATE: GroupInfo group; @@ -862,10 +886,27 @@ class Manager implements Signal { } break; } + } else { + if (isSync) { + threadId = destination; + } else { + threadId = source; + } } if (message.isEndSession()) { handleEndSession(isSync ? destination : source); } + if (message.isExpirationUpdate() || message.getBody().isPresent()) { + ThreadInfo thread = threadStore.getThread(threadId); + if (thread == null) { + thread = new ThreadInfo(); + thread.id = threadId; + } + if (thread.messageExpirationTime != message.getExpiresInSeconds()) { + thread.messageExpirationTime = message.getExpiresInSeconds(); + threadStore.updateThread(thread); + } + } if (message.getAttachments().isPresent()) { for (SignalServiceAttachment attachment : message.getAttachments().get()) { if (attachment.isPointer()) { @@ -1273,7 +1314,7 @@ class Manager implements Signal { .withLength(groupsFile.length()) .build(); - sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); } } finally { groupsFile.delete(); @@ -1302,7 +1343,7 @@ class Manager implements Signal { .withLength(contactsFile.length()) .build(); - sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); + sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); } } finally { contactsFile.delete(); diff --git a/src/main/java/org/asamk/signal/ThreadInfo.java b/src/main/java/org/asamk/signal/ThreadInfo.java new file mode 100644 index 00000000..a664059b --- /dev/null +++ b/src/main/java/org/asamk/signal/ThreadInfo.java @@ -0,0 +1,11 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ThreadInfo { + @JsonProperty + public String id; + + @JsonProperty + public int messageExpirationTime; +} From 197619f1c0032411cc48379860f0e4e8bc9efddc Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 3 Nov 2016 20:51:25 +0100 Subject: [PATCH 0157/2005] Handle AssertionError also when linking devices --- src/main/java/org/asamk/signal/Main.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 5dd471f9..84619b76 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -177,6 +177,9 @@ public class Main { } catch (IOException e) { System.err.println("Link request error: " + e.getMessage()); return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; } catch (InvalidKeyException e) { e.printStackTrace(); return 2; @@ -202,6 +205,9 @@ public class Main { } catch (InvalidKeyException e) { e.printStackTrace(); return 2; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; } catch (URISyntaxException e) { e.printStackTrace(); return 2; From 3b22ae1b31a915a256c8a30daa95608e571021fb Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 5 Nov 2016 23:10:22 +0100 Subject: [PATCH 0158/2005] Update dependency --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d747ef73..cadf2f82 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'org.whispersystems:signal-service-java:2.3.1_fetchMessages_provisioning' + compile 'org.whispersystems:signal-service-java:2.3.1_fetchMessages_provisioning_2' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' From 447dd1cb4fa22186567aea51928e1434dd5d911d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 13 Nov 2016 19:24:23 +0100 Subject: [PATCH 0159/2005] Use new fork on maven central --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cadf2f82..ef0b0b36 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'org.whispersystems:signal-service-java:2.3.1_fetchMessages_provisioning_2' + compile 'com.github.turasa:signal-service-java:2.3.1_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' From 1bae3ba6f04e608724ea55feb65d3909c3536b79 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 14 Nov 2016 13:25:30 +0100 Subject: [PATCH 0160/2005] Update dependency --- build.gradle | 2 +- src/main/java/org/asamk/signal/JsonIdentityKeyStore.java | 9 +++++---- .../java/org/asamk/signal/JsonSignalProtocolStore.java | 8 ++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index ef0b0b36..cc762c37 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.3.1_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.4.0_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java index 14c0d11e..d71e3581 100644 --- a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.*; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.IdentityKeyStore; import java.io.IOException; @@ -36,8 +37,8 @@ class JsonIdentityKeyStore implements IdentityKeyStore { } @Override - public void saveIdentity(String name, IdentityKey identityKey) { - saveIdentity(name, identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); + public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { + saveIdentity(address.getName(), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); } /** @@ -71,8 +72,8 @@ class JsonIdentityKeyStore implements IdentityKeyStore { } @Override - public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - List identities = trustedKeys.get(name); + public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) { + List identities = trustedKeys.get(address.getName()); if (identities == null) { // Trust on first use return true; diff --git a/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java index a3159e48..79f49c7f 100644 --- a/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java @@ -65,8 +65,8 @@ class JsonSignalProtocolStore implements SignalProtocolStore { } @Override - public void saveIdentity(String name, IdentityKey identityKey) { - identityKeyStore.saveIdentity(name, identityKey); + public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { + identityKeyStore.saveIdentity(address, identityKey); } public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) { @@ -82,8 +82,8 @@ class JsonSignalProtocolStore implements SignalProtocolStore { } @Override - public boolean isTrustedIdentity(String name, IdentityKey identityKey) { - return identityKeyStore.isTrustedIdentity(name, identityKey); + public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) { + return identityKeyStore.isTrustedIdentity(address, identityKey); } @Override From 6ae610e2b1a8a37c9a0f57e24803a6f56cbe3d72 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 14 Nov 2016 17:02:32 +0100 Subject: [PATCH 0161/2005] Update gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 52928 -> 54224 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 20 +++++++++++--------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 6ffa237849ef3607e39c3b334a92a65367962071..d6e2637affb74a80bfbe87bd2da57e81b2f3c661 100644 GIT binary patch delta 15212 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 y-Dd3fn delta 13961 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 Date: Fri, 18 Nov 2016 13:39:53 +0100 Subject: [PATCH 0162/2005] Format timestamps --- src/main/java/org/asamk/signal/Main.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 84619b76..b2550fe7 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -227,8 +227,8 @@ public class Main { for (DeviceInfo d : devices) { System.out.println("Device " + d.getId() + (d.getId() == m.getDeviceId() ? " (this device)" : "") + ":"); System.out.println(" Name: " + d.getName()); - System.out.println(" Created: " + d.getCreated()); - System.out.println(" Last seen: " + d.getLastSeen()); + System.out.println(" Created: " + formatTimestamp(d.getCreated())); + System.out.println(" Last seen: " + formatTimestamp(d.getLastSeen())); } } catch (IOException e) { e.printStackTrace(); From 6aefa38ee871f76355a8e0ff18d4e3309142db1c Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 18 Nov 2016 21:49:41 +0100 Subject: [PATCH 0163/2005] Add man page --- man/Makefile | 10 ++ man/signal-cli.1.txt | 234 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 man/Makefile create mode 100644 man/signal-cli.1.txt diff --git a/man/Makefile b/man/Makefile new file mode 100644 index 00000000..47ccd35f --- /dev/null +++ b/man/Makefile @@ -0,0 +1,10 @@ +A2X = a2x + +MANPAGESRC = signal-cli.1 + +.PHONY: all +all: $(MANPAGESRC) + +%: %.txt + @echo "Generating manpage for $@" + $(A2X) --no-xmllint --doctype manpage --format manpage "$^" diff --git a/man/signal-cli.1.txt b/man/signal-cli.1.txt new file mode 100644 index 00000000..52d56f68 --- /dev/null +++ b/man/signal-cli.1.txt @@ -0,0 +1,234 @@ +///// +vim:set ts=4 sw=4 tw=82 noet: +///// +:quotes.~: + +signal-cli (1) +============ + +Name +---- +signal-cli - A commandline and dbus interface for the Signal messenger + +Synopsis +-------- +*signal-cli* [--config CONFIG] [-h | -v | -u USERNAME | --dbus | --dbus-system] command [command-options] + +Description +----------- + +signal-cli is a commandline interface for libsignal-service-java. It supports +registering, verifying, sending and receiving messages. For registering you need a +phone number where you can receive SMS or incoming calls. +signal-cli was primarily developed to be used on servers to notify admins of +important events. For this use-case, it has a dbus interface, that can be used to +send messages from any programming language that has dbus bindings. + +Options +------- + +*-h*, *--help*:: + Show help message and quit. + +*-v*, *--version*:: + Print the version and quit. + +*--config* CONFIG:: + Set the path, where to store the config. + (Default: $HOME/.config/signal) + +*-u* USERNAME, *--username* USERNAME:: + Specify your phone number, that will be your identifier. + +*--dbus*:: + Make request via user dbus. + +*--dbus-system*:: + Make request via system dbus. + +Commands +-------- + +register +~~~~~~~~ +Register a phone number with SMS or voice verification. Use the verify command to +complete the verification. + +*-v*, *--voice*:: + The verification should be done over voice, not SMS. + +verify +~~~~~~ +Verify the number using the code received via SMS or voice. + +VERIFICATIONCODE:: + The verification code. + +link +~~~~ +Link to an existing device, instead of registering a new number. This shows a +"tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can +just use this URI. If you want to link to an Android/iOS device, create a QR code +with the URI (e.g. with qrencode) and scan that in the Signal app. + +*-n* NAME, *--name* NAME:: + Optionally specify a name to describe this new device. By default "cli" will + be used. + +addDevice +~~~~~~~~~ +Link another device to this device. Only works, if this is the master device. + +*--uri* URI:: + Specify the uri contained in the QR code shown by the new device. + +listDevices +~~~~~~~~~~~ +Show a list of connected devices. + +removeDevice +~~~~~~~~~~~~ +Remove a connected device. Only works, if this is the master device. + +*-d* DEVICEID, *--deviceId* DEVICEID:: + Specify the device you want to remove. Use listDevices to see the deviceIds. + +send +~~~~ +Send a message to another user or group. + +RECIPIENT:: + Specify the recipients’ phone number. + +*-g* GROUP, *--group* GROUP:: + Specify the recipient group ID in base64 encoding. + +*-m* MESSAGE, *--message* MESSAGE:: + Specify the message, if missing, standard input is used. + +*-a* [ATTACHMENT [ATTACHMENT ...]], *--attachment* [ATTACHMENT [ATTACHMENT ...]]:: + Add one or more files as attachment. + +*-e*, *--endsession*:: + Clear session state and send end session message. + +receive +~~~~~~~ +Query the server for new messages. New messages are printed on standardoutput and +attachments are downloaded to the config directory. + +*-t* TIMEOUT, *--timeout* TIMEOUT:: + Number of seconds to wait for new messages (negative values disable timeout). + Default is 5 seconds. + +updateGroup +~~~~~~~~~~~ +Create or update a group. + +*-g* GROUP, *--group* GROUP:: + Specify the recipient group ID in base64 encoding. If not specified, a new + group with a new random ID is generated. + +*-n* NAME, *--name* NAME:: + Specify the new group name. + +*-a* AVATAR, *--avatar* AVATAR:: + Specify a new group avatar image file. + +*-m* [MEMBER [MEMBER ...]], *--member* [MEMBER [MEMBER ...]]:: + Specify one or more members to add to the group. + +quitGroup +~~~~~~~~~ +Send a quit group message to all group members and remove self from member list. + +*-g* GROUP, *--group* GROUP:: + Specify the recipient group ID in base64 encoding. + + +listIdentities +~~~~~~~~~~~~~~ +List all known identity keys and their trust status, fingerprint and safety +number. + +*-n* NUMBER, *--number* NUMBER:: + Only show identity keys for the given phone number. + +trust +~~~~~ +Set the trust level of a given number. The first time a key for a number is seen, +it is trusted by default (TOFU). If the key changes, the new key must be trusted +manually. + +number:: + Specify the phone number, for which to set the trust. + +*-a*, *--trust-all-known-keys*:: + Trust all known keys of this user, only use this for testing. + +*-v* VERIFIED_FINGERPRINT, *--verified-fingerprint* VERIFIED_FINGERPRINT:: + Specify the safety number or fingerprint of the key, only use this option if you have verified + the fingerprint. + + +daemon +~~~~~~ +signal-cli can run in daemon mode and provides an experimental dbus interface. For +dbus support you need jni/unix-java.so installed on your system (Debian: +libunixsocket-java ArchLinux: libmatthew-unix-java (AUR)). + +*--system*:: + Use DBus system bus instead of user bus. + + +Examples +-------- + +Register a number (with SMS verification):: + signal-cli -u USERNAME register + +Verify the number using the code received via SMS or voice:: + signal-cli -u USERNAME verify CODE + +Send a message to one or more recipients:: + signal-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] + +Pipe the message content from another process:: + uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]] + +Create a group:: + signal-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]] + +Add member to a group:: + signal-cli -u USERNAME updateGroup -g GROUP_ID -m "NEW_MEMBER" + +Leave a group:: + signal-cli -u USERNAME quitGroup -g GROUP_ID + +Send a message to a group:: + signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID + +Trust new key, after having verified it:: + signal-cli -u USERNAME trust -v FINGER_PRINT NUMBER + +Trust new key, without having verified it. Only use this if you don't care about security:: + signal-cli -u USERNAME trust -a NUMBER + +Files +----- +The password and cryptographic keys are created when registering and stored in the +current users home directory, the directory can be changed with *--config*: + + $HOME/.config/signal/ + +For legacy users, the old config directory is used as a fallback: + + $HOME/.config/textsecure/ + + +Authors +------- + +Maintained by AsamK , who is assisted by other open +source contributors. For more information about signal-cli development, see +. From 998ec5e7aa3151bbcfd09943b7e1e67302f51b17 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 18 Nov 2016 13:10:02 +0100 Subject: [PATCH 0164/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cc762c37..3e7bbb71 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.5.0' +version = '0.5.1' compileJava.options.encoding = 'UTF-8' From 8bfef24ef787be0e4dee0ca054e6c128afdf0c55 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 25 Nov 2016 14:03:58 +0100 Subject: [PATCH 0165/2005] Support sending and receiving group info requests --- build.gradle | 2 +- src/main/java/org/asamk/signal/Manager.java | 97 +++++++++++++++++---- 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/build.gradle b/build.gradle index 3e7bbb71..e61d0d99 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.4.0_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.4.1_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 1aefc252..b16c8eae 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -663,28 +663,15 @@ class Manager implements Signal { } } - SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) - .withId(g.groupId) - .withName(g.name) - .withMembers(new ArrayList<>(g.members)); - - File aFile = getGroupAvatarFile(g.groupId); if (avatarFile != null) { createPrivateDirectories(avatarsPath); + File aFile = getGroupAvatarFile(g.groupId); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } - if (aFile.exists()) { - try { - group.withAvatar(createAttachment(aFile)); - } catch (IOException e) { - throw new AttachmentInvalidException(avatarFile, e); - } - } groupStore.updateGroup(g); - SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()); + SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); // Don't send group message to ourself final List membersSend = new ArrayList<>(g.members); @@ -693,6 +680,60 @@ class Manager implements Signal { return g.groupId; } + private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions { + if (groupId == null) { + return; + } + GroupInfo g = getGroupForSending(groupId); + + if (!g.members.contains(recipient)) { + return; + } + + SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); + + // Send group message only to the recipient who requested it + final List membersSend = new ArrayList<>(); + membersSend.add(recipient); + sendMessage(messageBuilder, membersSend); + } + + private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) { + SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) + .withId(g.groupId) + .withName(g.name) + .withMembers(new ArrayList<>(g.members)); + + File aFile = getGroupAvatarFile(g.groupId); + if (aFile.exists()) { + try { + group.withAvatar(createAttachment(aFile)); + } catch (IOException e) { + throw new AttachmentInvalidException(aFile.toString(), e); + } + } + + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()); + } + + private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions { + if (groupId == null) { + return; + } + + SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO) + .withId(groupId); + + SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()); + + // Send group info request message to the recipient who sent us a message with this groupId + final List membersSend = new ArrayList<>(); + membersSend.add(recipient); + sendMessage(messageBuilder, membersSend); + } + @Override public void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException { @@ -847,10 +888,9 @@ class Manager implements Signal { if (message.getGroupInfo().isPresent()) { SignalServiceGroup groupInfo = message.getGroupInfo().get(); threadId = Base64.encodeBytes(groupInfo.getGroupId()); + GroupInfo group = groupStore.getGroup(groupInfo.getGroupId()); switch (groupInfo.getType()) { case UPDATE: - GroupInfo group; - group = groupStore.getGroup(groupInfo.getGroupId()); if (group == null) { group = new GroupInfo(groupInfo.getGroupId()); } @@ -877,14 +917,33 @@ class Manager implements Signal { groupStore.updateGroup(group); break; case DELIVER: + if (group == null) { + try { + sendGroupInfoRequest(groupInfo.getGroupId(), source); + } catch (IOException | EncapsulatedExceptions e) { + e.printStackTrace(); + } + } break; case QUIT: - group = groupStore.getGroup(groupInfo.getGroupId()); - if (group != null) { + if (group == null) { + try { + sendGroupInfoRequest(groupInfo.getGroupId(), source); + } catch (IOException | EncapsulatedExceptions e) { + e.printStackTrace(); + } + } else { group.members.remove(source); groupStore.updateGroup(group); } break; + case REQUEST_INFO: + try { + sendUpdateGroupMessage(groupInfo.getGroupId(), source); + } catch (IOException | EncapsulatedExceptions e) { + e.printStackTrace(); + } + break; } } else { if (isSync) { From c542fb87cb7c5ff7146c0853328189a5a1ebbb6d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 26 Nov 2016 12:44:17 +0100 Subject: [PATCH 0166/2005] Add missing close of attachment input stream --- src/main/java/org/asamk/signal/Manager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index b16c8eae..ee5aeeb3 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1320,6 +1320,7 @@ class Manager implements Signal { if (output != null) { output.close(); } + input.close(); if (!tmpFile.delete()) { System.err.println("Failed to delete temp file: " + tmpFile); } From 5b839bbae08762cc6012f5941a46bb613801b126 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 26 Nov 2016 13:26:04 +0100 Subject: [PATCH 0167/2005] Fix crash when receiving group request for unkown group Fixes #33 --- src/main/java/org/asamk/signal/Manager.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index ee5aeeb3..8de14b2d 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -938,10 +938,14 @@ class Manager implements Signal { } break; case REQUEST_INFO: - try { - sendUpdateGroupMessage(groupInfo.getGroupId(), source); - } catch (IOException | EncapsulatedExceptions e) { - e.printStackTrace(); + if (group != null) { + try { + sendUpdateGroupMessage(groupInfo.getGroupId(), source); + } catch (IOException | EncapsulatedExceptions e) { + e.printStackTrace(); + } catch (NotAGroupMemberException e) { + // We have left this group, so don't send a group update message + } } break; } From eb0860d350fd0fda94536f369deddf8b938fd169 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 26 Nov 2016 13:42:29 +0100 Subject: [PATCH 0168/2005] Add another missing close() --- src/main/java/org/asamk/signal/Manager.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 8de14b2d..77647d27 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1372,13 +1372,17 @@ class Manager implements Signal { if (groupsFile.exists() && groupsFile.length() > 0) { FileInputStream contactsFileStream = new FileInputStream(groupsFile); - SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() - .withStream(contactsFileStream) - .withContentType("application/octet-stream") - .withLength(groupsFile.length()) - .build(); + try { + SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(contactsFileStream) + .withContentType("application/octet-stream") + .withLength(groupsFile.length()) + .build(); - sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + } finally { + contactsFileStream.close(); + } } } finally { groupsFile.delete(); From 9dc42e4d33ae51441f39da97f7768483d0e030df Mon Sep 17 00:00:00 2001 From: Lars Wallenborn Date: Mon, 28 Nov 2016 10:20:32 +0100 Subject: [PATCH 0169/2005] Update README.md (#31) --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 617742c2..8aa5cfb2 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,12 @@ usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-sys signal-cli -u USERNAME trust -a NUMBER +* Set configuration directory + + signal-cli --config=/home/other_user/.config/signal + + This is particularily useful in the case, when you would like to run the signal-cli tool as a different user as the one, that was used to register the account. You should make sure, that the caller has full read/write access to the given directory. + ## DBus service signal-cli can run in daemon mode and provides an experimental dbus interface. From ae3e5be1241b3ae9d27fff950c3bb54bdcdc32e7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 28 Nov 2016 10:30:52 +0100 Subject: [PATCH 0170/2005] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8aa5cfb2..984f2d67 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-system] {link,addDevice,listDevices,removeDevice,register,verify,send,quitGroup,updateGroup,listIdentities,trust,receive,daemon} ... +See also: [man page in asciidoc format](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.txt) + * Register a number (with SMS verification) signal-cli -u USERNAME register @@ -109,7 +111,7 @@ usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-sys signal-cli --config=/home/other_user/.config/signal - This is particularily useful in the case, when you would like to run the signal-cli tool as a different user as the one, that was used to register the account. You should make sure, that the caller has full read/write access to the given directory. + This is particularily useful in the case, when you would like to run the signal-cli tool as a different user as the one, that was used to register the account. You should make sure, that the caller has full read/write access to the given directory. ## DBus service From e364610c9304d6770118f36d02f8df74e452a43f Mon Sep 17 00:00:00 2001 From: Benedikt Constantin Radtke Date: Sun, 27 Nov 2016 19:11:44 +0100 Subject: [PATCH 0171/2005] use java7's nio api to get more detailed error messages --- src/main/java/org/asamk/signal/Manager.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 77647d27..d329990d 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1325,8 +1325,11 @@ class Manager implements Signal { output.close(); } input.close(); - if (!tmpFile.delete()) { - System.err.println("Failed to delete temp file: " + tmpFile); + try { + Files.delete(tmpFile.toPath()); + } catch(Exception e) { + System.out.println("Failed to delete temp file: " + tmpFile); + e.printStackTrace(); } } return outputFile; From 2351a89b0046d54f0830c536cc504a04436cf2b4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 28 Nov 2016 12:28:44 +0100 Subject: [PATCH 0172/2005] Use nio Files.delete instead of File.delete everywhere --- src/main/java/org/asamk/signal/Manager.java | 91 +++++++++++++-------- src/main/java/org/asamk/signal/Util.java | 6 ++ 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index d329990d..d06a07c0 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -993,7 +993,6 @@ class Manager implements Signal { continue; } - String sender = dir.getName(); for (final File fileEntry : dir.listFiles()) { if (!fileEntry.isFile()) { continue; @@ -1019,7 +1018,11 @@ class Manager implements Signal { } save(); handler.handleMessage(envelope, content, null); - fileEntry.delete(); + try { + Files.delete(fileEntry.toPath()); + } catch (IOException e) { + System.out.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); + } } } } @@ -1069,12 +1072,12 @@ class Manager implements Signal { save(); handler.handleMessage(envelope, content, exception); if (exception == null || !(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { + File cacheFile = null; try { - File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp()); - cacheFile.delete(); + cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp()); + Files.delete(cacheFile.toPath()); } catch (IOException e) { - // Ignoring - return; + System.out.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); } } } @@ -1114,8 +1117,10 @@ class Manager implements Signal { } } if (syncMessage.getGroups().isPresent()) { + File tmpFile = null; try { - DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer())); + tmpFile = Util.createTempFile(); + DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile)); DeviceGroup g; while ((g = s.read()) != null) { GroupInfo syncGroup = groupStore.getGroup(g.getId()); @@ -1135,14 +1140,24 @@ class Manager implements Signal { } } catch (Exception e) { e.printStackTrace(); + } finally { + if (tmpFile != null) { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); + } + } } if (syncMessage.getBlockedList().isPresent()) { // TODO store list of blocked numbers } } if (syncMessage.getContacts().isPresent()) { + File tmpFile = null; try { - DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer())); + tmpFile = Util.createTempFile(); + DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer(), tmpFile)); DeviceContact c; while ((c = s.read()) != null) { ContactInfo contact = contactStore.getContact(c.getNumber()); @@ -1164,6 +1179,14 @@ class Manager implements Signal { } } catch (Exception e) { e.printStackTrace(); + } finally { + if (tmpFile != null) { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); + } + } } } } @@ -1305,7 +1328,7 @@ class Manager implements Signal { final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); - File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); + File tmpFile = Util.createTempFile(); InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); OutputStream output = null; @@ -1327,20 +1350,16 @@ class Manager implements Signal { input.close(); try { Files.delete(tmpFile.toPath()); - } catch(Exception e) { - System.out.println("Failed to delete temp file: " + tmpFile); - e.printStackTrace(); + } catch (IOException e) { + System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); } } return outputFile; } - private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { + private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); - File file = File.createTempFile("ts_tmp", "tmp"); - file.deleteOnExit(); - - return messageReceiver.retrieveAttachment(pointer, file); + return messageReceiver.retrieveAttachment(pointer, tmpFile); } private String canonicalizeNumber(String number) throws InvalidNumberException { @@ -1359,7 +1378,7 @@ class Manager implements Signal { } private void sendGroups() throws IOException, UntrustedIdentityException { - File groupsFile = File.createTempFile("multidevice-group-update", ".tmp"); + File groupsFile = Util.createTempFile(); try { DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile)); @@ -1374,26 +1393,27 @@ class Manager implements Signal { } if (groupsFile.exists() && groupsFile.length() > 0) { - FileInputStream contactsFileStream = new FileInputStream(groupsFile); - try { + try (FileInputStream groupsFileStream = new FileInputStream(groupsFile)) { SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() - .withStream(contactsFileStream) + .withStream(groupsFileStream) .withContentType("application/octet-stream") .withLength(groupsFile.length()) .build(); sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); - } finally { - contactsFileStream.close(); } } } finally { - groupsFile.delete(); + try { + Files.delete(groupsFile.toPath()); + } catch (IOException e) { + System.out.println("Failed to delete temp file “" + groupsFile + "”: " + e.getMessage()); + } } } private void sendContacts() throws IOException, UntrustedIdentityException { - File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp"); + File contactsFile = Util.createTempFile(); try { DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile)); @@ -1407,17 +1427,22 @@ class Manager implements Signal { } if (contactsFile.exists() && contactsFile.length() > 0) { - FileInputStream contactsFileStream = new FileInputStream(contactsFile); - SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() - .withStream(contactsFileStream) - .withContentType("application/octet-stream") - .withLength(contactsFile.length()) - .build(); + try (FileInputStream contactsFileStream = new FileInputStream(contactsFile)) { + SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(contactsFileStream) + .withContentType("application/octet-stream") + .withLength(contactsFile.length()) + .build(); - sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); + sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); + } } } finally { - contactsFile.delete(); + try { + Files.delete(contactsFile.toPath()); + } catch (IOException e) { + System.out.println("Failed to delete temp file “" + contactsFile + "”: " + e.getMessage()); + } } } diff --git a/src/main/java/org/asamk/signal/Util.java b/src/main/java/org/asamk/signal/Util.java index 66a08731..4eeabf1c 100644 --- a/src/main/java/org/asamk/signal/Util.java +++ b/src/main/java/org/asamk/signal/Util.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import java.io.File; +import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -22,4 +24,8 @@ class Util { throw new AssertionError(e); } } + + public static File createTempFile() throws IOException { + return File.createTempFile("signal_tmp_", ".tmp"); + } } From c5cf78a50ad213fb21728f4d648de51dfac7f07e Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 28 Nov 2016 12:38:43 +0100 Subject: [PATCH 0173/2005] Allow millisecond timeouts --- src/main/java/org/asamk/signal/Main.java | 13 +++++++------ src/main/java/org/asamk/signal/Manager.java | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index b2550fe7..d67bfca0 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -46,6 +46,7 @@ import java.security.Security; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class Main { @@ -365,9 +366,9 @@ public class Main { System.err.println("User is not registered."); return 1; } - int timeout = 5; - if (ns.getInt("timeout") != null) { - timeout = ns.getInt("timeout"); + double timeout = 5; + if (ns.getDouble("timeout") != null) { + timeout = ns.getDouble("timeout"); } boolean returnOnTimeout = true; if (timeout < 0) { @@ -375,7 +376,7 @@ public class Main { timeout = 3600; } try { - m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m)); + m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, new ReceiveMessageHandler(m)); } catch (IOException e) { System.err.println("Error while receiving messages: " + e.getMessage()); return 3; @@ -549,7 +550,7 @@ public class Main { return 2; } try { - m.receiveMessages(3600, false, new DbusReceiveMessageHandler(m, conn)); + m.receiveMessages(1, TimeUnit.HOURS, false, new DbusReceiveMessageHandler(m, conn)); } catch (IOException e) { System.err.println("Error while receiving messages: " + e.getMessage()); return 3; @@ -719,7 +720,7 @@ public class Main { Subparser parserReceive = subparsers.addParser("receive"); parserReceive.addArgument("-t", "--timeout") - .type(int.class) + .type(double.class) .help("Number of seconds to wait for new messages (negative values disable timeout)"); Subparser parserDaemon = subparsers.addParser("daemon"); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index d06a07c0..6598bfde 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1027,7 +1027,7 @@ class Manager implements Signal { } } - public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { + public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler); final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); SignalServiceMessagePipe messagePipe = null; @@ -1041,7 +1041,7 @@ class Manager implements Signal { Exception exception = null; final long now = new Date().getTime(); try { - envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS, new SignalServiceMessagePipe.MessagePipeCallback() { + envelope = messagePipe.read(timeout, unit, new SignalServiceMessagePipe.MessagePipeCallback() { @Override public void onMessage(SignalServiceEnvelope envelope) { // store message on disk, before acknowledging receipt to the server From c3bfcff5a887d9cc706f087b5fe6c6c524ab8ced Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 29 Nov 2016 11:00:49 +0100 Subject: [PATCH 0174/2005] Update gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 54224 -> 54227 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- gradlew | 11 ++++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d6e2637affb74a80bfbe87bd2da57e81b2f3c661..51288f9c2f05faf8d42e1a751a387ca7923882c3 100644 GIT binary patch delta 2024 zcmZWp4Nz276u$2*un4=Bi@?I$g#{aQMMO~qB?PiDP#`qMa0J~P6jKAm(a4{`%)hZo z?KF+Q6N*LQfBvE9kgy7iOS=n$Mvemg3@OB@HHK)?dFL|Pvz@tnzy02KzH`pKXP>iN z)m*O9W=vPPc`@ec$tqT_+NllG;+7ISd*!Ai#@MW#iJ8840v;6JFBI5wWhpAlLsas1 zZ3Jx($H?0$v*0yI1~9>^lL(WySxq2S_n`-YM!i}ja3gRo2eHOmZp7$Ahj8&IbBw#p zz{1fT^kiD(4z9g5Hd9N?_=GGU0^4VHdJ*^_=MdMJEUg6K$^2~H7uZe7uKwti*v&Rv zdIm0AU~zPP+~Arq8s@z8c2dR9Pd?Yo{xK`7T}Ea;u{TvRC6rFXWM+?@)sR)#ne4`_f4#A!TmhE+N@s$0*HLay8 z4%*6E)TB9gxSv;X{#X(>{O7w&&Sajs!$D%@Jr2fI0XMW=4CX#v)m!Khtg6XS;Vglz zX1sEyz^gWT2HtK8UI!E9FZ{Ui4ZmMzeNi)Cq z-SFQ%7z-TCSeQcN3n#46CfVn>CjH?U)SSU`#wj#OI!d@qXM#yrG*m~*6-lhoV`-uA zRIi890s1)7_CQjn)(wKYS6(6YkgJjyVI(@+XavhuZc>1>HWX<;hz}hMCkE(hq(jW4 z&M*gpcO6#;)=A==(F5KF@?C;dLL^c7ks7%;>f}~mn^~`A>~;WSA&O3##ty`L!h<>; zi9Oy6-edG(tlP|(Ng*~Tpx6&4)$8dD&uz7I9mES9smG-dehka&CsIL2>m@NihjO&l zhmpTsJ9p|%oQ+3#v=NFhUkeFMRy9nw_`tVK8p`v_M(4aB6dG_1Qv}?HVp~-3|K9`8 zMTgdBfYUZyo_^@FNy20BYX~yQ5pTE_c7KLt>~nwdc1PwysVfpTH0Ve;@1@C2WhgYO zUleNd;9kKnNXgT|-3ATic+?aIwkSwVT#%AN+MtZyi5?2;W5?sqp}+@C6m6$oVHExRU6QE*?pXl81EzPKmOwY4D{hm5S(bN#cY` zx}c&;zh_;}ZjwZE6=pTS+$w+QcJNLOOZ-kYqT@{*{}@Hq>Wk#@w8=oW#%4+EsKzS` XfVY}?^J|+WVJYltj`FxvBZvMAtQotM delta 2098 zcmZWqYfuwc6y9u*Kte!5k&q1!BSlaILKQ{Ch;~{T5z%3)rBD%~5vw=@wo)kycAVND z9l@ivN>PjWjI}CN8HvgSkXL{}^oNgDZMA5905ff6TEw0`i*XirW^>Pe_dDl&_uO-D z97m=0qf%w+0;!9fVcgu96k}ewGE|8}(yHtCS_}-sq?OM}^BKq=$d@p$dG8|c_;@Kl zbcE3`7|jnglO=L@-hdhMUIKKAa2XjXs?Dxsbg5-588$=)47TWcc(k zJOZmD@6k+hRGGlNqfb*35^GrILq_?MUO5>>nbiVEmvw^-7|za6#^G8b2CeBEa4lbf zDQEl^y{9$WV@~&_rHOOTI70p`+xE?2MMiy!*86y_dFF$m;E>^eHs4G)zu14Mok=c* zjukqqRkd@PaqO4RS8f=$w`o^t*Tws$bybx&1G`IPCEoU`ipW21_3xSY_;n0{N)R}=4@#w;e9n&w^S;|9W0+=dI8)*a5q7^C|LXlRa=U1LDM+=PN~9cT zYtjT@>A6ROF|W2?Fkj7nV~Vc(JpEzWn<=>|^n2%;5! zo}(rP8aP%f0ezMhCN*k;`6RthmKB`9aJTT)Ig>C;6G!$l0vpYO(3Zdc|H`(s&L}G$F!$gb7lOg)5fuvTN)q~eiYOL4jbt!Zd zaJJ6t4HbQx(!cG)z}GSG6j9)yaB}+VYA7uxpqXPoj2scBB`6P}geZazHmeD$L&&Zl zVY$KI%_@TBEgU=1K=5px4Cb|{Nb!ZQvmRIBCQef0D@wj~36T$Ex~xtHpPMD12WwD0AM1W33*+{P}R+CP+dl&ZoLG zB?s2#Xy93^2Y*=LMJp#61pl@``kL8>t1`k5`4ixDfh&}>sR^z?SZ*RX*`$J}ZG!I* z!o#@;Yawop2lRCbaB4fpURzJ>vUYvY8&`x=4Bwzz;cokMN}{oGY+k`gZa-MqBSb5) zagtBqds`B%Yog114}S^6n6S;lMS%<(Nyw8zFNp3CTn!x@d!P_57bfr!`QOjJKJgy< z=HcDJ|8u;0s!Nv@Q|f)ifqZIGz3uSNekK*eT*aLfpOihI@0>=eb2~XU5y7TYMMC##WMBQCFzoR%!9IpiA%=hKUfl*`U5Fp4^7f zt>+L7910V)24CVObKL`uq5E?@FSA7MC(M-RiJ5~R?LuWNc8)!Hir{CbIN07ORA-9s z>a8-2QHRGTMiis}ERk1r2T&O;Cl|()Bk@brnjj*+e~ySk_qf90b}b1X=;qjWYMoO> zwd0%{oYBLvp>;T=h21?9h+T(}#o}bUTp9~A@KWOYHQ2$izh1#T^@k4}p#*JlaFSf8 MaKyM?wDMa12Wsh)NB{r; diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6032e29a..05ab972d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Nov 14 16:58:51 CET 2016 +#Tue Nov 29 10:59:02 CET 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-bin.zip diff --git a/gradlew b/gradlew index 4ef3a871..4453ccea 100755 --- a/gradlew +++ b/gradlew @@ -155,13 +155,14 @@ if $cygwin ; then fi # Escape application args -for s in "${@}" ; do - s=\"$s\" - APP_ARGS=$APP_ARGS" "$s -done +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- "$DEFAULT_JVM_OPTS" "$JAVA_OPTS" "$GRADLE_OPTS" "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then From 447a188ff96ab6802a88d0fe2950b334047b1a67 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 29 Nov 2016 11:15:27 +0100 Subject: [PATCH 0175/2005] Use try-with-ressource statements instead of manually closing stream --- src/main/java/org/asamk/signal/Manager.java | 92 ++++++++------------- 1 file changed, 35 insertions(+), 57 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 6598bfde..78b66da7 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1223,26 +1223,26 @@ class Manager implements Signal { private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { try (FileOutputStream f = new FileOutputStream(file)) { - DataOutputStream out = new DataOutputStream(f); - out.writeInt(1); // version - out.writeInt(envelope.getType()); - out.writeUTF(envelope.getSource()); - out.writeInt(envelope.getSourceDevice()); - out.writeUTF(envelope.getRelay()); - out.writeLong(envelope.getTimestamp()); - if (envelope.hasContent()) { - out.writeInt(envelope.getContent().length); - out.write(envelope.getContent()); - } else { - out.writeInt(0); + try (DataOutputStream out = new DataOutputStream(f)) { + out.writeInt(1); // version + out.writeInt(envelope.getType()); + out.writeUTF(envelope.getSource()); + out.writeInt(envelope.getSourceDevice()); + out.writeUTF(envelope.getRelay()); + out.writeLong(envelope.getTimestamp()); + if (envelope.hasContent()) { + out.writeInt(envelope.getContent().length); + out.write(envelope.getContent()); + } else { + out.writeInt(0); + } + if (envelope.hasLegacyMessage()) { + out.writeInt(envelope.getLegacyMessage().length); + out.write(envelope.getLegacyMessage()); + } else { + out.writeInt(0); + } } - if (envelope.hasLegacyMessage()) { - out.writeInt(envelope.getLegacyMessage().length); - out.write(envelope.getLegacyMessage()); - } else { - out.writeInt(0); - } - out.close(); } } @@ -1288,9 +1288,7 @@ class Manager implements Signal { private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException { InputStream input = stream.getInputStream(); - OutputStream output = null; - try { - output = new FileOutputStream(outputFile); + try (OutputStream output = new FileOutputStream(outputFile)) { byte[] buffer = new byte[4096]; int read; @@ -1300,10 +1298,6 @@ class Manager implements Signal { } catch (FileNotFoundException e) { e.printStackTrace(); return null; - } finally { - if (output != null) { - output.close(); - } } return outputFile; } @@ -1311,43 +1305,31 @@ class Manager implements Signal { private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException { if (storePreview && pointer.getPreview().isPresent()) { File previewFile = new File(outputFile + ".preview"); - OutputStream output = null; - try { - output = new FileOutputStream(previewFile); + try (OutputStream output = new FileOutputStream(previewFile)) { byte[] preview = pointer.getPreview().get(); output.write(preview, 0, preview.length); } catch (FileNotFoundException e) { e.printStackTrace(); return null; - } finally { - if (output != null) { - output.close(); - } } } final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); File tmpFile = Util.createTempFile(); - InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); + try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile)) { + try (OutputStream output = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[4096]; + int read; - OutputStream output = null; - try { - output = new FileOutputStream(outputFile); - byte[] buffer = new byte[4096]; - int read; - - while ((read = input.read(buffer)) != -1) { - output.write(buffer, 0, read); + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; } - } catch (FileNotFoundException e) { - e.printStackTrace(); - return null; } finally { - if (output != null) { - output.close(); - } - input.close(); try { Files.delete(tmpFile.toPath()); } catch (IOException e) { @@ -1381,15 +1363,13 @@ class Manager implements Signal { File groupsFile = Util.createTempFile(); try { - DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile)); - try { + try (OutputStream fos = new FileOutputStream(groupsFile)) { + DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos); for (GroupInfo record : groupStore.getGroups()) { out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId), record.active)); } - } finally { - out.close(); } if (groupsFile.exists() && groupsFile.length() > 0) { @@ -1416,14 +1396,12 @@ class Manager implements Signal { File contactsFile = Util.createTempFile(); try { - DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile)); - try { + try (OutputStream fos = new FileOutputStream(contactsFile)) { + DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : contactStore.getContacts()) { out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), createContactAvatarAttachment(record.number), Optional.fromNullable(record.color))); } - } finally { - out.close(); } if (contactsFile.exists() && contactsFile.length() > 0) { From 1b7c46b1c239dc485ed821e00f26701ce59dd737 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 16 Dec 2016 11:32:26 +0100 Subject: [PATCH 0176/2005] Update dependency --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e61d0d99..86783766 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.4.1_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.4.2_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' From aa9aadacf1711100decbd7c00e8c18b4ee1e9e9a Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 16 Dec 2016 11:32:47 +0100 Subject: [PATCH 0177/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 86783766..d97111d2 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.5.1' +version = '0.5.2' compileJava.options.encoding = 'UTF-8' From d89e93ad473cc1c6dd5f4dc615b7d0c5721e3dc2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 22 Dec 2016 12:27:43 +0100 Subject: [PATCH 0178/2005] Add --ignore-attachments flag to receive and daemon command Fixes #41 --- man/signal-cli.1.txt | 4 ++++ src/main/java/org/asamk/signal/Main.java | 12 ++++++++++-- src/main/java/org/asamk/signal/Manager.java | 20 ++++++++++---------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/man/signal-cli.1.txt b/man/signal-cli.1.txt index 52d56f68..c4800d61 100644 --- a/man/signal-cli.1.txt +++ b/man/signal-cli.1.txt @@ -120,6 +120,8 @@ attachments are downloaded to the config directory. *-t* TIMEOUT, *--timeout* TIMEOUT:: Number of seconds to wait for new messages (negative values disable timeout). Default is 5 seconds. +*--ignore-attachments*:: + Don’t download attachments of received messages. updateGroup ~~~~~~~~~~~ @@ -179,6 +181,8 @@ libunixsocket-java ArchLinux: libmatthew-unix-java (AUR)). *--system*:: Use DBus system bus instead of user bus. +*--ignore-attachments*:: + Don’t download attachments of received messages. Examples diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index d67bfca0..7d89d480 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -375,8 +375,9 @@ public class Main { returnOnTimeout = false; timeout = 3600; } + boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); try { - m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, new ReceiveMessageHandler(m)); + m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, ignoreAttachments, new ReceiveMessageHandler(m)); } catch (IOException e) { System.err.println("Error while receiving messages: " + e.getMessage()); return 3; @@ -549,8 +550,9 @@ public class Main { e.printStackTrace(); return 2; } + ignoreAttachments = ns.getBoolean("ignore_attachments"); try { - m.receiveMessages(1, TimeUnit.HOURS, false, new DbusReceiveMessageHandler(m, conn)); + m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, new DbusReceiveMessageHandler(m, conn)); } catch (IOException e) { System.err.println("Error while receiving messages: " + e.getMessage()); return 3; @@ -722,11 +724,17 @@ public class Main { parserReceive.addArgument("-t", "--timeout") .type(double.class) .help("Number of seconds to wait for new messages (negative values disable timeout)"); + parserReceive.addArgument("--ignore-attachments") + .help("Don’t download attachments of received messages.") + .action(Arguments.storeTrue()); Subparser parserDaemon = subparsers.addParser("daemon"); parserDaemon.addArgument("--system") .action(Arguments.storeTrue()) .help("Use DBus system bus instead of user bus."); + parserDaemon.addArgument("--ignore-attachments") + .help("Don’t download attachments of received messages.") + .action(Arguments.storeTrue()); try { Namespace ns = parser.parseArgs(args); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 78b66da7..e5214302 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -883,7 +883,7 @@ class Manager implements Signal { void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e); } - private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) { + private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination, boolean ignoreAttachments) { String threadId; if (message.getGroupInfo().isPresent()) { SignalServiceGroup groupInfo = message.getGroupInfo().get(); @@ -970,7 +970,7 @@ class Manager implements Signal { threadStore.updateThread(thread); } } - if (message.getAttachments().isPresent()) { + if (message.getAttachments().isPresent() && !ignoreAttachments) { for (SignalServiceAttachment attachment : message.getAttachments().get()) { if (attachment.isPointer()) { try { @@ -983,7 +983,7 @@ class Manager implements Signal { } } - public void retryFailedReceivedMessages(ReceiveMessageHandler handler) { + public void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { final File cachePath = new File(getMessageCachePath()); if (!cachePath.exists()) { return; @@ -1014,7 +1014,7 @@ class Manager implements Signal { } catch (Exception e) { continue; } - handleMessage(envelope, content); + handleMessage(envelope, content, ignoreAttachments); } save(); handler.handleMessage(envelope, content, null); @@ -1027,8 +1027,8 @@ class Manager implements Signal { } } - public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { - retryFailedReceivedMessages(handler); + public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { + retryFailedReceivedMessages(handler, ignoreAttachments); final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); SignalServiceMessagePipe messagePipe = null; @@ -1067,7 +1067,7 @@ class Manager implements Signal { } catch (Exception e) { exception = e; } - handleMessage(envelope, content); + handleMessage(envelope, content, ignoreAttachments); } save(); handler.handleMessage(envelope, content, exception); @@ -1087,17 +1087,17 @@ class Manager implements Signal { } } - private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content) { + private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) { if (content != null) { if (content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); - handleSignalServiceDataMessage(message, false, envelope.getSource(), username); + handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments); } if (content.getSyncMessage().isPresent()) { SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { SignalServiceDataMessage message = syncMessage.getSent().get().getMessage(); - handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get()); + handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get(), ignoreAttachments); } if (syncMessage.getRequest().isPresent()) { RequestMessage rm = syncMessage.getRequest().get(); From d83e0526fbcc08b3abb355dbff9ad3d6c79a2851 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 22 Dec 2016 12:37:59 +0100 Subject: [PATCH 0179/2005] Show better error message, if libunix-java.so is not available Fixes #39 --- src/main/java/org/asamk/signal/Main.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 7d89d480..633ef469 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -88,6 +88,9 @@ public class Main { ts = (Signal) dBusConn.getRemoteObject( SIGNAL_BUSNAME, SIGNAL_OBJECTPATH, Signal.class); + } catch (UnsatisfiedLinkError e) { + System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); + return 1; } catch (DBusException e) { e.printStackTrace(); if (dBusConn != null) { @@ -350,6 +353,9 @@ public class Main { System.out.println(); } }); + } catch (UnsatisfiedLinkError e) { + System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); + return 1; } catch (DBusException e) { e.printStackTrace(); return 1; @@ -546,6 +552,9 @@ public class Main { conn = DBusConnection.getConnection(busType); conn.exportObject(SIGNAL_OBJECTPATH, m); conn.requestBusName(SIGNAL_BUSNAME); + } catch (UnsatisfiedLinkError e) { + System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); + return 1; } catch (DBusException e) { e.printStackTrace(); return 2; From 6411b09aab88b891608bdb70c1a717c550f83052 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 7 Jan 2017 17:07:56 +0100 Subject: [PATCH 0180/2005] Update dependencies --- build.gradle | 2 +- src/main/java/org/asamk/signal/Manager.java | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index d97111d2..fdb28534 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.4.2_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.4.4_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index e5214302..a272883b 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -53,6 +53,7 @@ import org.whispersystems.signalservice.api.push.exceptions.*; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; +import org.whispersystems.signalservice.internal.push.SignalServiceUrl; import java.io.*; import java.net.URI; @@ -77,6 +78,7 @@ import static java.nio.file.attribute.PosixFilePermission.*; class Manager implements Signal { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); + private final static SignalServiceUrl[] serviceUrls = new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}; public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); @@ -217,7 +219,7 @@ class Manager implements Signal { migrateLegacyConfigs(); - accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceUrls, username, password, deviceId, USER_AGENT); try { if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { refreshPreKeys(); @@ -342,7 +344,7 @@ class Manager implements Signal { public void register(boolean voiceVerification) throws IOException { password = Util.getSecret(18); - accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceUrls, username, password, USER_AGENT); if (voiceVerification) accountManager.requestVoiceVerificationCode(); @@ -356,7 +358,7 @@ class Manager implements Signal { public URI getDeviceLinkUri() throws TimeoutException, IOException { password = Util.getSecret(18); - accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceUrls, username, password, USER_AGENT); String uuid = accountManager.getNewDeviceUuid(); registered = false; @@ -783,7 +785,7 @@ class Manager implements Signal { private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); try { messageSender.sendMessage(message); @@ -800,7 +802,7 @@ class Manager implements Signal { SignalServiceDataMessage message = null; try { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); message = messageBuilder.build(); @@ -1029,7 +1031,7 @@ class Manager implements Signal { public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); SignalServiceMessagePipe messagePipe = null; try { @@ -1314,7 +1316,7 @@ class Manager implements Signal { } } - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); File tmpFile = Util.createTempFile(); try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile)) { @@ -1340,7 +1342,7 @@ class Manager implements Signal { } private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); return messageReceiver.retrieveAttachment(pointer, tmpFile); } From 1e639d18bfc984b8c4ffc27703f5c14125b0bb52 Mon Sep 17 00:00:00 2001 From: Christoph Haefner Date: Wed, 25 Jan 2017 05:57:59 +0100 Subject: [PATCH 0181/2005] Clarify to use --dbus-system if system bus is isntalled --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 984f2d67..960fe814 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ systemctl enable signal.service systemctl reload dbus.service ``` -Then just execute the send command from above, the service will be autostarted by dbus the first time it is requested. +Make sure to use "--dbus-system" with the send command, the service will be autostarted by dbus the first time it is requested. ## Storage From 39dad8b642543b4a3c3bab2318ebd187c1197878 Mon Sep 17 00:00:00 2001 From: Christoph Haefner Date: Wed, 25 Jan 2017 06:14:27 +0100 Subject: [PATCH 0182/2005] Clarify config store when using signal.service --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 960fe814..b1c3c1fc 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,9 @@ To run on the system bus you need to take some additional steps. It’s advisable to run signal-cli as a separate unix user, the following steps assume you created a user named *signal-cli*. These steps, executed as root, should work on all distributions using systemd. +Mind the fact that signal.service executes the signal-cli with "--config /var/lib/signal-cli". +If you registered with user signal-cli, remove the config option. + ```bash cp data/org.asamk.Signal.conf /etc/dbus-1/system.d/ cp data/org.asamk.Signal.service /usr/share/dbus-1/system-services/ From 568963f18791c8a98a9b44a5ad7a451945cd3e51 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 28 Jan 2017 08:40:12 +0100 Subject: [PATCH 0183/2005] Update gradle --- gradle/wrapper/gradle-wrapper.jar | Bin 54227 -> 54208 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 51288f9c2f05faf8d42e1a751a387ca7923882c3..92901afcf80bf879ad611e0b0ef83bd719d159b9 100644 GIT binary patch delta 4720 zcmZ`+2|QG77au0u*muTQvSnY(S|}-zN<>72FnFnv5;AGgiz2R&ETNEnEivTHZZMWa zWNlN3$=jPmzIz9CZ+_q7ckgwa|M@@Xoaa2xbM)n)>vPZ?N6gV%_)sWjW)vnw$}E;c zocFi%ayjg)Aqs`Ejy1C8TOd#gbg19(m0%E_hsCVHX;Rl#xb$5fCY3AEp;=%upotoPcSpMQ&E}?ueACQkPGUw>z3U&D^rBaj7(Ok>JDNbrY=2hq;m_!cVGobPMN6 z2ZoO#gI-X(&=L$?jc7A zkKhY6V&O!T-elmRhMUpSW?l;<``Yek!Tx2LfG=@amAr}5JC0-Bhb?W$k8eCLhtn()+$R(PLG!^YOLz`gN~a!~;a>uzE$~Ui6=eKQ#8GR)w3eJN3*T zV|U`6QcfeMKh-%YmqyYFI2A&gPcd?$mIXAm6ZI1G6H_rE!>J&ZFL6y(Ww*{kc6vr)YnnDZ7PVliw_GN%|B9_<5}~?CreLp zC2q&G={t(ziLQ!w!(L;0H<=c+3f-d83=>lq3L~Y;Z7=290HOcAtt{)y9kzX2HQQQB zup_>l=1uVKJdkb#*aS+bB}7k_)E2~vM4Z1>f4=NAMOizo6BM4r?;()BT&e$T+Wm0k3+Y{T@1q+WLQPl9;vH^9N7zN}F>&w5yN~T3 z=k~PyDJeCakVqLch`oq8TI~0fzZlyj9x>}cqy~hJh<4(YD-?^g_uh`til22l5!WoUe?3(j(l)e<(2TmD-k_MeO)tShy_PY4r$=-&gu%sw>Gj2 zpXGBJ>J_%}I?->Ra_3@8KG{R$F&i#Be>E&-UQ$8t;$3OI!>5KzF@u=Ur71a8GV9K= z2qJg5{<|H^g#7UzVuvWF9{M;74Y1>$pF$h!C^;8IkJJxPBkQxK)5jyiXRROjjf*U1 zhuKnvIkD5*#=>9mu5 z*mm!P_A)y=_oRz$3l>fK3p{7KPN;7(*k|S@BYtxeQV^wZedm=6t) z-3QRV`m;-U(Y?m?uH^v~{1!ql=KXhRE%d%%J(&gatedlLwKBFnO_?DnzurkHd^TXt zxUdpClNT5Nz76|c)$+oF0;`mPfT%bs|UKkrfWxzx3kSs21|-T;Sdn13i$ zSnY6?;8iNts*wNQz9=V`!dRIRrS^A8r}Jf}g_iGyzQ!25n}k}cBb(lmHD$c|a3y=! z*->^WGL`v)W<1_%Z1JCAuB$Z$Mm}f0F?j|XS{q>VnshnNypG|(8EJD&vZN)&V$&Ll zIho=t86w<8@mWFx>CGN;CfcPKo+J<4!z}zME0^b2xB&qA5`(IZ!F_pv*nf>sP+5#+r@((`9`HZBa2& zb=7fhWVKvFy<+IA?~=7j@?hVS#A#!1XIx#>C3@XmO=!*Srq&r2^daLC4_6Hao<2SJ zp*E$|th=gZd6iW>QtW-nejh7M1xZqca%x- z5n^WZr+&L~Tf0>4O z(>g}K`n?Blxe_tY;rZ-p*zBOw$_SZFn^^G1>)|3aYr0q zASZS5$IfW0k|2U^bkJNm0KY6$15ih^4_YC2wA)4Cc?u`k`Thfxo2T;JP`EUgdeJ0X0XRg z$Wu21bH6qK4_9%bqPem{R|jQ5vD3m#xopQvO-8WCV`6^ux-^^@gmHt<5pz0vKGg5F z5VEF+Uf^I^`@r57Cf2)^4;drVop^zEa{4TH-)DSj4fLgN-JS(aBLW?iJ0JS*(%*sJY8ohz! z=B&&G*(c6{{QV%mGJ@Y(o@VrKvp6D1u@)%EWR-P?`4LJ#nlen8;;UU*UoJLgj=Pp zP(X!_>i{g*pAG?RZ4koR87xKBO^f>Gdbg7pI2R6lq!4qexdQC2G6*{85Cnd<2^d4b z7}vGkw!U{Ik3E#T(Z<_$ge8C$A`bYftmXNY@`$ z2zv0KP|XT|o+8SCdY1~E|Mz_eZr|VJ;BSL*H2z0jTj6*BrGGaB3~(9rNf&`%N(QK1 zMu-*KNr3|EOra44Q^esb1f-JqNg#;?ST#xrB3GcEvK_&*O5?$20n{wy2?zyybPtPY zE=cqZJXLlgh<4}GCZ5R`*}Q(W-H>a>MKr|cA`!%vfoPTaJ+ClBS*ZMxrULNWUny)N*a6*xCnC1Uv&PPZ+T2`*nyy2>>qlK_!j??)6D4 otX=;b{R6x;0NYL2uzU{qxMBs4m@|Okpit`I57a>9J%^;z=Y7wa>U?~6KAz9g6wkU0f;c&$!jQln ziG0#47A4Ky0a5x81lc9(*=UW4ef0a_#{w-9Xn+8;5DxhO4=Y%{eg~BCOE8Ho#AUyX zB8dxt}}w`>?&3qXP~Fl4%i}SVvNWj2tNzjOS}GY zfAXk#(T4DAr_$|?-@g7)d8(l<*4A05>PcoKh4J!Z%HTJpLEp(Aql41L(9@XdqZB=R z`QBVdrJY_+R~oVEh=+_sXPY|BP>ZZx*!T+OX*r*YMich%6^%wm?pk)~VZ0Sw0@}UVBKBRI=_# za-aWox%7d6>j$VtMl&=$r}vS^yUhBos;7>B&}kN<2p6Wla zk6U7KvWk}Kw4w4!BFot4OZYOI znP>M8eM!r@^4^*et?O=Os_-V{^fmqVbx+kczD~={?J}LzN-j4we3v&-Lg~wDZ{B~a z`&f(qnBc({9!}h3V+Vh;Iq5;~6xHsd(w5|)%+SW^ypephtA`b1ZL&vzpfhhT+}Wxi9W2V=D%<4lNQ;Y>+{ljIN>Je z0{5?(_rWl^WeL5Yo*pS2>d6CoF#GWm9!r>Ih!V?VjOR z_r|0Lt<{eQJ4Cc2zj|;zimtU?Yu9#4<@AMqh3W3^xN@bO$n&2oQ~2&8RzUUQoqFdsn2;_lP#z6WEi!4a^3Dg{Sd9JI)AOKdhT^;N`YYx^|3J}T*_m? zV)+60b5iV&UOHZt?dH;Gbz}2LajX~fYeaWxH-71gFn)91k!qXn8)`)Bh)u@IyZ3xX9am(nQI(vSn>eXR&!ezI( z9eY;AX`6LBW^1vn-O=8s1@c2P5jox$ zL;DWT8B9dgg@114GHnm;yeLO0zvgVSd9su;uTCqdmYYoK^DFxqs8V zxqatfk={5DgbL;y|IEME+h>+#bdasa5qFF83HjzbT9~fa!PSN_f-%EVKeA;sN$Rqi zBm)oa!PsvNhUy;p@8=5G8D%@R&uN&;Xq}}x{4_kMZD&Tiy}r!@XS*`{KZl3CwUVOE zXmhx-!phg$g^!(2u$+S(dUfR;%QLpUx|yC*Iaz3IWT+L$rC8=I>y%dTU5s%5v3KiW z?nK=2_uQ1rjHVwccjsa&bO`3#*Bmj@N{Kd?5j^7ZbEE#S?b)>}V=i3&H?qgY<+4r4 z9#5)-56$q3AI^LH{j1~Mk^#*8X+d!;t(fQ>iW_dhOH zo1+e0vJ|{7a>t89(T2kF-rVazhHY7!nzifr6IBTX>3Fpsqcj${n9Z|kzXj!4``Ay2 zV{+4Z#$-kKKU4Buv&(MGvLBw!+I6whBKF=9>N$9Jr~8MMfg`>X4|N+<>zrg`d~$;K zUe~i*RpPk8HU8kM0rG>x`azaNBL`)Xn;rf5j)MV7GiC52h18qkSyFicf*D<QuG z`^bAXC*`p7PYV&}25;fxWlW^Q+QCNPuuKhm^Hrdjz zokfg==5`kNJcuJl6bm+k;DSevsuEKA$HgGcVx6cl&KT>-ruzKk7uvFsJEB ze!nLEW9+NH{NTWLvs`{L9J>36N!VlL5JE32TE9nj83$VSA*ryTWVm2`KT4#>DWAod zlU6h5Me74~tyZAr&Rw7vE;c{H0({kbs!H0oHeL?Jk#Tb#PFVGCc43J=s#X|G@S^7{pz-A^4HhHw@W(Z=SE zJ)}4Pu)LUU>OCQ$Ve-&jV()oZ#Xhe%gIyCky~eJBjd%6#3Tceel|E52JzNK2yCIfR zc8<=z*tttDLX8J{zxAn0H@1XC{dBUL)i9S*eE+eZTv)+LMU4ooNLX;DoBZ!o5s}{F z*0+!ZQ!e%##|iz~3PhF-Y21^<20>}Oa6~Sc0d8{BTMy+d>^+ll+&2$x0bb+xa^<3d z49ed2d=?10iiaRg_(LE87JxbE+x=@;&`}5kG-2ddfp!>qdvNhWJurRFhi<^5`CG!X zm!sv^Nur!+nKQi+V{afM4#Pi6`;N|m9+ca*P;X3sJBwy9daIjSn0S(Jn`877r7tn^ z)ABKl+C|Q`0aD2c>I4NoL|e!ju75Ka5h5(?x= z^SMEiC@%!P5Q88h#$p#Je(=vjTY^(pa zgdCo4aCPHKv}L1C&xs^}%>Z|d`X4A#?+WfP(VqmbX%a-^UHG?lyEw*}Sv%sXod#2B>lK&25XWGj)C-_a>IIj+_8$0Q`=7fMNp{yZEJD8i5Kj1w>U0iFA)yB-&k) zXeW~1BDY!rb^tihz~HOyGo2K438V3t@c@G#t^x(6zO(@Of7aR~eSJAt(il9Y29~TL N!iC)e?3Fc$=D$uhE=&Lb diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 05ab972d..129f9851 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Nov 29 10:59:02 CET 2016 +#Tue Jan 17 21:20:45 CET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip From 7ccfa2674693b36b96b759d274944a7da3ab1167 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 28 Jan 2017 08:52:51 +0100 Subject: [PATCH 0184/2005] Update dependencies --- build.gradle | 2 +- src/main/java/org/asamk/signal/Manager.java | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index fdb28534..ddad876a 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.4.4_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.4.7_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index a272883b..ab1f1191 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -110,6 +110,7 @@ class Manager implements Signal { private JsonGroupStore groupStore; private JsonContactsStore contactStore; private JsonThreadStore threadStore; + private SignalServiceMessagePipe messagePipe = null; public Manager(String username, String settingsPath) { this.username = username; @@ -786,7 +787,7 @@ class Manager implements Signal { private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password, - deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); + deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.absent()); try { messageSender.sendMessage(message); } catch (UntrustedIdentityException e) { @@ -803,7 +804,7 @@ class Manager implements Signal { SignalServiceDataMessage message = null; try { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password, - deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); + deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.absent()); message = messageBuilder.build(); if (message.getGroupInfo().isPresent()) { @@ -1032,10 +1033,11 @@ class Manager implements Signal { public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); - SignalServiceMessagePipe messagePipe = null; try { - messagePipe = messageReceiver.createMessagePipe(); + if (messagePipe == null) { + messagePipe = messageReceiver.createMessagePipe(); + } while (true) { SignalServiceEnvelope envelope; @@ -1084,8 +1086,10 @@ class Manager implements Signal { } } } finally { - if (messagePipe != null) + if (messagePipe != null) { messagePipe.shutdown(); + messagePipe = null; + } } } From b4e34961393fc9bea90bc997e2316be6e779d3d8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 28 Jan 2017 10:48:55 +0100 Subject: [PATCH 0185/2005] Update README, man page Add note about country calling code --- README.md | 2 ++ man/signal-cli.1.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index b1c3c1fc..3e354013 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-sys See also: [man page in asciidoc format](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.txt) +The USERNAME (your phone number) must include the country calling code, i.e. the number must start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list of all country codes. + * Register a number (with SMS verification) signal-cli -u USERNAME register diff --git a/man/signal-cli.1.txt b/man/signal-cli.1.txt index c4800d61..ddf855fd 100644 --- a/man/signal-cli.1.txt +++ b/man/signal-cli.1.txt @@ -39,6 +39,8 @@ Options *-u* USERNAME, *--username* USERNAME:: Specify your phone number, that will be your identifier. + The phone number must include the country calling code, i.e. the number must + start with a "+" sign. *--dbus*:: Make request via user dbus. From e083a20a7e07881e22722dcb3a549abf93d16a90 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 29 Jan 2017 14:11:05 +0100 Subject: [PATCH 0186/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index ddad876a..e482f24e 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.5.2' +version = '0.5.3' compileJava.options.encoding = 'UTF-8' From fc888a3d89ccfa4db55ff74739748c23d22d47e8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Feb 2017 13:26:47 +0100 Subject: [PATCH 0187/2005] Update dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e482f24e..883adb7c 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.4.7_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.4.7_unofficial_2' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' From d0e8bf6b446c69cabec40d79035d1900c0eda075 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 17 Feb 2017 20:39:29 +0100 Subject: [PATCH 0188/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 883adb7c..7d39c9f4 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.5.3' +version = '0.5.4' compileJava.options.encoding = 'UTF-8' From 92c1f16799cfa6a53e3512848cf31f54ac86e90d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 18 Feb 2017 10:48:26 +0100 Subject: [PATCH 0189/2005] Update dependency Fixes #56 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7d39c9f4..016be693 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.4.7_unofficial_2' + compile 'com.github.turasa:signal-service-java:2.4.7_unofficial_3' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' From be963ed49b6b32ab90707c453f8ce2c85b72e428 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 18 Feb 2017 12:01:55 +0100 Subject: [PATCH 0190/2005] Add unregister command Fixes #57 --- man/signal-cli.1.txt | 7 +++++++ src/main/java/org/asamk/signal/Main.java | 19 +++++++++++++++++++ src/main/java/org/asamk/signal/Manager.java | 7 +++++++ 3 files changed, 33 insertions(+) diff --git a/man/signal-cli.1.txt b/man/signal-cli.1.txt index ddf855fd..79df4d18 100644 --- a/man/signal-cli.1.txt +++ b/man/signal-cli.1.txt @@ -66,6 +66,13 @@ Verify the number using the code received via SMS or voice. VERIFICATIONCODE:: The verification code. +unregister +~~~~~~~~ +Disable push support for this device, i.e. this device won't receive any more messages. +If this is the master device, other users can't send messages to this number anymore. +Use "updateAccount" to undo this. +To remove a linked device, use "removeDevice" from the master device. + link ~~~~ Link to an existing device, instead of registering a new number. This shows a diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 633ef469..ff590307 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -138,6 +138,22 @@ public class Main { return 3; } break; + case "unregister": + if (dBusConn != null) { + System.err.println("unregister is not yet implemented via dbus"); + return 1; + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + m.unregister(); + } catch (IOException e) { + System.err.println("Unregister error: " + e.getMessage()); + return 3; + } + break; case "verify": if (dBusConn != null) { System.err.println("verify is not yet implemented via dbus"); @@ -679,6 +695,9 @@ public class Main { .help("The verification should be done over voice, not sms.") .action(Arguments.storeTrue()); + Subparser parserUnregister = subparsers.addParser("unregister"); + parserUnregister.help("Unregister the current device from the signal server."); + Subparser parserVerify = subparsers.addParser("verify"); parserVerify.addArgument("verificationCode") .help("The verification code you received via sms or voice call."); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index ab1f1191..a6dfcae5 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -356,6 +356,13 @@ class Manager implements Signal { save(); } + public void unregister() throws IOException { + // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. + // If this is the master device, other users can't send messages to this number anymore. + // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. + accountManager.setGcmId(Optional.absent()); + } + public URI getDeviceLinkUri() throws TimeoutException, IOException { password = Util.getSecret(18); From 0a68303ca448f69a5655beb23f4213187dbce0b4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 18 Feb 2017 12:03:17 +0100 Subject: [PATCH 0191/2005] Add command to update account attributes This can fix problems with receiving messages, if for some reason, the fetchesMessages property of the server is set incorrectly. --- man/signal-cli.1.txt | 5 +++++ src/main/java/org/asamk/signal/Main.java | 19 +++++++++++++++++++ src/main/java/org/asamk/signal/Manager.java | 4 ++++ 3 files changed, 28 insertions(+) diff --git a/man/signal-cli.1.txt b/man/signal-cli.1.txt index 79df4d18..a3ccf347 100644 --- a/man/signal-cli.1.txt +++ b/man/signal-cli.1.txt @@ -73,6 +73,11 @@ If this is the master device, other users can't send messages to this number any Use "updateAccount" to undo this. To remove a linked device, use "removeDevice" from the master device. +updateAccount +~~~~~~~~ +Update the account attributes on the signal server. +Can fix problems with receiving messages. + link ~~~~ Link to an existing device, instead of registering a new number. This shows a diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index ff590307..2377e125 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -154,6 +154,22 @@ public class Main { return 3; } break; + case "updateAccount": + if (dBusConn != null) { + System.err.println("updateAccount is not yet implemented via dbus"); + return 1; + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + m.updateAccountAttributes(); + } catch (IOException e) { + System.err.println("UpdateAccount error: " + e.getMessage()); + return 3; + } + break; case "verify": if (dBusConn != null) { System.err.println("verify is not yet implemented via dbus"); @@ -698,6 +714,9 @@ public class Main { Subparser parserUnregister = subparsers.addParser("unregister"); parserUnregister.help("Unregister the current device from the signal server."); + Subparser parserUpdateAccount = subparsers.addParser("updateAccount"); + parserUpdateAccount.help("Update the account attributes on the signal server."); + Subparser parserVerify = subparsers.addParser("verify"); parserVerify.addArgument("verificationCode") .help("The verification code you received via sms or voice call."); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index a6dfcae5..d51e8b57 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -356,6 +356,10 @@ class Manager implements Signal { save(); } + public void updateAccountAttributes() throws IOException { + accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), false, true); + } + public void unregister() throws IOException { // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // If this is the master device, other users can't send messages to this number anymore. From 74f21c4f18ba7fde79e32e36bff28fe3cf828bfe Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 18 Feb 2017 12:35:49 +0100 Subject: [PATCH 0192/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 016be693..d9478372 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.5.4' +version = '0.5.5' compileJava.options.encoding = 'UTF-8' From c68cfe7d7c15eef53ae047e80672282e546759dd Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 18 Feb 2017 13:15:02 +0100 Subject: [PATCH 0193/2005] Update dependency --- build.gradle | 2 +- src/main/java/org/asamk/signal/Manager.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index d9478372..9d3b2b4f 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.4.7_unofficial_3' + compile 'com.github.turasa:signal-service-java:2.5.0_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index d51e8b57..578565ef 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -357,7 +357,7 @@ class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), false, true); + accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false,true); } public void unregister() throws IOException { @@ -507,7 +507,7 @@ class Manager implements Signal { public void verifyAccount(String verificationCode) throws IOException { verificationCode = verificationCode.replace("-", ""); signalingKey = Util.getSecret(52); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, true); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false,true); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; From 8e8de2fe3922f3e06c0ccd24213fb7c2e7fbfce9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 19 Feb 2017 17:17:55 +0100 Subject: [PATCH 0194/2005] Rename man page to show it rendered on github --- README.md | 2 +- man/Makefile | 2 +- man/{signal-cli.1.txt => signal-cli.1.adoc} | 7 +++---- 3 files changed, 5 insertions(+), 6 deletions(-) rename man/{signal-cli.1.txt => signal-cli.1.adoc} (99%) diff --git a/README.md b/README.md index 3e354013..5526a9e6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-system] {link,addDevice,listDevices,removeDevice,register,verify,send,quitGroup,updateGroup,listIdentities,trust,receive,daemon} ... -See also: [man page in asciidoc format](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.txt) +See also: [man page in asciidoc format](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.adoc) The USERNAME (your phone number) must include the country calling code, i.e. the number must start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list of all country codes. diff --git a/man/Makefile b/man/Makefile index 47ccd35f..adbe7394 100644 --- a/man/Makefile +++ b/man/Makefile @@ -5,6 +5,6 @@ MANPAGESRC = signal-cli.1 .PHONY: all all: $(MANPAGESRC) -%: %.txt +%: %.adoc @echo "Generating manpage for $@" $(A2X) --no-xmllint --doctype manpage --format manpage "$^" diff --git a/man/signal-cli.1.txt b/man/signal-cli.1.adoc similarity index 99% rename from man/signal-cli.1.txt rename to man/signal-cli.1.adoc index a3ccf347..d4a85104 100644 --- a/man/signal-cli.1.txt +++ b/man/signal-cli.1.adoc @@ -3,8 +3,7 @@ vim:set ts=4 sw=4 tw=82 noet: ///// :quotes.~: -signal-cli (1) -============ += signal-cli (1) Name ---- @@ -67,14 +66,14 @@ VERIFICATIONCODE:: The verification code. unregister -~~~~~~~~ +~~~~~~~~~~ Disable push support for this device, i.e. this device won't receive any more messages. If this is the master device, other users can't send messages to this number anymore. Use "updateAccount" to undo this. To remove a linked device, use "removeDevice" from the master device. updateAccount -~~~~~~~~ +~~~~~~~~~~~~~ Update the account attributes on the signal server. Can fix problems with receiving messages. From 8e7fce54b97f56602951b0deb1d5da8dbc4bf898 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Sun, 19 Feb 2017 16:36:23 +0100 Subject: [PATCH 0195/2005] Add eclipse plugin to build.gradle. This allows easy generation of eclipse project files using `./gradlew eclipse` --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 9d3b2b4f..346c13e7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'java' apply plugin: 'application' +apply plugin: 'eclipse' sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 From 453f31891ca8958f7c936cab61861e78ffc635b4 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Sun, 19 Feb 2017 17:14:02 +0100 Subject: [PATCH 0196/2005] Modify gitignore to include eclipse settings --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index e98305a3..96f13754 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ build/ *.swp *.iml local.properties +.classpath +.project +.settings/ + From b0d7daeca202690bc9ad9a2259e910eb1f192824 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Mon, 20 Feb 2017 14:28:41 +0100 Subject: [PATCH 0197/2005] Add ListGroups command Option: -d/--detailed to display group members --- src/main/java/org/asamk/signal/Main.java | 32 +++++++++++++++++++++ src/main/java/org/asamk/signal/Manager.java | 4 +++ 2 files changed, 36 insertions(+) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 2377e125..a0059824 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -491,6 +491,22 @@ public class Main { return 3; } + break; + case "listGroups": + if (dBusConn != null) { + System.err.println("listGroups is not yet implemented via dbus"); + return 1; + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + List groups = m.getGroups(); + + for (GroupInfo group : groups) { + printGroup(group, ns.getBoolean("detailed")); + } break; case "listIdentities": if (dBusConn != null) { @@ -622,6 +638,17 @@ public class Main { System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, theirId.trustLevel, theirId.added, Hex.toStringCondensed(theirId.getFingerprint()), digits)); } + + private static void printGroup(GroupInfo group, boolean detailed) { + System.out.println(String.format("Group id: %s\n Group name: %s \n active: %s", + Base64.encodeBytes(group.groupId), group.name, group.active)); + if (detailed) { + System.out.println(" Members:"); + for (String member : group.members) { + System.out.println(" " + member); + } + } + } private static String formatSafetyNumber(String digits) { final int partCount = 12; @@ -751,6 +778,11 @@ public class Main { parserUpdateGroup.addArgument("-m", "--member") .nargs("*") .help("Specify one or more members to add to the group"); + + Subparser parserListGroups = subparsers.addParser("listGroups"); + parserListGroups.addArgument("-d", "--detailed").action(Arguments.storeTrue()) + .help("List members of each group"); + parserListGroups.help("List group name and ids"); Subparser parserListIdentities = subparsers.addParser("listIdentities"); parserListIdentities.addArgument("-n", "--number") diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 578565ef..2b2d1d70 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -580,6 +580,10 @@ class Manager implements Signal { } throw new NotAGroupMemberException(groupId, g.name); } + + public List getGroups() { + return groupStore.getGroups(); + } @Override public void sendGroupMessage(String messageText, List attachments, From 5845dad7690da09c696649544a1f396b0a01f080 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Tue, 21 Feb 2017 09:48:58 +0100 Subject: [PATCH 0198/2005] Whitespace and output formatting fixes --- src/main/java/org/asamk/signal/Main.java | 14 +++++++------- src/main/java/org/asamk/signal/Manager.java | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index a0059824..b18b2e0a 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -503,9 +503,10 @@ public class Main { } List groups = m.getGroups(); + boolean detailed = ns.getBoolean("detailed"); for (GroupInfo group : groups) { - printGroup(group, ns.getBoolean("detailed")); + printGroup(group, detailed); } break; case "listIdentities": @@ -640,13 +641,12 @@ public class Main { } private static void printGroup(GroupInfo group, boolean detailed) { - System.out.println(String.format("Group id: %s\n Group name: %s \n active: %s", - Base64.encodeBytes(group.groupId), group.name, group.active)); if (detailed) { - System.out.println(" Members:"); - for (String member : group.members) { - System.out.println(" " + member); - } + System.out.println(String.format("Id: %s Name: %s Active: %s Members: %s", + Base64.encodeBytes(group.groupId), group.name, group.active, group.members)); + } else { + System.out.println(String.format("Id: %s Name: %s Active: %s", Base64.encodeBytes(group.groupId), + group.name, group.active)); } } diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 2b2d1d70..a50cb481 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -582,7 +582,7 @@ class Manager implements Signal { } public List getGroups() { - return groupStore.getGroups(); + return groupStore.getGroups(); } @Override From 8cd782ef942a2bfd69d6fa56fda5d7b7b3923627 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 22 Feb 2017 21:22:40 +0100 Subject: [PATCH 0199/2005] Add listGroups command to man page --- man/signal-cli.1.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index d4a85104..cdd56d62 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -160,6 +160,12 @@ Send a quit group message to all group members and remove self from member list. *-g* GROUP, *--group* GROUP:: Specify the recipient group ID in base64 encoding. +listGroups +~~~~~~~~~~~ +Show a list of known groups. + +*-d*, *--detailed*:: + Include the list of members of each group. listIdentities ~~~~~~~~~~~~~~ From 6f2e8716c71ec9763ec7ca123b0c01c1f5dd63fe Mon Sep 17 00:00:00 2001 From: Finn Date: Wed, 22 Feb 2017 12:26:34 -0800 Subject: [PATCH 0200/2005] Allow retreving and updating group info and contact names via dbus (#62) * dbus method to get contact info * Add getGroupName method * Save after updating contact name * allow group updates over dbus * Allow retreiving group member list as well * Space after if before conditions if( -> if ( * Return an empty string if the contact is unknown * Handle null/non-existant groups better * Remove debug output and allow updating the avatar * Remove extra variables in update messages --- src/main/java/org/asamk/Signal.java | 10 ++++ src/main/java/org/asamk/signal/Manager.java | 59 +++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 02fc22dd..79bf0172 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -19,6 +19,16 @@ public interface Signal extends DBusInterface { void sendGroupMessage(String message, List attachments, byte[] groupId) throws EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, IOException; + String getContactName(String number); + + void setContactName(String number, String name); + + String getGroupName(byte[] groupId); + + List getGroupMembers(byte[] groupId); + + void updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException; + class MessageReceived extends DBusSignal { private long timestamp; private String sender; diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index a50cb481..4a5d4680 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -779,6 +779,65 @@ class Manager implements Signal { sendMessage(messageBuilder, recipients); } + @Override + public String getContactName(String number) { + ContactInfo contact = contactStore.getContact(number); + if (contact == null) { + return ""; + } else { + return contact.name; + } + } + + @Override + public void setContactName(String number, String name) { + ContactInfo contact = contactStore.getContact(number); + if (contact == null) { + contact = new ContactInfo(); + contact.number = number; + System.out.println("Add contact " + number + " named " + name); + } else { + System.out.println("Updating contact " + number + " name " + contact.name + " -> " + name); + } + contact.name = name; + contactStore.updateContact(contact); + save(); + } + + @Override + public String getGroupName(byte[] groupId) { + GroupInfo group = getGroup(groupId); + if (group == null) { + return ""; + } else { + return group.name; + } + } + + @Override + public List getGroupMembers(byte[] groupId) { + GroupInfo group = getGroup(groupId); + if (group == null) { + return new ArrayList(); + } else { + return new ArrayList(group.members); + } + } + + @Override + public void updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + if (name.isEmpty()) { + name = null; + } + if (members.size() == 0) { + members = null; + } + if (avatar.isEmpty()) { + avatar = null; + } + sendUpdateGroupMessage(groupId, name, members, avatar); + } + private void requestSyncGroups() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); From 0744dcccf117064dcf6c8dbb1312565c0f3e15cb Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 25 Feb 2017 18:25:31 +0100 Subject: [PATCH 0201/2005] Move information from README to wiki/man page --- README.md | 123 ++++-------------------------------------- man/signal-cli.1.adoc | 1 + 2 files changed, 10 insertions(+), 114 deletions(-) diff --git a/README.md b/README.md index 5526a9e6..d95b2601 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # signal-cli -signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. To be able to receive messages signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java [does not yet support registering for the websocket support](https://github.com/WhisperSystems/libsignal-service-java/pull/5) nor [provisioning as a slave device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For registering you need a phone number where you can receive SMS or incoming calls. -It is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings. +signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. +To be able to link to an existing Signal-Android/signal-cli instance, signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java does not yet support [provisioning as a slave device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). +For registering you need a phone number where you can receive SMS or incoming calls. +signal-cli is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface, that can be used to send messages from any programming language that has dbus bindings. ## Installation @@ -18,136 +20,29 @@ sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ ## Usage -usage: signal-cli [-h] [-v] [--config CONFIG] [-u USERNAME | --dbus | --dbus-system] {link,addDevice,listDevices,removeDevice,register,verify,send,quitGroup,updateGroup,listIdentities,trust,receive,daemon} ... - -See also: [man page in asciidoc format](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.adoc) - -The USERNAME (your phone number) must include the country calling code, i.e. the number must start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list of all country codes. +Important: The USERNAME (your phone number) must include the country calling code, i.e. the number must start with a "+" sign. (See [Wikipedia](https://en.wikipedia.org/wiki/List_of_country_calling_codes) for a list of all country codes. * Register a number (with SMS verification) signal-cli -u USERNAME register -* Register a number (with voice verification) - - signal-cli -u USERNAME register -v - * Verify the number using the code received via SMS or voice signal-cli -u USERNAME verify CODE -* Send a message to one or more recipients +* Send a message - signal-cli -u USERNAME send -m "This is a message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] + signal-cli -u USERNAME send -m "This is a message" RECIPIENT * Pipe the message content from another process. - uname -a | signal-cli -u USERNAME send [RECIPIENT [RECIPIENT ...]] + uname -a | signal-cli -u USERNAME send RECIPIENT * Receive messages signal-cli -u USERNAME receive -* Groups - - * Create a group - - signal-cli -u USERNAME updateGroup -n "Group name" -m [MEMBER [MEMBER ...]] - - * Update a group - - signal-cli -u USERNAME updateGroup -g GROUP_ID -n "New group name" -a "AVATAR_IMAGE_FILE" - - * Add member to a group - - signal-cli -u USERNAME updateGroup -g GROUP_ID -m "NEW_MEMBER" - - * Leave a group - - signal-cli -u USERNAME quitGroup -g GROUP_ID - - * Send a message to a group - - signal-cli -u USERNAME send -m "This is a message" -g GROUP_ID - -* Linking other devices (Provisioning) - - * Connect to another device - - signal-cli link -n "optional device name" - - This shows a "tsdevice:/…" link, if you want to connect to another signal-cli instance, you can just use this link. If you want to link to and Android device, create a QR code with the link (e.g. with [qrencode](https://fukuchi.org/works/qrencode/)) and scan that in the Signal Android app. - - * Add another device - - signal-cli -u USERNAME addDevice --uri "tsdevice:/…" - - The "tsdevice:/…" link is the one shown by the new signal-cli instance or contained in the QR code shown in Signal-Desktop or similar apps. - Only the master device (that was registered directly, not linked) can add new devices. - - * Manage linked devices - - signal-cli -u USERNAME listDevices - - signal-cli -u USERNAME removeDevice -d DEVICE_ID - -* Manage trusted keys - - * View all known keys - - signal-cli -u USERNAME listIdentities - - * View known keys of one number - - signal-cli -u USERNAME listIdentities -n NUMBER - - * Trust new key, after having verified it - - signal-cli -u USERNAME trust -v FINGER_PRINT NUMBER - - * Trust new key, without having verified it. Only use this if you don't care about security - - signal-cli -u USERNAME trust -a NUMBER - -* Set configuration directory - - signal-cli --config=/home/other_user/.config/signal - - This is particularily useful in the case, when you would like to run the signal-cli tool as a different user as the one, that was used to register the account. You should make sure, that the caller has full read/write access to the given directory. - -## DBus service - -signal-cli can run in daemon mode and provides an experimental dbus interface. -For dbus support you need jni/unix-java.so installed on your system (Debian: libunixsocket-java ArchLinux: libmatthew-unix-java (AUR)). - -* Run in daemon mode (dbus session bus) - - signal-cli -u USERNAME daemon - -* Send a message via dbus - - signal-cli --dbus send -m "Message" [RECIPIENT [RECIPIENT ...]] [-a [ATTACHMENT [ATTACHMENT ...]]] - -### System bus - -To run on the system bus you need to take some additional steps. -It’s advisable to run signal-cli as a separate unix user, the following steps assume you created a user named *signal-cli*. -These steps, executed as root, should work on all distributions using systemd. - -Mind the fact that signal.service executes the signal-cli with "--config /var/lib/signal-cli". -If you registered with user signal-cli, remove the config option. - -```bash -cp data/org.asamk.Signal.conf /etc/dbus-1/system.d/ -cp data/org.asamk.Signal.service /usr/share/dbus-1/system-services/ -cp data/signal.service /etc/systemd/system/ -sed -i -e "s|%dir%||" -e "s|%number%||" /etc/systemd/system/signal.service -systemctl daemon-reload -systemctl enable signal.service -systemctl reload dbus.service -``` - -Make sure to use "--dbus-system" with the send command, the service will be autostarted by dbus the first time it is requested. +For more information read the [man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli.1.adoc) and the [wiki](https://github.com/AsamK/signal-cli/wiki). ## Storage diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index cdd56d62..f1e7fca1 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -34,6 +34,7 @@ Options *--config* CONFIG:: Set the path, where to store the config. + Make sure you have full read/write access to the given directory. (Default: $HOME/.config/signal) *-u* USERNAME, *--username* USERNAME:: From d91e20e1f8a8626ce8e5d5f11535dc05f3c63f31 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Thu, 23 Feb 2017 16:48:10 +0100 Subject: [PATCH 0202/2005] Moving files to packages --- .../java/org/asamk/signal/{ => storage/contacts}/ContactInfo.java | 0 .../asamk/signal/{ => storage/contacts}/JsonContactsStore.java | 0 .../java/org/asamk/signal/{ => storage/groups}/GroupInfo.java | 0 .../org/asamk/signal/{ => storage/groups}/JsonGroupStore.java | 0 .../asamk/signal/{ => storage/protocol}/JsonIdentityKeyStore.java | 0 .../org/asamk/signal/{ => storage/protocol}/JsonPreKeyStore.java | 0 .../org/asamk/signal/{ => storage/protocol}/JsonSessionStore.java | 0 .../signal/{ => storage/protocol}/JsonSignalProtocolStore.java | 0 .../signal/{ => storage/protocol}/JsonSignedPreKeyStore.java | 0 .../org/asamk/signal/{ => storage/thread}/JsonThreadStore.java | 0 .../java/org/asamk/signal/{ => storage/thread}/ThreadInfo.java | 0 src/main/java/org/asamk/signal/{ => util}/Base64.java | 0 src/main/java/org/asamk/signal/{ => util}/Hex.java | 0 src/main/java/org/asamk/signal/{ => util}/Util.java | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename src/main/java/org/asamk/signal/{ => storage/contacts}/ContactInfo.java (100%) rename src/main/java/org/asamk/signal/{ => storage/contacts}/JsonContactsStore.java (100%) rename src/main/java/org/asamk/signal/{ => storage/groups}/GroupInfo.java (100%) rename src/main/java/org/asamk/signal/{ => storage/groups}/JsonGroupStore.java (100%) rename src/main/java/org/asamk/signal/{ => storage/protocol}/JsonIdentityKeyStore.java (100%) rename src/main/java/org/asamk/signal/{ => storage/protocol}/JsonPreKeyStore.java (100%) rename src/main/java/org/asamk/signal/{ => storage/protocol}/JsonSessionStore.java (100%) rename src/main/java/org/asamk/signal/{ => storage/protocol}/JsonSignalProtocolStore.java (100%) rename src/main/java/org/asamk/signal/{ => storage/protocol}/JsonSignedPreKeyStore.java (100%) rename src/main/java/org/asamk/signal/{ => storage/thread}/JsonThreadStore.java (100%) rename src/main/java/org/asamk/signal/{ => storage/thread}/ThreadInfo.java (100%) rename src/main/java/org/asamk/signal/{ => util}/Base64.java (100%) rename src/main/java/org/asamk/signal/{ => util}/Hex.java (100%) rename src/main/java/org/asamk/signal/{ => util}/Util.java (100%) diff --git a/src/main/java/org/asamk/signal/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java similarity index 100% rename from src/main/java/org/asamk/signal/ContactInfo.java rename to src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java diff --git a/src/main/java/org/asamk/signal/JsonContactsStore.java b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java similarity index 100% rename from src/main/java/org/asamk/signal/JsonContactsStore.java rename to src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java diff --git a/src/main/java/org/asamk/signal/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java similarity index 100% rename from src/main/java/org/asamk/signal/GroupInfo.java rename to src/main/java/org/asamk/signal/storage/groups/GroupInfo.java diff --git a/src/main/java/org/asamk/signal/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java similarity index 100% rename from src/main/java/org/asamk/signal/JsonGroupStore.java rename to src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java diff --git a/src/main/java/org/asamk/signal/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java similarity index 100% rename from src/main/java/org/asamk/signal/JsonIdentityKeyStore.java rename to src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java diff --git a/src/main/java/org/asamk/signal/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java similarity index 100% rename from src/main/java/org/asamk/signal/JsonPreKeyStore.java rename to src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java diff --git a/src/main/java/org/asamk/signal/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java similarity index 100% rename from src/main/java/org/asamk/signal/JsonSessionStore.java rename to src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java diff --git a/src/main/java/org/asamk/signal/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java similarity index 100% rename from src/main/java/org/asamk/signal/JsonSignalProtocolStore.java rename to src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java diff --git a/src/main/java/org/asamk/signal/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java similarity index 100% rename from src/main/java/org/asamk/signal/JsonSignedPreKeyStore.java rename to src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java diff --git a/src/main/java/org/asamk/signal/JsonThreadStore.java b/src/main/java/org/asamk/signal/storage/thread/JsonThreadStore.java similarity index 100% rename from src/main/java/org/asamk/signal/JsonThreadStore.java rename to src/main/java/org/asamk/signal/storage/thread/JsonThreadStore.java diff --git a/src/main/java/org/asamk/signal/ThreadInfo.java b/src/main/java/org/asamk/signal/storage/thread/ThreadInfo.java similarity index 100% rename from src/main/java/org/asamk/signal/ThreadInfo.java rename to src/main/java/org/asamk/signal/storage/thread/ThreadInfo.java diff --git a/src/main/java/org/asamk/signal/Base64.java b/src/main/java/org/asamk/signal/util/Base64.java similarity index 100% rename from src/main/java/org/asamk/signal/Base64.java rename to src/main/java/org/asamk/signal/util/Base64.java diff --git a/src/main/java/org/asamk/signal/Hex.java b/src/main/java/org/asamk/signal/util/Hex.java similarity index 100% rename from src/main/java/org/asamk/signal/Hex.java rename to src/main/java/org/asamk/signal/util/Hex.java diff --git a/src/main/java/org/asamk/signal/Util.java b/src/main/java/org/asamk/signal/util/Util.java similarity index 100% rename from src/main/java/org/asamk/signal/Util.java rename to src/main/java/org/asamk/signal/util/Util.java From a5aeec8902b04f46cc9a3366f7d21ee38ae7de52 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Thu, 23 Feb 2017 16:49:14 +0100 Subject: [PATCH 0203/2005] Modifying methods to be public to match package division --- .../asamk/signal/GroupNotFoundException.java | 1 + src/main/java/org/asamk/signal/Main.java | 9 +- src/main/java/org/asamk/signal/Manager.java | 136 +++++++++++++----- .../signal/NotAGroupMemberException.java | 1 + .../signal/storage/contacts/ContactInfo.java | 2 +- .../storage/contacts/JsonContactsStore.java | 8 +- .../signal/storage/groups/GroupInfo.java | 2 +- .../signal/storage/groups/JsonGroupStore.java | 10 +- .../protocol/JsonIdentityKeyStore.java | 19 ++- .../storage/protocol/JsonPreKeyStore.java | 4 +- .../storage/protocol/JsonSessionStore.java | 4 +- .../protocol/JsonSignalProtocolStore.java | 6 +- .../protocol/JsonSignedPreKeyStore.java | 4 +- .../storage/thread/JsonThreadStore.java | 8 +- .../signal/storage/thread/ThreadInfo.java | 2 +- .../java/org/asamk/signal/util/Base64.java | 2 +- src/main/java/org/asamk/signal/util/Hex.java | 2 +- src/main/java/org/asamk/signal/util/Util.java | 4 +- 18 files changed, 158 insertions(+), 66 deletions(-) diff --git a/src/main/java/org/asamk/signal/GroupNotFoundException.java b/src/main/java/org/asamk/signal/GroupNotFoundException.java index 0218c508..d9b51fa0 100644 --- a/src/main/java/org/asamk/signal/GroupNotFoundException.java +++ b/src/main/java/org/asamk/signal/GroupNotFoundException.java @@ -1,5 +1,6 @@ package org.asamk.signal; +import org.asamk.signal.util.Base64; import org.freedesktop.dbus.exceptions.DBusExecutionException; public class GroupNotFoundException extends DBusExecutionException { diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index b18b2e0a..571f9d42 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -21,6 +21,11 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; import org.apache.http.util.TextUtils; import org.asamk.Signal; +import org.asamk.signal.storage.contacts.ContactInfo; +import org.asamk.signal.storage.groups.GroupInfo; +import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; +import org.asamk.signal.util.Base64; +import org.asamk.signal.util.Hex; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.DBusSigHandler; import org.freedesktop.dbus.exceptions.DBusException; @@ -635,9 +640,9 @@ public class Main { } private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) { - String digits = formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.identityKey)); + String digits = formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey())); System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, - theirId.trustLevel, theirId.added, Hex.toStringCondensed(theirId.getFingerprint()), digits)); + theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits)); } private static void printGroup(GroupInfo group, boolean detailed) { diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 4a5d4680..ce8ee00c 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -16,18 +16,70 @@ */ package org.asamk.signal; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.node.ObjectNode; +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidObjectException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + import org.apache.http.util.TextUtils; import org.asamk.Signal; -import org.whispersystems.libsignal.*; +import org.asamk.signal.storage.contacts.ContactInfo; +import org.asamk.signal.storage.contacts.JsonContactsStore; +import org.asamk.signal.storage.groups.GroupInfo; +import org.asamk.signal.storage.groups.JsonGroupStore; +import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; +import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; +import org.asamk.signal.storage.thread.JsonThreadStore; +import org.asamk.signal.storage.thread.ThreadInfo; +import org.asamk.signal.util.Base64; +import org.asamk.signal.util.Util; +import org.whispersystems.libsignal.DuplicateMessageException; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.InvalidKeyIdException; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.InvalidVersionException; +import org.whispersystems.libsignal.LegacyMessageException; +import org.whispersystems.libsignal.NoSessionException; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; @@ -44,36 +96,44 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.*; -import org.whispersystems.signalservice.api.messages.multidevice.*; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; +import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; -import org.whispersystems.signalservice.api.push.exceptions.*; +import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; +import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceUrl; -import java.io.*; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.*; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import static java.nio.file.attribute.PosixFilePermission.*; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; class Manager implements Signal { private final static String URL = "https://textsecure-service.whispersystems.org"; @@ -1532,11 +1592,11 @@ class Manager implements Signal { return false; } for (JsonIdentityKeyStore.Identity id : ids) { - if (!Arrays.equals(id.identityKey.serialize(), fingerprint)) { + if (!Arrays.equals(id.getIdentityKey().serialize(), fingerprint)) { continue; } - signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED); + signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); save(); return true; } @@ -1555,11 +1615,11 @@ class Manager implements Signal { return false; } for (JsonIdentityKeyStore.Identity id : ids) { - if (!safetyNumber.equals(computeSafetyNumber(name, id.identityKey))) { + if (!safetyNumber.equals(computeSafetyNumber(name, id.getIdentityKey()))) { continue; } - signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED); + signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); save(); return true; } @@ -1577,8 +1637,8 @@ class Manager implements Signal { return false; } for (JsonIdentityKeyStore.Identity id : ids) { - if (id.trustLevel == TrustLevel.UNTRUSTED) { - signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_UNVERIFIED); + if (id.getTrustLevel() == TrustLevel.UNTRUSTED) { + signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); } } save(); diff --git a/src/main/java/org/asamk/signal/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/NotAGroupMemberException.java index 52ba4238..d525d7e9 100644 --- a/src/main/java/org/asamk/signal/NotAGroupMemberException.java +++ b/src/main/java/org/asamk/signal/NotAGroupMemberException.java @@ -1,5 +1,6 @@ package org.asamk.signal; +import org.asamk.signal.util.Base64; import org.freedesktop.dbus.exceptions.DBusExecutionException; public class NotAGroupMemberException extends DBusExecutionException { diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java index c607238f..f66792b2 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java +++ b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.storage.contacts; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java index 500684fe..702f78e3 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.storage.contacts; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; @@ -21,15 +21,15 @@ public class JsonContactsStore { private static final ObjectMapper jsonProcessor = new ObjectMapper(); - void updateContact(ContactInfo contact) { + public void updateContact(ContactInfo contact) { contacts.put(contact.number, contact); } - ContactInfo getContact(String number) { + public ContactInfo getContact(String number) { return contacts.get(number); } - List getContacts() { + public List getContacts() { return new ArrayList<>(contacts.values()); } diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index 610e8f9f..e28a5921 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.storage.groups; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index aa7e3a45..b5b0e54e 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.storage.groups; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; @@ -13,6 +13,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.asamk.signal.util.Base64; + public class JsonGroupStore { @JsonProperty("groups") @JsonSerialize(using = JsonGroupStore.MapToListSerializer.class) @@ -23,16 +25,16 @@ public class JsonGroupStore { private static final ObjectMapper jsonProcessor = new ObjectMapper(); - void updateGroup(GroupInfo group) { + public void updateGroup(GroupInfo group) { groups.put(Base64.encodeBytes(group.groupId), group); } - GroupInfo getGroup(byte[] groupId) { + public GroupInfo getGroup(byte[] groupId) { GroupInfo g = groups.get(Base64.encodeBytes(groupId)); return g; } - List getGroups() { + public List getGroups() { return new ArrayList<>(groups.values()); } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index d71e3581..bda7b817 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -1,9 +1,12 @@ -package org.asamk.signal; +package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; + +import org.asamk.signal.TrustLevel; +import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; @@ -13,7 +16,7 @@ import org.whispersystems.libsignal.state.IdentityKeyStore; import java.io.IOException; import java.util.*; -class JsonIdentityKeyStore implements IdentityKeyStore { +public class JsonIdentityKeyStore implements IdentityKeyStore { private final Map> trustedKeys = new HashMap<>(); @@ -177,6 +180,18 @@ class JsonIdentityKeyStore implements IdentityKeyStore { return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; } + + public IdentityKey getIdentityKey() { + return this.identityKey; + } + + public TrustLevel getTrustLevel() { + return this.trustLevel; + } + + public Date getDateAdded() { + return this.added; + } public byte[] getFingerprint() { return identityKey.getPublicKey().serialize(); diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java index d4c8d521..9be6cca3 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java @@ -1,9 +1,11 @@ -package org.asamk.signal; +package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; + +import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.PreKeyStore; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index cd4d55ad..c8377ada 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -1,9 +1,11 @@ -package org.asamk.signal; +package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; + +import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionStore; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 79f49c7f..71a5e4b9 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -1,8 +1,10 @@ -package org.asamk.signal; +package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import org.asamk.signal.TrustLevel; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyIdException; @@ -15,7 +17,7 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; import java.util.List; import java.util.Map; -class JsonSignalProtocolStore implements SignalProtocolStore { +public class JsonSignalProtocolStore implements SignalProtocolStore { @JsonProperty("preKeys") @JsonDeserialize(using = JsonPreKeyStore.JsonPreKeyStoreDeserializer.class) diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java index cdcd506b..e1ff1228 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java @@ -1,9 +1,11 @@ -package org.asamk.signal; +package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; + +import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; diff --git a/src/main/java/org/asamk/signal/storage/thread/JsonThreadStore.java b/src/main/java/org/asamk/signal/storage/thread/JsonThreadStore.java index 3a8eb830..ce77a15f 100644 --- a/src/main/java/org/asamk/signal/storage/thread/JsonThreadStore.java +++ b/src/main/java/org/asamk/signal/storage/thread/JsonThreadStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.storage.thread; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; @@ -21,15 +21,15 @@ public class JsonThreadStore { private static final ObjectMapper jsonProcessor = new ObjectMapper(); - void updateThread(ThreadInfo thread) { + public void updateThread(ThreadInfo thread) { threads.put(thread.id, thread); } - ThreadInfo getThread(String id) { + public ThreadInfo getThread(String id) { return threads.get(id); } - List getThreads() { + public List getThreads() { return new ArrayList<>(threads.values()); } diff --git a/src/main/java/org/asamk/signal/storage/thread/ThreadInfo.java b/src/main/java/org/asamk/signal/storage/thread/ThreadInfo.java index a664059b..da94f219 100644 --- a/src/main/java/org/asamk/signal/storage/thread/ThreadInfo.java +++ b/src/main/java/org/asamk/signal/storage/thread/ThreadInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.storage.thread; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/org/asamk/signal/util/Base64.java b/src/main/java/org/asamk/signal/util/Base64.java index 517bb7dd..d5e523b1 100644 --- a/src/main/java/org/asamk/signal/util/Base64.java +++ b/src/main/java/org/asamk/signal/util/Base64.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.util; /** *

Encodes and decodes to and from Base64 notation.

diff --git a/src/main/java/org/asamk/signal/util/Hex.java b/src/main/java/org/asamk/signal/util/Hex.java index 696ca62b..623c5cf8 100644 --- a/src/main/java/org/asamk/signal/util/Hex.java +++ b/src/main/java/org/asamk/signal/util/Hex.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.util; public class Hex { diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 4eeabf1c..679e1384 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -1,11 +1,11 @@ -package org.asamk.signal; +package org.asamk.signal.util; import java.io.File; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -class Util { +public class Util { public static String getSecret(int size) { byte[] secret = getSecretBytes(size); return Base64.encodeBytes(secret); From b1ec1e65878a9cc5bceb83d1c0b30306559aea8c Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Sat, 25 Feb 2017 19:15:59 +0100 Subject: [PATCH 0204/2005] Rename thread package to threads --- src/main/java/org/asamk/signal/Manager.java | 4 ++-- .../signal/storage/{thread => threads}/JsonThreadStore.java | 2 +- .../asamk/signal/storage/{thread => threads}/ThreadInfo.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/main/java/org/asamk/signal/storage/{thread => threads}/JsonThreadStore.java (97%) rename src/main/java/org/asamk/signal/storage/{thread => threads}/ThreadInfo.java (81%) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index ce8ee00c..8b2477e7 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -67,8 +67,8 @@ import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; -import org.asamk.signal.storage.thread.JsonThreadStore; -import org.asamk.signal.storage.thread.ThreadInfo; +import org.asamk.signal.storage.threads.JsonThreadStore; +import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.Base64; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.DuplicateMessageException; diff --git a/src/main/java/org/asamk/signal/storage/thread/JsonThreadStore.java b/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java similarity index 97% rename from src/main/java/org/asamk/signal/storage/thread/JsonThreadStore.java rename to src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java index ce77a15f..b32d629d 100644 --- a/src/main/java/org/asamk/signal/storage/thread/JsonThreadStore.java +++ b/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.thread; +package org.asamk.signal.storage.threads; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; diff --git a/src/main/java/org/asamk/signal/storage/thread/ThreadInfo.java b/src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java similarity index 81% rename from src/main/java/org/asamk/signal/storage/thread/ThreadInfo.java rename to src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java index da94f219..3fc28405 100644 --- a/src/main/java/org/asamk/signal/storage/thread/ThreadInfo.java +++ b/src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java @@ -1,4 +1,4 @@ -package org.asamk.signal.storage.thread; +package org.asamk.signal.storage.threads; import com.fasterxml.jackson.annotation.JsonProperty; From 4730e9cbc7e440b8ca975fa4713a6e15fe0597c7 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Sat, 25 Feb 2017 19:21:31 +0100 Subject: [PATCH 0205/2005] Re-order imports to match original structure --- src/main/java/org/asamk/signal/Manager.java | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 8b2477e7..b20ca002 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -20,6 +20,16 @@ import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; + import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; @@ -125,16 +135,6 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceUrl; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.node.ObjectNode; - class Manager implements Signal { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); From 0e16594a874f5480361b56c0592fa6468b8066c3 Mon Sep 17 00:00:00 2001 From: Pim Otte Date: Sat, 25 Feb 2017 19:46:48 +0100 Subject: [PATCH 0206/2005] Actual re-order (I'm a dummy) --- src/main/java/org/asamk/signal/Main.java | 28 ++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 571f9d42..b07db4a2 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -18,7 +18,13 @@ package org.asamk.signal; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; -import net.sourceforge.argparse4j.inf.*; +import net.sourceforge.argparse4j.inf.ArgumentParser; +import net.sourceforge.argparse4j.inf.ArgumentParserException; +import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import net.sourceforge.argparse4j.inf.Subparsers; + import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.asamk.signal.storage.contacts.ContactInfo; @@ -32,8 +38,17 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.*; -import org.whispersystems.signalservice.api.messages.multidevice.*; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; +import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; @@ -50,7 +65,12 @@ import java.nio.charset.Charset; import java.security.Security; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; From 474372da6b56942638b7b870eb748a0a8f269cfe Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 26 Feb 2017 11:29:25 +0100 Subject: [PATCH 0207/2005] Reformat --- .idea/codeStyleSettings.xml | 15 +++ src/main/java/org/asamk/signal/Main.java | 36 ++---- src/main/java/org/asamk/signal/Manager.java | 104 +++++------------- .../signal/storage/groups/JsonGroupStore.java | 3 +- .../protocol/JsonIdentityKeyStore.java | 7 +- .../storage/protocol/JsonPreKeyStore.java | 1 - .../storage/protocol/JsonSessionStore.java | 1 - .../protocol/JsonSignalProtocolStore.java | 1 - .../protocol/JsonSignedPreKeyStore.java | 1 - 9 files changed, 54 insertions(+), 115 deletions(-) create mode 100644 .idea/codeStyleSettings.xml diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 00000000..f62dae59 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index b07db4a2..f9084516 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -18,13 +18,7 @@ package org.asamk.signal; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; -import net.sourceforge.argparse4j.inf.ArgumentParser; -import net.sourceforge.argparse4j.inf.ArgumentParserException; -import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup; -import net.sourceforge.argparse4j.inf.Namespace; -import net.sourceforge.argparse4j.inf.Subparser; -import net.sourceforge.argparse4j.inf.Subparsers; - +import net.sourceforge.argparse4j.inf.*; import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.asamk.signal.storage.contacts.ContactInfo; @@ -38,17 +32,8 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; -import org.whispersystems.signalservice.api.messages.SignalServiceContent; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; -import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; -import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.multidevice.*; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; @@ -65,12 +50,7 @@ import java.nio.charset.Charset; import java.security.Security; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -526,10 +506,10 @@ public class Main { System.err.println("User is not registered."); return 1; } - + List groups = m.getGroups(); boolean detailed = ns.getBoolean("detailed"); - + for (GroupInfo group : groups) { printGroup(group, detailed); } @@ -664,7 +644,7 @@ public class Main { System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits)); } - + private static void printGroup(GroupInfo group, boolean detailed) { if (detailed) { System.out.println(String.format("Id: %s Name: %s Active: %s Members: %s", @@ -803,7 +783,7 @@ public class Main { parserUpdateGroup.addArgument("-m", "--member") .nargs("*") .help("Specify one or more members to add to the group"); - + Subparser parserListGroups = subparsers.addParser("listGroups"); parserListGroups.addArgument("-d", "--detailed").action(Arguments.storeTrue()) .help("List members of each group"); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index b20ca002..e817c0d0 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -16,10 +16,6 @@ */ package org.asamk.signal; -import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; -import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; -import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; - import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonGenerator; @@ -29,46 +25,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InvalidObjectException; -import java.io.OutputStream; -import java.io.RandomAccessFile; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.net.URLEncoder; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.asamk.signal.storage.contacts.ContactInfo; @@ -81,15 +37,7 @@ import org.asamk.signal.storage.threads.JsonThreadStore; import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.Base64; import org.asamk.signal.util.Util; -import org.whispersystems.libsignal.DuplicateMessageException; -import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.InvalidKeyIdException; -import org.whispersystems.libsignal.InvalidMessageException; -import org.whispersystems.libsignal.InvalidVersionException; -import org.whispersystems.libsignal.LegacyMessageException; -import org.whispersystems.libsignal.NoSessionException; +import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; @@ -106,35 +54,37 @@ import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; -import org.whispersystems.signalservice.api.messages.SignalServiceContent; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; -import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.multidevice.*; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; -import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; -import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; -import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; -import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.push.exceptions.*; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceUrl; +import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static java.nio.file.attribute.PosixFilePermission.*; + class Manager implements Signal { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); @@ -417,7 +367,7 @@ class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false,true); + accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false, true); } public void unregister() throws IOException { @@ -567,7 +517,7 @@ class Manager implements Signal { public void verifyAccount(String verificationCode) throws IOException { verificationCode = verificationCode.replace("-", ""); signalingKey = Util.getSecret(52); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false,true); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false, true); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; @@ -640,7 +590,7 @@ class Manager implements Signal { } throw new NotAGroupMemberException(groupId, g.name); } - + public List getGroups() { return groupStore.getGroups(); } diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index b5b0e54e..1f019e3b 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.asamk.signal.util.Base64; import java.io.IOException; import java.util.ArrayList; @@ -13,8 +14,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import org.asamk.signal.util.Base64; - public class JsonGroupStore { @JsonProperty("groups") @JsonSerialize(using = JsonGroupStore.MapToListSerializer.class) diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index bda7b817..5d8e0ea3 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; - import org.asamk.signal.TrustLevel; import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.IdentityKey; @@ -180,15 +179,15 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; } - + public IdentityKey getIdentityKey() { return this.identityKey; } - + public TrustLevel getTrustLevel() { return this.trustLevel; } - + public Date getDateAdded() { return this.added; } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java index 9be6cca3..2f1bad96 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; - import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.PreKeyRecord; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index c8377ada..4290e157 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; - import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.SessionRecord; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 71a5e4b9..8f8f3e73 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -3,7 +3,6 @@ package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; - import org.asamk.signal.TrustLevel; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java index e1ff1228..d6123495 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java @@ -4,7 +4,6 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; - import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.SignedPreKeyRecord; From 3c3d3e92dd68c381d3e03b6447a73614e6a86ff4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 1 Apr 2017 12:46:33 +0200 Subject: [PATCH 0208/2005] Update dependencies, add attachment filename support Fixes #76 --- build.gradle | 2 +- src/main/java/org/asamk/signal/Main.java | 1 + src/main/java/org/asamk/signal/Manager.java | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 346c13e7..7440b5fc 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.5.0_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.5.5_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index f9084516..c8963659 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -1038,6 +1038,7 @@ public class Main { if (attachment.isPointer()) { final SignalServiceAttachmentPointer pointer = attachment.asPointer(); System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); + System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-")); System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); File file = m.getAttachmentFile(pointer.getId()); if (file.exists()) { diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index e817c0d0..575bba22 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -96,6 +96,7 @@ class Manager implements Signal { private final static int PREKEY_MINIMUM_COUNT = 20; private static final int PREKEY_BATCH_SIZE = 100; + private static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; private final String settingsPath; private final String dataPath; @@ -557,7 +558,7 @@ class Manager implements Signal { if (mime == null) { mime = "application/octet-stream"; } - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), null); } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -1407,7 +1408,7 @@ class Manager implements Signal { final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); File tmpFile = Util.createTempFile(); - try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile)) { + try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) { try (OutputStream output = new FileOutputStream(outputFile)) { byte[] buffer = new byte[4096]; int read; @@ -1431,7 +1432,7 @@ class Manager implements Signal { private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); - return messageReceiver.retrieveAttachment(pointer, tmpFile); + return messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE); } private String canonicalizeNumber(String number) throws InvalidNumberException { From debcabd01442c028eb645f94f0510a8d235a416b Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 7 May 2017 10:31:18 +0200 Subject: [PATCH 0209/2005] Implement updateGroup command via dbus Fixes #77 --- src/main/java/org/asamk/Signal.java | 2 +- src/main/java/org/asamk/signal/Main.java | 23 +++++++++++++++------ src/main/java/org/asamk/signal/Manager.java | 7 +++++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 79bf0172..9eb9b4cd 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -27,7 +27,7 @@ public interface Signal extends DBusInterface { List getGroupMembers(byte[] groupId); - void updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException; + byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException; class MessageReceived extends DBusSignal { private long timestamp; diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index c8963659..8a85f3d3 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -460,11 +460,7 @@ public class Main { break; case "updateGroup": - if (dBusConn != null) { - System.err.println("updateGroup is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { + if (dBusConn == null && !m.isRegistered()) { System.err.println("User is not registered."); return 1; } @@ -474,8 +470,23 @@ public class Main { if (ns.getString("group") != null) { groupId = decodeGroupId(ns.getString("group")); } - byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.getList("member"), ns.getString("avatar")); if (groupId == null) { + groupId = new byte[0]; + } + String groupName = ns.getString("name"); + if (groupName == null) { + groupName = ""; + } + List groupMembers = ns.getList("member"); + if (groupMembers == null) { + groupMembers = new ArrayList(); + } + String groupAvatar = ns.getString("avatar"); + if (groupAvatar == null) { + groupAvatar = ""; + } + byte[] newGroupId = ts.updateGroup(groupId, groupName, groupMembers, groupAvatar); + if (groupId.length != newGroupId.length) { System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …"); } } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 575bba22..e1c9f29a 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -836,7 +836,10 @@ class Manager implements Signal { } @Override - public void updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + if (groupId.length == 0) { + groupId = null; + } if (name.isEmpty()) { name = null; } @@ -846,7 +849,7 @@ class Manager implements Signal { if (avatar.isEmpty()) { avatar = null; } - sendUpdateGroupMessage(groupId, name, members, avatar); + return sendUpdateGroupMessage(groupId, name, members, avatar); } private void requestSyncGroups() throws IOException { From 78518e0898d0132aed74ee0de366b08fd6b60482 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 7 May 2017 11:20:07 +0200 Subject: [PATCH 0210/2005] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 54208 -> 54212 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 92901afcf80bf879ad611e0b0ef83bd719d159b9..0da4609c7121e284bfc262f636c03338a7cc8679 100644 GIT binary patch delta 2084 zcmZ9Ne@t6d6vuC03+;dcD}w;91zI9d*!U@KV;~X~4HyK$ZEpPN)`-Eefe;u|qWFh@ zkeTq~EbfP(C@Kpi;Kl?cB(PF`Q`%yTbIT@d&M{exOrvhj%=6A;jK@n}?#Z{G^S$T3 zbMNg)r`*~pSLbBO6RulT953H;qhPS`bCEe<`jG-iF&D^hqD!lJ zt1$M>m=`z-dP-I}PgF7uZ%yrMabfJV8s6*hg^C{cK3nNo7lLZbyNQOd%G;X-xV5an z-YH}3OF3gZV5LR}-k|JzwrWyFyjQhT3~tmEia~m9zk+^xP`67?K~nu$4Fxw_f>jhK zKXQohmew>A*|n-A=>4rfpTs4dxY)4on9vLuB%6BO8HrAqzvv7dc$1Nh*Z zlO^M}u(dq`-&&rOZW6d}NzBD=Wg^UQ9(ok3qd2tUP+s|Z%zCel8Savd5vU&x6#5En zqdd3F;8Uv~I7Zz`x`tAa405f$;BD0h3r@H0?5*lThb#CRBu-I~V~r!`;6J5NORkIs zdNLL!VSJ(8niwMZm@oZy=1pY0Js3-nFlic^utF08kz*cI$IdaHlLugF21pI?ho)8e z6O`3F(aF3X8k{068|S$rI+ELTI>;FpI*aXgHsbDQ9~ z)@adpLQB$qlW%Rb3ZqqAz5kQZU(MLH0LJu^Rrth<`Me6QPiP2sJsf;Shn>D- zWGqAizmbZtP?jsiPHN~I4m6mSKXYR&(jQ+5JO&3QcT!PplRVd3Nb}ey!)T6Kbx}q$ z?u~o+X}3wzcAE(FwkqKRlMgISd(b>L-gguSA*jVSRFV)6E{<8fSU#QVo`)1<#{if=ngVhF#A$;eiaqBeqMD zzoQnKhXX)si>0|=vhmzX9hK8)izI0XrI>n@4Dhdw?k@12Nm8Z>6+_5*G-!>nSzM&U3G|!@KR#)O+Du@2@>_>I61tV)Mpt zeF!ev6DjQ;rloh2TYWbVWpm$61&*}0=>TRTWix&sQT{n*rqH@h?Bs>gVdAc z`A~Az9`ZY9L+A>+ZkK?s43X>Ec`bpGn_B`N9md8btaaGy;piT`p_vBXUa~At{56d6Xc;OPSASG0W8xF`r(+iWi&r-$cwQMU&f+gr+F4wTlG!reMY^Azt;aE z4`u|bMM6TPcX+V(#a}%LdE4lxrobAz)Kd0`RnvO9$Y{BHxD|`0XN*z9L{|{y!bKw z1qSc`yKX+7I81bU_Sc6U_kU?TeM&0MG3&?~wNu$xM`g z@bTco_-_S{R@=6PQ@y4ud=!ccK#Rb;3-DZP7)@`3HwL?5ao74SbCXa;%of*;)e(J2+XTdlLh zcm7t6#AI*A5)|YDh`eGV!($CWqd(!XT4IL}PLL#&R?|qQ}cmVfJM{5<{Wi86=->c;b}A z?N-vBu+9NziXAY9H}S&-dbD=_x0im_!1OHUZ+SWU9HPDvCCA)+t| zZglFXlDQtKx?q%Tlgn1fuI;8uW)vA@GU@MkO2QKGa;1^S1FM&Dy0kkZXFuIrXWOBEcbS^&dN4%H!)UxSblhPvslg=Yw@ zY#?!R&s@r~%027ueaupVmr;@;%c@2su9(Q+?vcdOQ>1M>Xvlk>&h3vzmQ> zm5HY4=#_**80<}y2W_Z=xW&A(t CGIZ4d diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 129f9851..6ac8ac49 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Jan 17 21:20:45 CET 2017 +#Sun May 07 11:19:00 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip From d270ea8c2eb406d0169ea114cfa60c0fc55c2229 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 20 May 2017 11:26:37 +0200 Subject: [PATCH 0211/2005] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 54212 -> 54783 bytes gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0da4609c7121e284bfc262f636c03338a7cc8679..88731cec4ecf6571ec00dc24d749961d2d3347b4 100644 GIT binary patch delta 6729 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 delta 6082 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6ac8ac49..29edf730 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun May 07 11:19:00 CEST 2017 +#Sat May 20 11:26:11 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-bin.zip From 0f1d0597ae73fc68cbf0579104840be0839deb53 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 20 May 2017 11:24:58 +0200 Subject: [PATCH 0212/2005] Update dependencies Sets the complete flag for contacts message, Fixes #81 --- build.gradle | 2 +- src/main/java/org/asamk/signal/Main.java | 10 ++++++++-- src/main/java/org/asamk/signal/Manager.java | 11 ++++++++--- .../signal/storage/contacts/JsonContactsStore.java | 7 +++++++ .../storage/protocol/JsonIdentityKeyStore.java | 12 +++++++----- .../storage/protocol/JsonSignalProtocolStore.java | 13 +++++-------- 6 files changed, 36 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index 7440b5fc..5749fbab 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.5.5_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.5.7_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 8a85f3d3..79174549 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -938,8 +938,13 @@ public class Main { SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getContacts().isPresent()) { - System.out.println("Received sync contacts"); - printAttachment(syncMessage.getContacts().get()); + final ContactsMessage contactsMessage = syncMessage.getContacts().get(); + if (contactsMessage.isComplete()) { + System.out.println("Received complete sync contacts"); + } else { + System.out.println("Received sync contacts"); + } + printAttachment(contactsMessage.getContactsStream()); } if (syncMessage.getGroups().isPresent()) { System.out.println("Received sync groups"); @@ -1051,6 +1056,7 @@ public class Main { System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-")); System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); + System.out.println(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no")); File file = m.getAttachmentFile(pointer.getId()); if (file.exists()) { System.out.println(" Stored plaintext in: " + file); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index e1c9f29a..a38d313f 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -558,7 +558,8 @@ class Manager implements Signal { if (mime == null) { mime = "application/octet-stream"; } - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), null); + // TODO mabybe add a parameter to set the voiceNote and preview option + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, Optional.absent(),null); } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -1251,7 +1252,11 @@ class Manager implements Signal { File tmpFile = null; try { tmpFile = Util.createTempFile(); - DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer(), tmpFile)); + final ContactsMessage contactsMessage = syncMessage.getContacts().get(); + DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)); + if (contactsMessage.isComplete()) { + contactStore.clear(); + } DeviceContact c; while ((c = s.read()) != null) { ContactInfo contact = contactStore.getContact(c.getNumber()); @@ -1506,7 +1511,7 @@ class Manager implements Signal { .withLength(contactsFile.length()) .build(); - sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream)); + sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, true))); } } } finally { diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java index 702f78e3..2024b86d 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java @@ -33,6 +33,13 @@ public class JsonContactsStore { return new ArrayList<>(contacts.values()); } + /** + * Remove all contacts from the store + */ + public void clear() { + contacts.clear(); + } + public static class MapToListSerializer extends JsonSerializer> { @Override public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index 5d8e0ea3..16209d77 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -39,8 +39,8 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { } @Override - public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { - saveIdentity(address.getName(), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); + public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { + return saveIdentity(address.getName(), identityKey, TrustLevel.TRUSTED_UNVERIFIED, null); } /** @@ -51,7 +51,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { * @param trustLevel * @param added Added timestamp, if null and the key is newly added, the current time is used. */ - public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel, Date added) { + public boolean saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel, Date added) { List identities = trustedKeys.get(name); if (identities == null) { identities = new ArrayList<>(); @@ -67,14 +67,16 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { if (added != null) { id.added = added; } - return; + return true; } } identities.add(new Identity(identityKey, trustLevel, added != null ? added : new Date())); + return false; } @Override - public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) { + public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { + // TODO implement possibility for different handling of incoming/outgoing trust decisions List identities = trustedKeys.get(address.getName()); if (identities == null) { // Trust on first use diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 8f8f3e73..885fdfb3 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -8,10 +8,7 @@ import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SessionRecord; -import org.whispersystems.libsignal.state.SignalProtocolStore; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.libsignal.state.*; import java.util.List; import java.util.Map; @@ -66,8 +63,8 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { } @Override - public void saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { - identityKeyStore.saveIdentity(address, identityKey); + public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) { + return identityKeyStore.saveIdentity(address, identityKey); } public void saveIdentity(String name, IdentityKey identityKey, TrustLevel trustLevel) { @@ -83,8 +80,8 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { } @Override - public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey) { - return identityKeyStore.isTrustedIdentity(address, identityKey); + public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { + return identityKeyStore.isTrustedIdentity(address, identityKey, direction); } @Override From d90015da0bdb4210ca02199f583f5ae1519a061d Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 9 Jun 2017 21:56:21 +0200 Subject: [PATCH 0213/2005] Update dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5749fbab..f1f1ebd3 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.5.7_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.5.10_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' From 23fcd8a23024cb92ef330662177302adaeb9c87c Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 11 Jun 2017 16:27:00 +0200 Subject: [PATCH 0214/2005] Update dependency --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f1f1ebd3..8bacf3b0 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.5.10_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.5.11_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' From 4377a2179b38360b00efc1c1db00a62f3f020db1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 11 Jun 2017 16:28:03 +0200 Subject: [PATCH 0215/2005] Send and receive verified messages Fixes #85 --- src/main/java/org/asamk/signal/Main.java | 10 ++++ src/main/java/org/asamk/signal/Manager.java | 46 ++++++++++++++++++- .../java/org/asamk/signal/TrustLevel.java | 26 +++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 79174549..a9097fc9 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -992,6 +992,16 @@ public class Main { System.out.println(" - " + number); } } + if (syncMessage.getVerified().isPresent()) { + System.out.println("Received sync message with verified identities:"); + final List verifiedList = syncMessage.getVerified().get(); + for (VerifiedMessage v : verifiedList) { + System.out.println(" - " + v.getDestination() + ": " + v.getVerified()); + String safetyNumber = formatSafetyNumber(m.computeSafetyNumber(v.getDestination(), v.getIdentityKey())); + System.out.println(" " + safetyNumber); + } + + } } } } else { diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index a38d313f..c9252f83 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -96,7 +96,7 @@ class Manager implements Signal { private final static int PREKEY_MINIMUM_COUNT = 20; private static final int PREKEY_BATCH_SIZE = 100; - private static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; + private static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; private final String settingsPath; private final String dataPath; @@ -559,7 +559,7 @@ class Manager implements Signal { mime = "application/octet-stream"; } // TODO mabybe add a parameter to set the voiceNote and preview option - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, Optional.absent(),null); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, Optional.absent(), null); } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -1199,6 +1199,7 @@ class Manager implements Signal { if (rm.isContactsRequest()) { try { sendContacts(); + sendVerifiedMessage(); } catch (UntrustedIdentityException | IOException e) { e.printStackTrace(); } @@ -1288,6 +1289,12 @@ class Manager implements Signal { } } } + if (syncMessage.getVerified().isPresent()) { + final List verifiedList = syncMessage.getVerified().get(); + for (VerifiedMessage v : verifiedList) { + signalProtocolStore.saveIdentity(v.getDestination(), v.getIdentityKey(), TrustLevel.fromVerifiedState(v.getVerified())); + } + } } } } @@ -1523,6 +1530,26 @@ class Manager implements Signal { } } + private void sendVerifiedMessage() throws IOException, UntrustedIdentityException { + List verifiedMessages = new LinkedList<>(); + for (Map.Entry> x : getIdentities().entrySet()) { + final String name = x.getKey(); + for (JsonIdentityKeyStore.Identity id : x.getValue()) { + if (id.getTrustLevel() == TrustLevel.TRUSTED_UNVERIFIED) { + continue; + } + VerifiedMessage verifiedMessage = new VerifiedMessage(name, id.getIdentityKey(), id.getTrustLevel().toVerifiedState()); + verifiedMessages.add(verifiedMessage); + } + } + sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessages)); + } + + private void sendVerifiedMessage(String destination, IdentityKey identityKey, TrustLevel trustLevel) throws IOException, UntrustedIdentityException { + VerifiedMessage verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState()); + sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); + } + public ContactInfo getContact(String number) { return contactStore.getContact(number); } @@ -1556,6 +1583,11 @@ class Manager implements Signal { } signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + try { + sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + } catch (IOException | UntrustedIdentityException e) { + e.printStackTrace(); + } save(); return true; } @@ -1579,6 +1611,11 @@ class Manager implements Signal { } signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + try { + sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + } catch (IOException | UntrustedIdentityException e) { + e.printStackTrace(); + } save(); return true; } @@ -1598,6 +1635,11 @@ class Manager implements Signal { for (JsonIdentityKeyStore.Identity id : ids) { if (id.getTrustLevel() == TrustLevel.UNTRUSTED) { signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + try { + sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + } catch (IOException | UntrustedIdentityException e) { + e.printStackTrace(); + } } } save(); diff --git a/src/main/java/org/asamk/signal/TrustLevel.java b/src/main/java/org/asamk/signal/TrustLevel.java index e9e7796d..5eaf960a 100644 --- a/src/main/java/org/asamk/signal/TrustLevel.java +++ b/src/main/java/org/asamk/signal/TrustLevel.java @@ -1,5 +1,7 @@ package org.asamk.signal; +import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; + public enum TrustLevel { UNTRUSTED, TRUSTED_UNVERIFIED, @@ -13,4 +15,28 @@ public enum TrustLevel { } return TrustLevel.cachedValues[i]; } + + public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) { + switch (verifiedState) { + case DEFAULT: + return TRUSTED_UNVERIFIED; + case UNVERIFIED: + return UNTRUSTED; + case VERIFIED: + return TRUSTED_VERIFIED; + } + throw new RuntimeException("Unknown verified state: " + verifiedState); + } + + public VerifiedMessage.VerifiedState toVerifiedState() { + switch (this) { + case TRUSTED_UNVERIFIED: + return VerifiedMessage.VerifiedState.DEFAULT; + case UNTRUSTED: + return VerifiedMessage.VerifiedState.UNVERIFIED; + case TRUSTED_VERIFIED: + return VerifiedMessage.VerifiedState.VERIFIED; + } + throw new RuntimeException("Unknown verified state: " + this); + } } From ef55196806a13b932f57bb9d8af971afd18ecdf1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 15 Jun 2017 19:43:06 +0200 Subject: [PATCH 0216/2005] Close input stream from received sync groups and contacts Fixes #75 --- src/main/java/org/asamk/signal/Manager.java | 90 +++++++++++---------- 1 file changed, 47 insertions(+), 43 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index c9252f83..49d22db0 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -807,9 +807,9 @@ class Manager implements Signal { if (contact == null) { contact = new ContactInfo(); contact.number = number; - System.out.println("Add contact " + number + " named " + name); + System.err.println("Add contact " + number + " named " + name); } else { - System.out.println("Updating contact " + number + " name " + contact.name + " -> " + name); + System.err.println("Updating contact " + number + " name " + contact.name + " -> " + name); } contact.name = name; contactStore.updateContact(contact); @@ -1113,7 +1113,7 @@ class Manager implements Signal { try { Files.delete(fileEntry.toPath()); } catch (IOException e) { - System.out.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); + System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); } } } @@ -1170,7 +1170,7 @@ class Manager implements Signal { cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp()); Files.delete(cacheFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); + System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); } } } @@ -1216,23 +1216,25 @@ class Manager implements Signal { File tmpFile = null; try { tmpFile = Util.createTempFile(); - DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile)); - DeviceGroup g; - while ((g = s.read()) != null) { - GroupInfo syncGroup = groupStore.getGroup(g.getId()); - if (syncGroup == null) { - syncGroup = new GroupInfo(g.getId()); - } - if (g.getName().isPresent()) { - syncGroup.name = g.getName().get(); - } - syncGroup.members.addAll(g.getMembers()); - syncGroup.active = g.isActive(); + try (InputStream attachmentAsStream = retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile)) { + DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream); + DeviceGroup g; + while ((g = s.read()) != null) { + GroupInfo syncGroup = groupStore.getGroup(g.getId()); + if (syncGroup == null) { + syncGroup = new GroupInfo(g.getId()); + } + if (g.getName().isPresent()) { + syncGroup.name = g.getName().get(); + } + syncGroup.members.addAll(g.getMembers()); + syncGroup.active = g.isActive(); - if (g.getAvatar().isPresent()) { - retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); + if (g.getAvatar().isPresent()) { + retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); + } + groupStore.updateGroup(syncGroup); } - groupStore.updateGroup(syncGroup); } } catch (Exception e) { e.printStackTrace(); @@ -1241,7 +1243,7 @@ class Manager implements Signal { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); + System.err.println("Failed to delete received groups temp file “" + tmpFile + "”: " + e.getMessage()); } } } @@ -1254,27 +1256,29 @@ class Manager implements Signal { try { tmpFile = Util.createTempFile(); final ContactsMessage contactsMessage = syncMessage.getContacts().get(); - DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)); - if (contactsMessage.isComplete()) { - contactStore.clear(); - } - DeviceContact c; - while ((c = s.read()) != null) { - ContactInfo contact = contactStore.getContact(c.getNumber()); - if (contact == null) { - contact = new ContactInfo(); - contact.number = c.getNumber(); + try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)) { + DeviceContactsInputStream s = new DeviceContactsInputStream(attachmentAsStream); + if (contactsMessage.isComplete()) { + contactStore.clear(); } - if (c.getName().isPresent()) { - contact.name = c.getName().get(); - } - if (c.getColor().isPresent()) { - contact.color = c.getColor().get(); - } - contactStore.updateContact(contact); + DeviceContact c; + while ((c = s.read()) != null) { + ContactInfo contact = contactStore.getContact(c.getNumber()); + if (contact == null) { + contact = new ContactInfo(); + contact.number = c.getNumber(); + } + if (c.getName().isPresent()) { + contact.name = c.getName().get(); + } + if (c.getColor().isPresent()) { + contact.color = c.getColor().get(); + } + contactStore.updateContact(contact); - if (c.getAvatar().isPresent()) { - retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); + if (c.getAvatar().isPresent()) { + retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); + } } } } catch (Exception e) { @@ -1284,7 +1288,7 @@ class Manager implements Signal { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); + System.err.println("Failed to delete received contacts temp file “" + tmpFile + "”: " + e.getMessage()); } } } @@ -1439,7 +1443,7 @@ class Manager implements Signal { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage()); + System.err.println("Failed to delete received attachment temp file “" + tmpFile + "”: " + e.getMessage()); } } return outputFile; @@ -1493,7 +1497,7 @@ class Manager implements Signal { try { Files.delete(groupsFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete temp file “" + groupsFile + "”: " + e.getMessage()); + System.err.println("Failed to delete groups temp file “" + groupsFile + "”: " + e.getMessage()); } } } @@ -1525,7 +1529,7 @@ class Manager implements Signal { try { Files.delete(contactsFile.toPath()); } catch (IOException e) { - System.out.println("Failed to delete temp file “" + contactsFile + "”: " + e.getMessage()); + System.err.println("Failed to delete contacts temp file “" + contactsFile + "”: " + e.getMessage()); } } } From 4d3e67ff83db070c90ddb4f435138f2dd22f27f6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 15 Jun 2017 23:46:16 +0200 Subject: [PATCH 0217/2005] Use Base64 from libsignal --- .../asamk/signal/GroupNotFoundException.java | 2 +- src/main/java/org/asamk/signal/Main.java | 2 +- src/main/java/org/asamk/signal/Manager.java | 2 +- .../signal/NotAGroupMemberException.java | 2 +- .../signal/storage/groups/JsonGroupStore.java | 2 +- .../protocol/JsonIdentityKeyStore.java | 2 +- .../storage/protocol/JsonPreKeyStore.java | 2 +- .../storage/protocol/JsonSessionStore.java | 2 +- .../protocol/JsonSignedPreKeyStore.java | 2 +- .../java/org/asamk/signal/util/Base64.java | 2135 ----------------- src/main/java/org/asamk/signal/util/Util.java | 2 + 11 files changed, 11 insertions(+), 2144 deletions(-) delete mode 100644 src/main/java/org/asamk/signal/util/Base64.java diff --git a/src/main/java/org/asamk/signal/GroupNotFoundException.java b/src/main/java/org/asamk/signal/GroupNotFoundException.java index d9b51fa0..9f8c681a 100644 --- a/src/main/java/org/asamk/signal/GroupNotFoundException.java +++ b/src/main/java/org/asamk/signal/GroupNotFoundException.java @@ -1,7 +1,7 @@ package org.asamk.signal; -import org.asamk.signal.util.Base64; import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.signalservice.internal.util.Base64; public class GroupNotFoundException extends DBusExecutionException { diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index a9097fc9..9acb9fb6 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -24,7 +24,6 @@ import org.asamk.Signal; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; -import org.asamk.signal.util.Base64; import org.asamk.signal.util.Hex; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.DBusSigHandler; @@ -39,6 +38,7 @@ import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptio import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.internal.util.Base64; import java.io.File; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 49d22db0..61f03f08 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -35,7 +35,6 @@ import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; import org.asamk.signal.storage.threads.JsonThreadStore; import org.asamk.signal.storage.threads.ThreadInfo; -import org.asamk.signal.util.Base64; import org.asamk.signal.util.Util; import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; @@ -64,6 +63,7 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.SignalServiceUrl; +import org.whispersystems.signalservice.internal.util.Base64; import java.io.*; import java.net.URI; diff --git a/src/main/java/org/asamk/signal/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/NotAGroupMemberException.java index d525d7e9..42e021ec 100644 --- a/src/main/java/org/asamk/signal/NotAGroupMemberException.java +++ b/src/main/java/org/asamk/signal/NotAGroupMemberException.java @@ -1,7 +1,7 @@ package org.asamk.signal; -import org.asamk.signal.util.Base64; import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.signalservice.internal.util.Base64; public class NotAGroupMemberException extends DBusExecutionException { diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index 1f019e3b..1cc9f151 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -6,7 +6,7 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.asamk.signal.util.Base64; +import org.whispersystems.signalservice.internal.util.Base64; import java.io.IOException; import java.util.ArrayList; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index 16209d77..7c069c8b 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -5,12 +5,12 @@ import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; import org.asamk.signal.TrustLevel; -import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.IdentityKeyStore; +import org.whispersystems.signalservice.internal.util.Base64; import java.io.IOException; import java.util.*; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java index 2f1bad96..d6bc02fa 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java @@ -4,10 +4,10 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; -import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.PreKeyStore; +import org.whispersystems.signalservice.internal.util.Base64; import java.io.IOException; import java.util.HashMap; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index 4290e157..1ae2e5df 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -4,10 +4,10 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; -import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionStore; +import org.whispersystems.signalservice.internal.util.Base64; import java.io.IOException; import java.util.*; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java index d6123495..d7d2a265 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java @@ -4,10 +4,10 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; -import org.asamk.signal.util.Base64; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; +import org.whispersystems.signalservice.internal.util.Base64; import java.io.IOException; import java.util.HashMap; diff --git a/src/main/java/org/asamk/signal/util/Base64.java b/src/main/java/org/asamk/signal/util/Base64.java deleted file mode 100644 index d5e523b1..00000000 --- a/src/main/java/org/asamk/signal/util/Base64.java +++ /dev/null @@ -1,2135 +0,0 @@ -package org.asamk.signal.util; - -/** - *

Encodes and decodes to and from Base64 notation.

- *

Homepage: http://iharder.net/base64.

- * - *

Example:

- * - * String encoded = Base64.encode( myByteArray ); - *
- * byte[] myByteArray = Base64.decode( encoded ); - * - *

The options parameter, which appears in a few places, is used to pass - * several pieces of information to the encoder. In the "higher level" methods such as - * encodeBytes( bytes, options ) the options parameter can be used to indicate such - * things as first gzipping the bytes before encoding them, not inserting linefeeds, - * and encoding using the URL-safe and Ordered dialects.

- * - *

Note, according to RFC3548, - * Section 2.1, implementations should not add line feeds unless explicitly told - * to do so. I've got Base64 set to this behavior now, although earlier versions - * broke lines by default.

- * - *

The constants defined in Base64 can be OR-ed together to combine options, so you - * might make a call like this:

- * - * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES ); - *

to compress the data before encoding it and then making the output have newline characters.

- *

Also...

- * String encoded = Base64.encodeBytes( crazyString.getBytes() ); - * - * - * - *

- * Change Log: - *

- *
    - *
  • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing - * the Base64.OutputStream closed the Base64 encoding (by padding with equals - * signs) too soon. Also added an option to suppress the automatic decoding - * of gzipped streams. Also added experimental support for specifying a - * class loader when using the - * {@link #decodeToObject(java.lang.String, int, java.lang.ClassLoader)} - * method.
  • - *
  • v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java - * footprint with its CharEncoders and so forth. Fixed some javadocs that were - * inconsistent. Removed imports and specified things like java.io.IOException - * explicitly inline.
  • - *
  • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the - * final encoded data will be so that the code doesn't have to create two output - * arrays: an oversized initial one and then a final, exact-sized one. Big win - * when using the {@link #encodeBytesToBytes(byte[])} family of methods (and not - * using the gzip options which uses a different mechanism with streams and stuff).
  • - *
  • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some - * similar helper methods to be more efficient with memory by not returning a - * String but just a byte array.
  • - *
  • v2.3 - This is not a drop-in replacement! This is two years of comments - * and bug fixes queued up and finally executed. Thanks to everyone who sent - * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else. - * Much bad coding was cleaned up including throwing exceptions where necessary - * instead of returning null values or something similar. Here are some changes - * that may affect you: - *
      - *
    • Does not break lines, by default. This is to keep in compliance with - * RFC3548.
    • - *
    • Throws exceptions instead of returning null values. Because some operations - * (especially those that may permit the GZIP option) use IO streams, there - * is a possiblity of an java.io.IOException being thrown. After some discussion and - * thought, I've changed the behavior of the methods to throw java.io.IOExceptions - * rather than return null if ever there's an error. I think this is more - * appropriate, though it will require some changes to your code. Sorry, - * it should have been done this way to begin with.
    • - *
    • Removed all references to System.out, System.err, and the like. - * Shame on me. All I can say is sorry they were ever there.
    • - *
    • Throws NullPointerExceptions and IllegalArgumentExceptions as needed - * such as when passed arrays are null or offsets are invalid.
    • - *
    • Cleaned up as much javadoc as I could to avoid any javadoc warnings. - * This was especially annoying before for people who were thorough in their - * own projects and then had gobs of javadoc warnings on this file.
    • - *
    - *
  • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug - * when using very small files (~< 40 bytes).
  • - *
  • v2.2 - Added some helper methods for encoding/decoding directly from - * one file to the next. Also added a main() method to support command line - * encoding/decoding from one file to the next. Also added these Base64 dialects: - *
      - *
    1. The default is RFC3548 format.
    2. - *
    3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates - * URL and file name friendly format as described in Section 4 of RFC3548. - * http://www.faqs.org/rfcs/rfc3548.html
    4. - *
    5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates - * URL and file name friendly format that preserves lexical ordering as described - * in http://www.faqs.org/qa/rfcc-1940.html
    6. - *
    - * Special thanks to Jim Kellerman at http://www.powerset.com/ - * for contributing the new Base64 dialects. - *
  • - * - *
  • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added - * some convenience methods for reading and writing to and from files.
  • - *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems - * with other encodings (like EBCDIC).
  • - *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the - * encoded data was a single byte.
  • - *
  • v2.0 - I got rid of methods that used booleans to set options. - * Now everything is more consolidated and cleaner. The code now detects - * when data that's being decoded is gzip-compressed and will decompress it - * automatically. Generally things are cleaner. You'll probably have to - * change some method calls that you were making to support the new - * options format (ints that you "OR" together).
  • - *
  • v1.5.1 - Fixed bug when decompressing and decoding to a - * byte[] using decode( String s, boolean gzipCompressed ). - * Added the ability to "suspend" encoding in the Output Stream so - * you can turn on and off the encoding if you need to embed base64 - * data in an otherwise "normal" stream (like an XML file).
  • - *
  • v1.5 - Output stream pases on flush() command but doesn't do anything itself. - * This helps when using GZIP streams. - * Added the ability to GZip-compress objects before encoding them.
  • - *
  • v1.4 - Added helper methods to read/write files.
  • - *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • - *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream - * where last buffer being read, if not completely full, was not returned.
  • - *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
  • - *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • - *
- * - *

- * I am placing this code in the Public Domain. Do with it as you will. - * This software comes with no guarantees or warranties but with - * plenty of well-wishing instead! - * Please visit http://iharder.net/base64 - * periodically to check for updates or to contribute improvements. - *

- * - * @author Robert Harder - * @author rob@iharder.net - * @version 2.3.3 - */ -public class Base64 { - -/* ******** P U B L I C F I E L D S ******** */ - - - /** - * No options specified. Value is zero. - */ - public final static int NO_OPTIONS = 0; - - /** - * Specify encoding in first bit. Value is one. - */ - public final static int ENCODE = 1; - - - /** - * Specify decoding in first bit. Value is zero. - */ - public final static int DECODE = 0; - - - /** - * Specify that data should be gzip-compressed in second bit. Value is two. - */ - public final static int GZIP = 2; - - /** - * Specify that gzipped data should not be automatically gunzipped. - */ - public final static int DONT_GUNZIP = 4; - - - /** - * Do break lines when encoding. Value is 8. - */ - public final static int DO_BREAK_LINES = 8; - - /** - * Encode using Base64-like encoding that is URL- and Filename-safe as described - * in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * It is important to note that data encoded this way is not officially valid Base64, - * or at the very least should not be called Base64 without also specifying that is - * was encoded using the URL- and Filename-safe dialect. - */ - public final static int URL_SAFE = 16; - - - /** - * Encode using the special "ordered" dialect of Base64 described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - public final static int ORDERED = 32; - - -/* ******** P R I V A T E F I E L D S ******** */ - - - /** - * Maximum line length (76) of Base64 output. - */ - private final static int MAX_LINE_LENGTH = 76; - - - /** - * The equals sign (=) as a byte. - */ - private final static byte EQUALS_SIGN = (byte) '='; - - - /** - * The new line character (\n) as a byte. - */ - private final static byte NEW_LINE = (byte) '\n'; - - - /** - * Preferred encoding. - */ - private final static String PREFERRED_ENCODING = "US-ASCII"; - - - private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding - private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding - - -/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ - - /** - * The 64 valid Base64 values. - */ - /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ - private final static byte[] _STANDARD_ALPHABET = { - (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', - (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', - (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', - (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', - (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', - (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', - (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', - (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/' - }; - - - /** - * Translates a Base64 value to either its 6-bit reconstruction value - * or a negative number indicating some other meaning. - */ - private final static byte[] _STANDARD_DECODABET = { - -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - 62, // Plus sign at decimal 43 - -9, -9, -9, // Decimal 44 - 46 - 63, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9 // Decimal 123 - 126 - /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - -/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ - - /** - * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: - * http://www.faqs.org/rfcs/rfc3548.html. - * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." - */ - private final static byte[] _URL_SAFE_ALPHABET = { - (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', - (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', - (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', - (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', - (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', - (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', - (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', - (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '-', (byte) '_' - }; - - /** - * Used in decoding URL- and Filename-safe dialects of Base64. - */ - private final static byte[] _URL_SAFE_DECODABET = { - -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 62, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' - 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' - -9, -9, -9, -9, // Decimal 91 - 94 - 63, // Underscore at decimal 95 - -9, // Decimal 96 - 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' - 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' - -9, -9, -9, -9 // Decimal 123 - 126 - /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - - -/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ - - /** - * I don't get the point of this technique, but someone requested it, - * and it is described here: - * http://www.faqs.org/qa/rfcc-1940.html. - */ - private final static byte[] _ORDERED_ALPHABET = { - (byte) '-', - (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', - (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', - (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', - (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', - (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', - (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', - (byte) '_', - (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', - (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', - (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', - (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z' - }; - - /** - * Used in decoding the "ordered" dialect of Base64. - */ - private final static byte[] _ORDERED_DECODABET = { - -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 - -5, -5, // Whitespace: Tab and Linefeed - -9, -9, // Decimal 11 - 12 - -5, // Whitespace: Carriage Return - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 - -9, -9, -9, -9, -9, // Decimal 27 - 31 - -5, // Whitespace: Space - -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 - -9, // Plus sign at decimal 43 - -9, // Decimal 44 - 0, // Minus sign at decimal 45 - -9, // Decimal 46 - -9, // Slash at decimal 47 - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine - -9, -9, -9, // Decimal 58 - 60 - -1, // Equals sign at decimal 61 - -9, -9, -9, // Decimal 62 - 64 - 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M' - 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z' - -9, -9, -9, -9, // Decimal 91 - 94 - 37, // Underscore at decimal 95 - -9, // Decimal 96 - 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm' - 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z' - -9, -9, -9, -9 // Decimal 123 - 126 - /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 - -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ - }; - - -/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ - - - /** - * Returns one of the _SOMETHING_ALPHABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URLSAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private final static byte[] getAlphabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_ALPHABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_ALPHABET; - } else { - return _STANDARD_ALPHABET; - } - } // end getAlphabet - - - /** - * Returns one of the _SOMETHING_DECODABET byte arrays depending on - * the options specified. - * It's possible, though silly, to specify ORDERED and URL_SAFE - * in which case one of them will be picked, though there is - * no guarantee as to which one will be picked. - */ - private final static byte[] getDecodabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) { - return _URL_SAFE_DECODABET; - } else if ((options & ORDERED) == ORDERED) { - return _ORDERED_DECODABET; - } else { - return _STANDARD_DECODABET; - } - } // end getAlphabet - - - /** - * Defeats instantiation. - */ - private Base64() { - } - - - public static int getEncodedLengthWithoutPadding(int unencodedLength) { - int remainderBytes = unencodedLength % 3; - int paddingBytes = 0; - - if (remainderBytes != 0) - paddingBytes = 3 - remainderBytes; - - return (((int) ((unencodedLength + 2) / 3)) * 4) - paddingBytes; - } - - public static int getEncodedBytesForTarget(int targetSize) { - return ((int) (targetSize * 3)) / 4; - } - - -/* ******** E N C O D I N G M E T H O D S ******** */ - - - /** - * Encodes up to the first three bytes of array threeBytes - * and returns a four-byte array in Base64 notation. - * The actual number of significant bytes in your array is - * given by numSigBytes. - * The array threeBytes needs only be as big as - * numSigBytes. - * Code can reuse a byte array by passing a four-byte array as b4. - * - * @param b4 A reusable byte array to reduce array instantiation - * @param threeBytes the array to convert - * @param numSigBytes the number of significant bytes in your array - * @return four byte array in Base64 notation. - * @since 1.5.1 - */ - private static byte[] encode3to4(byte[] b4, byte[] threeBytes, int numSigBytes, int options) { - encode3to4(threeBytes, 0, numSigBytes, b4, 0, options); - return b4; - } // end encode3to4 - - - /** - *

Encodes up to three bytes of the array source - * and writes the resulting four Base64 bytes to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 3 for - * the source array or destOffset + 4 for - * the destination array. - * The actual number of significant bytes in your array is - * given by numSigBytes.

- *

This is the lowest level of the encoding methods with - * all possible parameters.

- * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param numSigBytes the number of significant bytes in your array - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @return the destination array - * @since 1.3 - */ - private static byte[] encode3to4( - byte[] source, int srcOffset, int numSigBytes, - byte[] destination, int destOffset, int options) { - - byte[] ALPHABET = getAlphabet(options); - - // 1 2 3 - // 01234567890123456789012345678901 Bit position - // --------000000001111111122222222 Array position from threeBytes - // --------| || || || | Six bit groups to index ALPHABET - // >>18 >>12 >> 6 >> 0 Right shift necessary - // 0x3f 0x3f 0x3f Additional AND - - // Create buffer with zero-padding if there are only one or two - // significant bytes passed in the array. - // We have to shift left 24 in order to flush out the 1's that appear - // when Java treats a value as negative that is cast from a byte to an int. - int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) - | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) - | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); - - switch (numSigBytes) { - case 3: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; - return destination; - - case 2: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - - case 1: - destination[destOffset] = ALPHABET[(inBuff >>> 18)]; - destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; - destination[destOffset + 2] = EQUALS_SIGN; - destination[destOffset + 3] = EQUALS_SIGN; - return destination; - - default: - return destination; - } // end switch - } // end encode3to4 - - - /** - * Performs Base64 encoding on the raw ByteBuffer, - * writing it to the encoded ByteBuffer. - * This is an experimental feature. Currently it does not - * pass along any options (such as {@link #DO_BREAK_LINES} - * or {@link #GZIP}. - * - * @param raw input buffer - * @param encoded output buffer - * @since 2.3 - */ - public static void encode(java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded) { - byte[] raw3 = new byte[3]; - byte[] enc4 = new byte[4]; - - while (raw.hasRemaining()) { - int rem = Math.min(3, raw.remaining()); - raw.get(raw3, 0, rem); - Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); - encoded.put(enc4); - } // end input remaining - } - - - /** - * Performs Base64 encoding on the raw ByteBuffer, - * writing it to the encoded CharBuffer. - * This is an experimental feature. Currently it does not - * pass along any options (such as {@link #DO_BREAK_LINES} - * or {@link #GZIP}. - * - * @param raw input buffer - * @param encoded output buffer - * @since 2.3 - */ - public static void encode(java.nio.ByteBuffer raw, java.nio.CharBuffer encoded) { - byte[] raw3 = new byte[3]; - byte[] enc4 = new byte[4]; - - while (raw.hasRemaining()) { - int rem = Math.min(3, raw.remaining()); - raw.get(raw3, 0, rem); - Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); - for (int i = 0; i < 4; i++) { - encoded.put((char) (enc4[i] & 0xFF)); - } - } // end input remaining - } - - - /** - * Serializes an object and returns the Base64-encoded - * version of that serialized object. - * - *

As of v 2.3, if the object - * cannot be serialized or there is another error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * The object is not GZip-compressed before being encoded. - * - * @param serializableObject The object to encode - * @return The Base64-encoded object - * @throws java.io.IOException if there is an error - * @throws NullPointerException if serializedObject is null - * @since 1.4 - */ - public static String encodeObject(java.io.Serializable serializableObject) - throws java.io.IOException { - return encodeObject(serializableObject, NO_OPTIONS); - } // end encodeObject - - - /** - * Serializes an object and returns the Base64-encoded - * version of that serialized object. - * - *

As of v 2.3, if the object - * cannot be serialized or there is another error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * The object is not GZip-compressed before being encoded. - * - * Example options:
-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     * 
- * - * Example: encodeObject( myObj, Base64.GZIP ) or - * - * Example: encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * @param serializableObject The object to encode - * @param options Specified options - * @return The Base64-encoded object - * @throws java.io.IOException if there is an error - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @since 2.0 - */ - public static String encodeObject(java.io.Serializable serializableObject, int options) - throws java.io.IOException { - - if (serializableObject == null) { - throw new NullPointerException("Cannot serialize a null object."); - } // end if: null - - // Streams - java.io.ByteArrayOutputStream baos = null; - java.io.OutputStream b64os = null; - java.util.zip.GZIPOutputStream gzos = null; - java.io.ObjectOutputStream oos = null; - - - try { - // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream(baos, ENCODE | options); - if ((options & GZIP) != 0) { - // Gzip - gzos = new java.util.zip.GZIPOutputStream(b64os); - oos = new java.io.ObjectOutputStream(gzos); - } else { - // Not gzipped - oos = new java.io.ObjectOutputStream(b64os); - } - oos.writeObject(serializableObject); - } // end try - catch (java.io.IOException e) { - // Catch it and then throw it immediately so that - // the finally{} block is called for cleanup. - throw e; - } // end catch - finally { - try { - oos.close(); - } catch (Exception e) { - } - try { - gzos.close(); - } catch (Exception e) { - } - try { - b64os.close(); - } catch (Exception e) { - } - try { - baos.close(); - } catch (Exception e) { - } - } // end finally - - // Return value according to relevant encoding. - try { - return new String(baos.toByteArray(), PREFERRED_ENCODING); - } // end try - catch (java.io.UnsupportedEncodingException uue) { - // Fall back to some Java default - return new String(baos.toByteArray()); - } // end catch - - } // end encode - - - /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. - * - * @param source The data to convert - * @return The data in Base64-encoded form - * @throws NullPointerException if source array is null - * @since 1.4 - */ - public static String encodeBytes(byte[] source) { - // Since we're not going to have the GZIP encoding turned on, - // we're not going to have an java.io.IOException thrown, so - // we should not force the user to have to catch it. - String encoded = null; - try { - encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } // end catch - assert encoded != null; - return encoded; - } // end encodeBytes - - - public static String encodeBytesWithoutPadding(byte[] source, int offset, int length) { - String encoded = null; - - try { - encoded = encodeBytes(source, offset, length, NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } - - assert encoded != null; - - if (encoded.charAt(encoded.length() - 2) == '=') - return encoded.substring(0, encoded.length() - 2); - else if (encoded.charAt(encoded.length() - 1) == '=') - return encoded.substring(0, encoded.length() - 1); - else return encoded; - - } - - public static String encodeBytesWithoutPadding(byte[] source) { - return encodeBytesWithoutPadding(source, 0, source.length); - } - - - /** - * Encodes a byte array into Base64 notation. - *

- * Example options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     *     Note: Technically, this makes your encoding non-compliant.
-     * 
- *

- * Example: encodeBytes( myData, Base64.GZIP ) or - *

- * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * - *

As of v 2.3, if there is an error with the GZIP stream, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param source The data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @since 2.0 - */ - public static String encodeBytes(byte[] source, int options) throws java.io.IOException { - return encodeBytes(source, 0, source.length, options); - } // end encodeBytes - - - /** - * Encodes a byte array into Base64 notation. - * Does not GZip-compress data. - * - *

As of v 2.3, if there is an error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @return The Base64-encoded data as a String - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid - * @since 1.4 - */ - public static String encodeBytes(byte[] source, int off, int len) { - // Since we're not going to have the GZIP encoding turned on, - // we're not going to have an java.io.IOException thrown, so - // we should not force the user to have to catch it. - String encoded = null; - try { - encoded = encodeBytes(source, off, len, NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : ex.getMessage(); - } // end catch - assert encoded != null; - return encoded; - } // end encodeBytes - - - /** - * Encodes a byte array into Base64 notation. - *

- * Example options:

-     *   GZIP: gzip-compresses object before encoding it.
-     *   DO_BREAK_LINES: break lines at 76 characters
-     *     Note: Technically, this makes your encoding non-compliant.
-     * 
- *

- * Example: encodeBytes( myData, Base64.GZIP ) or - *

- * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) - * - * - *

As of v 2.3, if there is an error with the GZIP stream, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned a null value, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @since 2.0 - */ - public static String encodeBytes(byte[] source, int off, int len, int options) throws java.io.IOException { - byte[] encoded = encodeBytesToBytes(source, off, len, options); - - // Return value according to relevant encoding. - try { - return new String(encoded, PREFERRED_ENCODING); - } // end try - catch (java.io.UnsupportedEncodingException uue) { - return new String(encoded); - } // end catch - - } // end encodeBytes - - - /** - * Similar to {@link #encodeBytes(byte[])} but returns - * a byte array instead of instantiating a String. This is more efficient - * if you're working with I/O streams and have large data sets to encode. - * - * @param source The data to convert - * @return The Base64-encoded data as a byte[] (of ASCII characters) - * @throws NullPointerException if source array is null - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes(byte[] source) { - byte[] encoded = null; - try { - encoded = encodeBytesToBytes(source, 0, source.length, Base64.NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); - } - return encoded; - } - - - /** - * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns - * a byte array instead of instantiating a String. This is more efficient - * if you're working with I/O streams and have large data sets to encode. - * - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert - * @param options Specified options - * @return The Base64-encoded data as a String - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @since 2.3.1 - */ - public static byte[] encodeBytesToBytes(byte[] source, int off, int len, int options) throws java.io.IOException { - - if (source == null) { - throw new NullPointerException("Cannot serialize a null array."); - } // end if: null - - if (off < 0) { - throw new IllegalArgumentException("Cannot have negative offset: " + off); - } // end if: off < 0 - - if (len < 0) { - throw new IllegalArgumentException("Cannot have length offset: " + len); - } // end if: len < 0 - - if (off + len > source.length) { - throw new IllegalArgumentException( - String.format("Cannot have offset of %d and length of %d with array of length %d", off, len, source.length)); - } // end if: off < 0 - - - // Compress? - if ((options & GZIP) != 0) { - java.io.ByteArrayOutputStream baos = null; - java.util.zip.GZIPOutputStream gzos = null; - Base64.OutputStream b64os = null; - - try { - // GZip -> Base64 -> ByteArray - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream(baos, ENCODE | options); - gzos = new java.util.zip.GZIPOutputStream(b64os); - - gzos.write(source, off, len); - gzos.close(); - } // end try - catch (java.io.IOException e) { - // Catch it and then throw it immediately so that - // the finally{} block is called for cleanup. - throw e; - } // end catch - finally { - try { - gzos.close(); - } catch (Exception e) { - } - try { - b64os.close(); - } catch (Exception e) { - } - try { - baos.close(); - } catch (Exception e) { - } - } // end finally - - return baos.toByteArray(); - } // end if: compress - - // Else, don't compress. Better not to use streams at all then. - else { - boolean breakLines = (options & DO_BREAK_LINES) > 0; - - //int len43 = len * 4 / 3; - //byte[] outBuff = new byte[ ( len43 ) // Main 4:3 - // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding - // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines - // Try to determine more precisely how big the array needs to be. - // If we get it right, we don't have to do an array copy, and - // we save a bunch of memory. - int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual encoding - if (breakLines) { - encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters - } - byte[] outBuff = new byte[encLen]; - - - int d = 0; - int e = 0; - int len2 = len - 2; - int lineLength = 0; - for (; d < len2; d += 3, e += 4) { - encode3to4(source, d + off, 3, outBuff, e, options); - - lineLength += 4; - if (breakLines && lineLength >= MAX_LINE_LENGTH) { - outBuff[e + 4] = NEW_LINE; - e++; - lineLength = 0; - } // end if: end of line - } // en dfor: each piece of array - - if (d < len) { - encode3to4(source, d + off, len - d, outBuff, e, options); - e += 4; - } // end if: some padding needed - - - // Only resize array if we didn't guess it right. - if (e < outBuff.length - 1) { - byte[] finalOut = new byte[e]; - System.arraycopy(outBuff, 0, finalOut, 0, e); - //System.err.println("Having to resize array from " + outBuff.length + " to " + e ); - return finalOut; - } else { - //System.err.println("No need to resize array."); - return outBuff; - } - - } // end else: don't compress - - } // end encodeBytesToBytes - - - - - -/* ******** D E C O D I N G M E T H O D S ******** */ - - - /** - * Decodes four bytes from array source - * and writes the resulting bytes (up to three of them) - * to destination. - * The source and destination arrays can be manipulated - * anywhere along their length by specifying - * srcOffset and destOffset. - * This method does not check to make sure your arrays - * are large enough to accomodate srcOffset + 4 for - * the source array or destOffset + 3 for - * the destination array. - * This method returns the actual number of bytes that - * were converted from the Base64 encoding. - *

This is the lowest level of the decoding methods with - * all possible parameters.

- * - * @param source the array to convert - * @param srcOffset the index where conversion begins - * @param destination the array to hold the conversion - * @param destOffset the index where output will be put - * @param options alphabet type is pulled from this (standard, url-safe, ordered) - * @return the number of decoded bytes converted - * @throws NullPointerException if source or destination arrays are null - * @throws IllegalArgumentException if srcOffset or destOffset are invalid - * or there is not enough room in the array. - * @since 1.3 - */ - private static int decode4to3( - byte[] source, int srcOffset, - byte[] destination, int destOffset, int options) { - - // Lots of error checking and exception throwing - if (source == null) { - throw new NullPointerException("Source array was null."); - } // end if - if (destination == null) { - throw new NullPointerException("Destination array was null."); - } // end if - if (srcOffset < 0 || srcOffset + 3 >= source.length) { - throw new IllegalArgumentException(String.format( - "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset)); - } // end if - if (destOffset < 0 || destOffset + 2 >= destination.length) { - throw new IllegalArgumentException(String.format( - "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset)); - } // end if - - - byte[] DECODABET = getDecodabet(options); - - // Example: Dk== - if (source[srcOffset + 2] == EQUALS_SIGN) { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); - - destination[destOffset] = (byte) (outBuff >>> 16); - return 1; - } - - // Example: DkL= - else if (source[srcOffset + 3] == EQUALS_SIGN) { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) - | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); - - destination[destOffset] = (byte) (outBuff >>> 16); - destination[destOffset + 1] = (byte) (outBuff >>> 8); - return 2; - } - - // Example: DkLE - else { - // Two ways to do the same thing. Don't know which way I like best. - //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) - // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) - // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) - // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); - int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) - | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) - | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) - | ((DECODABET[source[srcOffset + 3]] & 0xFF)); - - - destination[destOffset] = (byte) (outBuff >> 16); - destination[destOffset + 1] = (byte) (outBuff >> 8); - destination[destOffset + 2] = (byte) (outBuff); - - return 3; - } - } // end decodeToBytes - - - /** - * Low-level access to decoding ASCII characters in - * the form of a byte array. Ignores GUNZIP option, if - * it's set. This is not generally a recommended method, - * although it is used internally as part of the decoding process. - * Special case: if len = 0, an empty array is returned. Still, - * if you need more speed and reduced memory footprint (and aren't - * gzipping), consider this method. - * - * @param source The Base64 encoded data - * @return decoded data - * @since 2.3.1 - */ - public static byte[] decode(byte[] source) { - byte[] decoded = null; - try { - decoded = decode(source, 0, source.length, Base64.NO_OPTIONS); - } catch (java.io.IOException ex) { - assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); - } - return decoded; - } - - - /** - * Low-level access to decoding ASCII characters in - * the form of a byte array. Ignores GUNZIP option, if - * it's set. This is not generally a recommended method, - * although it is used internally as part of the decoding process. - * Special case: if len = 0, an empty array is returned. Still, - * if you need more speed and reduced memory footprint (and aren't - * gzipping), consider this method. - * - * @param source The Base64 encoded data - * @param off The offset of where to begin decoding - * @param len The length of characters to decode - * @param options Can specify options such as alphabet type to use - * @return decoded data - * @throws java.io.IOException If bogus characters exist in source data - * @since 1.3 - */ - public static byte[] decode(byte[] source, int off, int len, int options) - throws java.io.IOException { - - // Lots of error checking and exception throwing - if (source == null) { - throw new NullPointerException("Cannot decode null source array."); - } // end if - if (off < 0 || off + len > source.length) { - throw new IllegalArgumentException(String.format( - "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len)); - } // end if - - if (len == 0) { - return new byte[0]; - } else if (len < 4) { - throw new IllegalArgumentException( - "Base64-encoded string must have at least four characters, but length specified was " + len); - } // end if - - byte[] DECODABET = getDecodabet(options); - - int len34 = len * 3 / 4; // Estimate on array size - byte[] outBuff = new byte[len34]; // Upper limit on size of output - int outBuffPosn = 0; // Keep track of where we're writing - - byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space - int b4Posn = 0; // Keep track of four byte input buffer - int i = 0; // Source array counter - byte sbiCrop = 0; // Low seven bits (ASCII) of input - byte sbiDecode = 0; // Special value from DECODABET - - for (i = off; i < off + len; i++) { // Loop through source - - sbiCrop = (byte) (source[i] & 0x7f); // Only the low seven bits - sbiDecode = DECODABET[sbiCrop]; // Special value - - // White space, Equals sign, or legit Base64 character - // Note the values such as -5 and -9 in the - // DECODABETs at the top of the file. - if (sbiDecode >= WHITE_SPACE_ENC) { - if (sbiDecode >= EQUALS_SIGN_ENC) { - b4[b4Posn++] = sbiCrop; // Save non-whitespace - if (b4Posn > 3) { // Time to decode? - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options); - b4Posn = 0; - - // If that was the equals sign, break out of 'for' loop - if (sbiCrop == EQUALS_SIGN) { - break; - } // end if: equals sign - } // end if: quartet built - } // end if: equals sign or better - } // end if: white space, equals sign or better - else { - // There's a bad input character in the Base64 stream. - throw new java.io.IOException(String.format( - "Bad Base64 input character '%c' in array position %d", source[i], i)); - } // end else: - } // each input character - - byte[] out = new byte[outBuffPosn]; - System.arraycopy(outBuff, 0, out, 0, outBuffPosn); - return out; - } // end decode - - - /** - * Decodes data from Base64 notation, automatically - * detecting gzip-compressed data and decompressing it. - * - * @param s the string to decode - * @return the decoded data - * @throws java.io.IOException If there is a problem - * @since 1.4 - */ - public static byte[] decode(String s) throws java.io.IOException { - return decode(s, NO_OPTIONS); - } - - - public static byte[] decodeWithoutPadding(String source) throws java.io.IOException { - int padding = source.length() % 4; - - if (padding == 1) source = source + "="; - else if (padding == 2) source = source + "=="; - else if (padding == 3) source = source + "="; - - return decode(source); - } - - - /** - * Decodes data from Base64 notation, automatically - * detecting gzip-compressed data and decompressing it. - * - * @param s the string to decode - * @param options encode options such as URL_SAFE - * @return the decoded data - * @throws java.io.IOException if there is an error - * @throws NullPointerException if s is null - * @since 1.4 - */ - public static byte[] decode(String s, int options) throws java.io.IOException { - - if (s == null) { - throw new NullPointerException("Input string was null."); - } // end if - - byte[] bytes; - try { - bytes = s.getBytes(PREFERRED_ENCODING); - } // end try - catch (java.io.UnsupportedEncodingException uee) { - bytes = s.getBytes(); - } // end catch - // - - // Decode - bytes = decode(bytes, 0, bytes.length, options); - - // Check to see if it's gzip-compressed - // GZIP Magic Two-Byte Number: 0x8b1f (35615) - boolean dontGunzip = (options & DONT_GUNZIP) != 0; - if ((bytes != null) && (bytes.length >= 4) && (!dontGunzip)) { - - int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); - if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) { - java.io.ByteArrayInputStream bais = null; - java.util.zip.GZIPInputStream gzis = null; - java.io.ByteArrayOutputStream baos = null; - byte[] buffer = new byte[2048]; - int length = 0; - - try { - baos = new java.io.ByteArrayOutputStream(); - bais = new java.io.ByteArrayInputStream(bytes); - gzis = new java.util.zip.GZIPInputStream(bais); - - while ((length = gzis.read(buffer)) >= 0) { - baos.write(buffer, 0, length); - } // end while: reading input - - // No error? Get new bytes. - bytes = baos.toByteArray(); - - } // end try - catch (java.io.IOException e) { - e.printStackTrace(); - // Just return originally-decoded bytes - } // end catch - finally { - try { - baos.close(); - } catch (Exception e) { - } - try { - gzis.close(); - } catch (Exception e) { - } - try { - bais.close(); - } catch (Exception e) { - } - } // end finally - - } // end if: gzipped - } // end if: bytes.length >= 2 - - return bytes; - } // end decode - - - /** - * Attempts to decode Base64 data and deserialize a Java - * Object within. Returns null if there was an error. - * - * @param encodedObject The Base64 data to decode - * @return The decoded and deserialized object - * @throws NullPointerException if encodedObject is null - * @throws java.io.IOException if there is a general error - * @throws ClassNotFoundException if the decoded object is of a - * class that cannot be found by the JVM - * @since 1.5 - */ - public static Object decodeToObject(String encodedObject) - throws java.io.IOException, java.lang.ClassNotFoundException { - return decodeToObject(encodedObject, NO_OPTIONS, null); - } - - - /** - * Attempts to decode Base64 data and deserialize a Java - * Object within. Returns null if there was an error. - * If loader is not null, it will be the class loader - * used when deserializing. - * - * @param encodedObject The Base64 data to decode - * @param options Various parameters related to decoding - * @param loader Optional class loader to use in deserializing classes. - * @return The decoded and deserialized object - * @throws NullPointerException if encodedObject is null - * @throws java.io.IOException if there is a general error - * @throws ClassNotFoundException if the decoded object is of a - * class that cannot be found by the JVM - * @since 2.3.4 - */ - public static Object decodeToObject( - String encodedObject, int options, final ClassLoader loader) - throws java.io.IOException, java.lang.ClassNotFoundException { - - // Decode and gunzip if necessary - byte[] objBytes = decode(encodedObject, options); - - java.io.ByteArrayInputStream bais = null; - java.io.ObjectInputStream ois = null; - Object obj = null; - - try { - bais = new java.io.ByteArrayInputStream(objBytes); - - // If no custom class loader is provided, use Java's builtin OIS. - if (loader == null) { - ois = new java.io.ObjectInputStream(bais); - } // end if: no loader provided - - // Else make a customized object input stream that uses - // the provided class loader. - else { - ois = new java.io.ObjectInputStream(bais) { - @Override - public Class resolveClass(java.io.ObjectStreamClass streamClass) - throws java.io.IOException, ClassNotFoundException { - Class c = Class.forName(streamClass.getName(), false, loader); - if (c == null) { - return super.resolveClass(streamClass); - } else { - return c; // Class loader knows of this class. - } // end else: not null - } // end resolveClass - }; // end ois - } // end else: no custom class loader - - obj = ois.readObject(); - } // end try - catch (java.io.IOException e) { - throw e; // Catch and throw in order to execute finally{} - } // end catch - catch (java.lang.ClassNotFoundException e) { - throw e; // Catch and throw in order to execute finally{} - } // end catch - finally { - try { - bais.close(); - } catch (Exception e) { - } - try { - ois.close(); - } catch (Exception e) { - } - } // end finally - - return obj; - } // end decodeObject - - - /** - * Convenience method for encoding data to a file. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param dataToEncode byte array of data to encode in base64 form - * @param filename Filename for saving encoded data - * @throws java.io.IOException if there is an error - * @throws NullPointerException if dataToEncode is null - * @since 2.1 - */ - public static void encodeToFile(byte[] dataToEncode, String filename) - throws java.io.IOException { - - if (dataToEncode == null) { - throw new NullPointerException("Data to encode was null."); - } // end iff - - Base64.OutputStream bos = null; - try { - bos = new Base64.OutputStream( - new java.io.FileOutputStream(filename), Base64.ENCODE); - bos.write(dataToEncode); - } // end try - catch (java.io.IOException e) { - throw e; // Catch and throw to execute finally{} block - } // end catch: java.io.IOException - finally { - try { - bos.close(); - } catch (Exception e) { - } - } // end finally - - } // end encodeToFile - - - /** - * Convenience method for decoding data to a file. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param dataToDecode Base64-encoded data as a string - * @param filename Filename for saving decoded data - * @throws java.io.IOException if there is an error - * @since 2.1 - */ - public static void decodeToFile(String dataToDecode, String filename) - throws java.io.IOException { - - Base64.OutputStream bos = null; - try { - bos = new Base64.OutputStream( - new java.io.FileOutputStream(filename), Base64.DECODE); - bos.write(dataToDecode.getBytes(PREFERRED_ENCODING)); - } // end try - catch (java.io.IOException e) { - throw e; // Catch and throw to execute finally{} block - } // end catch: java.io.IOException - finally { - try { - bos.close(); - } catch (Exception e) { - } - } // end finally - - } // end decodeToFile - - - /** - * Convenience method for reading a base64-encoded - * file and decoding it. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param filename Filename for reading encoded data - * @return decoded byte array - * @throws java.io.IOException if there is an error - * @since 2.1 - */ - public static byte[] decodeFromFile(String filename) - throws java.io.IOException { - - byte[] decodedData = null; - Base64.InputStream bis = null; - try { - // Set up some useful variables - java.io.File file = new java.io.File(filename); - byte[] buffer = null; - int length = 0; - int numBytes = 0; - - // Check for size of file - if (file.length() > Integer.MAX_VALUE) { - throw new java.io.IOException("File is too big for this convenience method (" + file.length() + " bytes)."); - } // end if: file too big for int index - buffer = new byte[(int) file.length()]; - - // Open a stream - bis = new Base64.InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream(file)), Base64.DECODE); - - // Read until done - while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { - length += numBytes; - } // end while - - // Save in a variable to return - decodedData = new byte[length]; - System.arraycopy(buffer, 0, decodedData, 0, length); - - } // end try - catch (java.io.IOException e) { - throw e; // Catch and release to execute finally{} - } // end catch: java.io.IOException - finally { - try { - bis.close(); - } catch (Exception e) { - } - } // end finally - - return decodedData; - } // end decodeFromFile - - - /** - * Convenience method for reading a binary file - * and base64-encoding it. - * - *

As of v 2.3, if there is a error, - * the method will throw an java.io.IOException. This is new to v2.3! - * In earlier versions, it just returned false, but - * in retrospect that's a pretty poor way to handle it.

- * - * @param filename Filename for reading binary data - * @return base64-encoded string - * @throws java.io.IOException if there is an error - * @since 2.1 - */ - public static String encodeFromFile(String filename) - throws java.io.IOException { - - String encodedData = null; - Base64.InputStream bis = null; - try { - // Set up some useful variables - java.io.File file = new java.io.File(filename); - byte[] buffer = new byte[Math.max((int) (file.length() * 1.4), 40)]; // Need max() for math on small files (v2.2.1) - int length = 0; - int numBytes = 0; - - // Open a stream - bis = new Base64.InputStream( - new java.io.BufferedInputStream( - new java.io.FileInputStream(file)), Base64.ENCODE); - - // Read until done - while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { - length += numBytes; - } // end while - - // Save in a variable to return - encodedData = new String(buffer, 0, length, Base64.PREFERRED_ENCODING); - - } // end try - catch (java.io.IOException e) { - throw e; // Catch and release to execute finally{} - } // end catch: java.io.IOException - finally { - try { - bis.close(); - } catch (Exception e) { - } - } // end finally - - return encodedData; - } // end encodeFromFile - - /** - * Reads infile and encodes it to outfile. - * - * @param infile Input file - * @param outfile Output file - * @throws java.io.IOException if there is an error - * @since 2.2 - */ - public static void encodeFileToFile(String infile, String outfile) - throws java.io.IOException { - - String encoded = Base64.encodeFromFile(infile); - java.io.OutputStream out = null; - try { - out = new java.io.BufferedOutputStream( - new java.io.FileOutputStream(outfile)); - out.write(encoded.getBytes("US-ASCII")); // Strict, 7-bit output. - } // end try - catch (java.io.IOException e) { - throw e; // Catch and release to execute finally{} - } // end catch - finally { - try { - out.close(); - } catch (Exception ex) { - } - } // end finally - } // end encodeFileToFile - - - /** - * Reads infile and decodes it to outfile. - * - * @param infile Input file - * @param outfile Output file - * @throws java.io.IOException if there is an error - * @since 2.2 - */ - public static void decodeFileToFile(String infile, String outfile) - throws java.io.IOException { - - byte[] decoded = Base64.decodeFromFile(infile); - java.io.OutputStream out = null; - try { - out = new java.io.BufferedOutputStream( - new java.io.FileOutputStream(outfile)); - out.write(decoded); - } // end try - catch (java.io.IOException e) { - throw e; // Catch and release to execute finally{} - } // end catch - finally { - try { - out.close(); - } catch (Exception ex) { - } - } // end finally - } // end decodeFileToFile - - - /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ - - - /** - * A {@link Base64.InputStream} will read data from another - * java.io.InputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class InputStream extends java.io.FilterInputStream { - - private boolean encode; // Encoding or decoding - private int position; // Current position in the buffer - private byte[] buffer; // Small buffer holding converted data - private int bufferLength; // Length of buffer (3 or 4) - private int numSigBytes; // Number of meaningful bytes in the buffer - private int lineLength; - private boolean breakLines; // Break lines at less than 80 characters - private int options; // Record options used to create the stream. - private byte[] decodabet; // Local copies to avoid extra method calls - - - /** - * Constructs a {@link Base64.InputStream} in DECODE mode. - * - * @param in the java.io.InputStream from which to read data. - * @since 1.3 - */ - public InputStream(java.io.InputStream in) { - this(in, DECODE); - } // end constructor - - - /** - * Constructs a {@link Base64.InputStream} in - * either ENCODE or DECODE mode. - * - * Valid options:
-         *   ENCODE or DECODE: Encode or Decode as data is read.
-         *   DO_BREAK_LINES: break lines at 76 characters
-         *     (only meaningful when encoding)
-         * 
- * - * Example: new Base64.InputStream( in, Base64.DECODE ) - * - * @param in the java.io.InputStream from which to read data. - * @param options Specified options - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DO_BREAK_LINES - * @since 2.0 - */ - public InputStream(java.io.InputStream in, int options) { - - super(in); - this.options = options; // Record for later - this.breakLines = (options & DO_BREAK_LINES) > 0; - this.encode = (options & ENCODE) > 0; - this.bufferLength = encode ? 4 : 3; - this.buffer = new byte[bufferLength]; - this.position = -1; - this.lineLength = 0; - this.decodabet = getDecodabet(options); - } // end constructor - - /** - * Reads enough of the input stream to convert - * to/from Base64 and returns the next byte. - * - * @return next byte - * @since 1.3 - */ - @Override - public int read() throws java.io.IOException { - - // Do we need to get data? - if (position < 0) { - if (encode) { - byte[] b3 = new byte[3]; - int numBinaryBytes = 0; - for (int i = 0; i < 3; i++) { - int b = in.read(); - - // If end of stream, b is -1. - if (b >= 0) { - b3[i] = (byte) b; - numBinaryBytes++; - } else { - break; // out of for loop - } // end else: end of stream - - } // end for: each needed input byte - - if (numBinaryBytes > 0) { - encode3to4(b3, 0, numBinaryBytes, buffer, 0, options); - position = 0; - numSigBytes = 4; - } // end if: got data - else { - return -1; // Must be end of stream - } // end else - } // end if: encoding - - // Else decoding - else { - byte[] b4 = new byte[4]; - int i = 0; - for (i = 0; i < 4; i++) { - // Read four "meaningful" bytes: - int b = 0; - do { - b = in.read(); - } - while (b >= 0 && decodabet[b & 0x7f] <= WHITE_SPACE_ENC); - - if (b < 0) { - break; // Reads a -1 if end of stream - } // end if: end of stream - - b4[i] = (byte) b; - } // end for: each needed input byte - - if (i == 4) { - numSigBytes = decode4to3(b4, 0, buffer, 0, options); - position = 0; - } // end if: got four characters - else if (i == 0) { - return -1; - } // end else if: also padded correctly - else { - // Must have broken out from above. - throw new java.io.IOException("Improperly padded Base64 input."); - } // end - - } // end else: decode - } // end else: get data - - // Got data? - if (position >= 0) { - // End of relevant data? - if ( /*!encode &&*/ position >= numSigBytes) { - return -1; - } // end if: got data - - if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) { - lineLength = 0; - return '\n'; - } // end if - else { - lineLength++; // This isn't important when decoding - // but throwing an extra "if" seems - // just as wasteful. - - int b = buffer[position++]; - - if (position >= bufferLength) { - position = -1; - } // end if: end - - return b & 0xFF; // This is how you "cast" a byte that's - // intended to be unsigned. - } // end else - } // end if: position >= 0 - - // Else error - else { - throw new java.io.IOException("Error in Base64 code reading stream."); - } // end else - } // end read - - - /** - * Calls {@link #read()} repeatedly until the end of stream - * is reached or len bytes are read. - * Returns number of bytes read into array or -1 if - * end of stream is encountered. - * - * @param dest array to hold values - * @param off offset for array - * @param len max number of bytes to read into array - * @return bytes read into array or -1 if end of stream is encountered. - * @since 1.3 - */ - @Override - public int read(byte[] dest, int off, int len) - throws java.io.IOException { - int i; - int b; - for (i = 0; i < len; i++) { - b = read(); - - if (b >= 0) { - dest[off + i] = (byte) b; - } else if (i == 0) { - return -1; - } else { - break; // Out of 'for' loop - } // Out of 'for' loop - } // end for: each byte read - return i; - } // end read - - } // end inner class InputStream - - - - - - - /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ - - - /** - * A {@link Base64.OutputStream} will write data to another - * java.io.OutputStream, given in the constructor, - * and encode/decode to/from Base64 notation on the fly. - * - * @see Base64 - * @since 1.3 - */ - public static class OutputStream extends java.io.FilterOutputStream { - - private boolean encode; - private int position; - private byte[] buffer; - private int bufferLength; - private int lineLength; - private boolean breakLines; - private byte[] b4; // Scratch used in a few places - private boolean suspendEncoding; - private int options; // Record for later - private byte[] decodabet; // Local copies to avoid extra method calls - - /** - * Constructs a {@link Base64.OutputStream} in ENCODE mode. - * - * @param out the java.io.OutputStream to which data will be written. - * @since 1.3 - */ - public OutputStream(java.io.OutputStream out) { - this(out, ENCODE); - } // end constructor - - - /** - * Constructs a {@link Base64.OutputStream} in - * either ENCODE or DECODE mode. - * - * Valid options:
-         *   ENCODE or DECODE: Encode or Decode as data is read.
-         *   DO_BREAK_LINES: don't break lines at 76 characters
-         *     (only meaningful when encoding)
-         * 
- * - * Example: new Base64.OutputStream( out, Base64.ENCODE ) - * - * @param out the java.io.OutputStream to which data will be written. - * @param options Specified options. - * @see Base64#ENCODE - * @see Base64#DECODE - * @see Base64#DO_BREAK_LINES - * @since 1.3 - */ - public OutputStream(java.io.OutputStream out, int options) { - super(out); - this.breakLines = (options & DO_BREAK_LINES) != 0; - this.encode = (options & ENCODE) != 0; - this.bufferLength = encode ? 3 : 4; - this.buffer = new byte[bufferLength]; - this.position = 0; - this.lineLength = 0; - this.suspendEncoding = false; - this.b4 = new byte[4]; - this.options = options; - this.decodabet = getDecodabet(options); - } // end constructor - - - /** - * Writes the byte to the output stream after - * converting to/from Base64 notation. - * When encoding, bytes are buffered three - * at a time before the output stream actually - * gets a write() call. - * When decoding, bytes are buffered four - * at a time. - * - * @param theByte the byte to write - * @since 1.3 - */ - @Override - public void write(int theByte) - throws java.io.IOException { - // Encoding suspended? - if (suspendEncoding) { - this.out.write(theByte); - return; - } // end if: supsended - - // Encode? - if (encode) { - buffer[position++] = (byte) theByte; - if (position >= bufferLength) { // Enough to encode. - - this.out.write(encode3to4(b4, buffer, bufferLength, options)); - - lineLength += 4; - if (breakLines && lineLength >= MAX_LINE_LENGTH) { - this.out.write(NEW_LINE); - lineLength = 0; - } // end if: end of line - - position = 0; - } // end if: enough to output - } // end if: encoding - - // Else, Decoding - else { - // Meaningful Base64 character? - if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) { - buffer[position++] = (byte) theByte; - if (position >= bufferLength) { // Enough to output. - - int len = Base64.decode4to3(buffer, 0, b4, 0, options); - out.write(b4, 0, len); - position = 0; - } // end if: enough to output - } // end if: meaningful base64 character - else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) { - throw new java.io.IOException("Invalid character in Base64 data."); - } // end else: not white space either - } // end else: decoding - } // end write - - - /** - * Calls {@link #write(int)} repeatedly until len - * bytes are written. - * - * @param theBytes array from which to read bytes - * @param off offset for array - * @param len max number of bytes to read into array - * @since 1.3 - */ - @Override - public void write(byte[] theBytes, int off, int len) - throws java.io.IOException { - // Encoding suspended? - if (suspendEncoding) { - this.out.write(theBytes, off, len); - return; - } // end if: supsended - - for (int i = 0; i < len; i++) { - write(theBytes[off + i]); - } // end for: each byte written - - } // end write - - - /** - * Method added by PHIL. [Thanks, PHIL. -Rob] - * This pads the buffer without closing the stream. - * - * @throws java.io.IOException if there's an error. - */ - public void flushBase64() throws java.io.IOException { - if (position > 0) { - if (encode) { - out.write(encode3to4(b4, buffer, position, options)); - position = 0; - } // end if: encoding - else { - throw new java.io.IOException("Base64 input not properly padded."); - } // end else: decoding - } // end if: buffer partially full - - } // end flush - - - /** - * Flushes and closes (I think, in the superclass) the stream. - * - * @since 1.3 - */ - @Override - public void close() throws java.io.IOException { - // 1. Ensure that pending characters are written - flushBase64(); - - // 2. Actually close the stream - // Base class both flushes and closes. - super.close(); - - buffer = null; - out = null; - } // end close - - - /** - * Suspends encoding of the stream. - * May be helpful if you need to embed a piece of - * base64-encoded data in a stream. - * - * @throws java.io.IOException if there's an error flushing - * @since 1.5.1 - */ - public void suspendEncoding() throws java.io.IOException { - flushBase64(); - this.suspendEncoding = true; - } // end suspendEncoding - - - /** - * Resumes encoding of the stream. - * May be helpful if you need to embed a piece of - * base64-encoded data in a stream. - * - * @since 1.5.1 - */ - public void resumeEncoding() { - this.suspendEncoding = false; - } // end resumeEncoding - - - } // end inner class OutputStream - - -} // end class Base64 diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 679e1384..19695ec6 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -1,5 +1,7 @@ package org.asamk.signal.util; +import org.whispersystems.signalservice.internal.util.Base64; + import java.io.File; import java.io.IOException; import java.security.NoSuchAlgorithmException; From 24ab58cc1405344dedbf67eaf5f4314bdae747eb Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 15 Jun 2017 23:47:00 +0200 Subject: [PATCH 0218/2005] Delete empty message cache directories --- src/main/java/org/asamk/signal/Manager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 61f03f08..dc979930 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1116,6 +1116,8 @@ class Manager implements Signal { System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage()); } } + // Try to delete directory if empty + dir.delete(); } } @@ -1169,6 +1171,8 @@ class Manager implements Signal { try { cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp()); Files.delete(cacheFile.toPath()); + // Try to delete directory if empty + new File(getMessageCachePath()).delete(); } catch (IOException e) { System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage()); } From a1f0d74a99e4dc5c2e7a7ee79f59684314da4c4f Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 16 Jun 2017 11:40:08 +0200 Subject: [PATCH 0219/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 8bacf3b0..5885ce5b 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.5.5' +version = '0.5.6' compileJava.options.encoding = 'UTF-8' From 8717665d1d273a32afef136c15c0d5abaaae0f85 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 15 Jun 2017 23:45:14 +0200 Subject: [PATCH 0220/2005] Implement json output for receive --- .../java/org/asamk/signal/JsonAttachment.java | 21 +++++++++ .../org/asamk/signal/JsonCallMessage.java | 31 +++++++++++++ .../org/asamk/signal/JsonDataMessage.java | 34 +++++++++++++++ src/main/java/org/asamk/signal/JsonError.java | 9 ++++ .../java/org/asamk/signal/JsonGroupInfo.java | 24 +++++++++++ .../org/asamk/signal/JsonMessageEnvelope.java | 36 ++++++++++++++++ .../org/asamk/signal/JsonSyncMessage.java | 24 +++++++++++ src/main/java/org/asamk/signal/Main.java | 43 ++++++++++++++++++- .../protocol/JsonSignalProtocolStore.java | 5 ++- 9 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/asamk/signal/JsonAttachment.java create mode 100644 src/main/java/org/asamk/signal/JsonCallMessage.java create mode 100644 src/main/java/org/asamk/signal/JsonDataMessage.java create mode 100644 src/main/java/org/asamk/signal/JsonError.java create mode 100644 src/main/java/org/asamk/signal/JsonGroupInfo.java create mode 100644 src/main/java/org/asamk/signal/JsonMessageEnvelope.java create mode 100644 src/main/java/org/asamk/signal/JsonSyncMessage.java diff --git a/src/main/java/org/asamk/signal/JsonAttachment.java b/src/main/java/org/asamk/signal/JsonAttachment.java new file mode 100644 index 00000000..53946df9 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonAttachment.java @@ -0,0 +1,21 @@ +package org.asamk.signal; + +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; + +class JsonAttachment { + String contentType; + long id; + int size; + + JsonAttachment(SignalServiceAttachment attachment) { + this.contentType = attachment.getContentType(); + final SignalServiceAttachmentPointer pointer = attachment.asPointer(); + if (attachment.isPointer()) { + this.id = pointer.getId(); + if (pointer.getSize().isPresent()) { + this.size = pointer.getSize().get(); + } + } + } +} diff --git a/src/main/java/org/asamk/signal/JsonCallMessage.java b/src/main/java/org/asamk/signal/JsonCallMessage.java new file mode 100644 index 00000000..98a01b29 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonCallMessage.java @@ -0,0 +1,31 @@ +package org.asamk.signal; + +import org.whispersystems.signalservice.api.messages.calls.*; + +import java.util.List; + +class JsonCallMessage { + OfferMessage offerMessage; + AnswerMessage answerMessage; + BusyMessage busyMessage; + HangupMessage hangupMessage; + List iceUpdateMessages; + + JsonCallMessage(SignalServiceCallMessage callMessage) { + if (callMessage.getOfferMessage().isPresent()) { + this.offerMessage = callMessage.getOfferMessage().get(); + } + if (callMessage.getAnswerMessage().isPresent()) { + this.answerMessage = callMessage.getAnswerMessage().get(); + } + if (callMessage.getBusyMessage().isPresent()) { + this.busyMessage = callMessage.getBusyMessage().get(); + } + if (callMessage.getHangupMessage().isPresent()) { + this.hangupMessage = callMessage.getHangupMessage().get(); + } + if (callMessage.getIceUpdateMessages().isPresent()) { + this.iceUpdateMessages = callMessage.getIceUpdateMessages().get(); + } + } +} diff --git a/src/main/java/org/asamk/signal/JsonDataMessage.java b/src/main/java/org/asamk/signal/JsonDataMessage.java new file mode 100644 index 00000000..eda54025 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonDataMessage.java @@ -0,0 +1,34 @@ +package org.asamk.signal; + +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; + +import java.util.ArrayList; +import java.util.List; + +class JsonDataMessage { + long timestamp; + String message; + int expiresInSeconds; + List attachments; + JsonGroupInfo groupInfo; + + JsonDataMessage(SignalServiceDataMessage dataMessage) { + this.timestamp = dataMessage.getTimestamp(); + if (dataMessage.getGroupInfo().isPresent()) { + this.groupInfo = new JsonGroupInfo(dataMessage.getGroupInfo().get()); + } + if (dataMessage.getBody().isPresent()) { + this.message = dataMessage.getBody().get(); + } + this.expiresInSeconds = dataMessage.getExpiresInSeconds(); + if (dataMessage.getAttachments().isPresent()) { + this.attachments = new ArrayList<>(dataMessage.getAttachments().get().size()); + for (SignalServiceAttachment attachment : dataMessage.getAttachments().get()) { + this.attachments.add(new JsonAttachment(attachment)); + } + } else { + this.attachments = new ArrayList<>(); + } + } +} diff --git a/src/main/java/org/asamk/signal/JsonError.java b/src/main/java/org/asamk/signal/JsonError.java new file mode 100644 index 00000000..05fe3ae6 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonError.java @@ -0,0 +1,9 @@ +package org.asamk.signal; + +class JsonError { + String message; + + JsonError(Throwable exception) { + this.message = exception.getMessage(); + } +} diff --git a/src/main/java/org/asamk/signal/JsonGroupInfo.java b/src/main/java/org/asamk/signal/JsonGroupInfo.java new file mode 100644 index 00000000..89c5515f --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonGroupInfo.java @@ -0,0 +1,24 @@ +package org.asamk.signal; + +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.internal.util.Base64; + +import java.util.List; + +class JsonGroupInfo { + String groupId; + List members; + String name; + String type; + + JsonGroupInfo(SignalServiceGroup groupInfo) { + this.groupId = Base64.encodeBytes(groupInfo.getGroupId()); + if (groupInfo.getMembers().isPresent()) { + this.members = groupInfo.getMembers().get(); + } + if (groupInfo.getName().isPresent()) { + this.name = groupInfo.getName().get(); + } + this.type = groupInfo.getType().toString(); + } +} diff --git a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/JsonMessageEnvelope.java new file mode 100644 index 00000000..2ce39cc5 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonMessageEnvelope.java @@ -0,0 +1,36 @@ +package org.asamk.signal; + +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +class JsonMessageEnvelope { + String source; + int sourceDevice; + String relay; + long timestamp; + boolean isReceipt; + JsonDataMessage dataMessage; + JsonSyncMessage syncMessage; + JsonCallMessage callMessage; + + public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content) { + SignalServiceAddress source = envelope.getSourceAddress(); + this.source = source.getNumber(); + this.sourceDevice = envelope.getSourceDevice(); + this.relay = source.getRelay().isPresent() ? source.getRelay().get() : null; + this.timestamp = envelope.getTimestamp(); + this.isReceipt = envelope.isReceipt(); + if (content != null) { + if (content.getDataMessage().isPresent()) { + this.dataMessage = new JsonDataMessage(content.getDataMessage().get()); + } + if (content.getSyncMessage().isPresent()) { + this.syncMessage = new JsonSyncMessage(content.getSyncMessage().get()); + } + if (content.getCallMessage().isPresent()) { + this.callMessage = new JsonCallMessage(content.getCallMessage().get()); + } + } + } +} diff --git a/src/main/java/org/asamk/signal/JsonSyncMessage.java b/src/main/java/org/asamk/signal/JsonSyncMessage.java new file mode 100644 index 00000000..92ad1cc5 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonSyncMessage.java @@ -0,0 +1,24 @@ +package org.asamk.signal; + +import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; + +import java.util.List; + +class JsonSyncMessage { + JsonDataMessage sentMessage; + List blockedNumbers; + List readMessages; + + JsonSyncMessage(SignalServiceSyncMessage syncMessage) { + if (syncMessage.getSent().isPresent()) { + this.sentMessage = new JsonDataMessage(syncMessage.getSent().get().getMessage()); + } + if (syncMessage.getBlockedList().isPresent()) { + this.blockedNumbers = syncMessage.getBlockedList().get().getNumbers(); + } + if (syncMessage.getRead().isPresent()) { + this.readMessages = syncMessage.getRead().get(); + } + } +} diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 9acb9fb6..afd5a515 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -16,6 +16,13 @@ */ package org.asamk.signal; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; @@ -420,7 +427,8 @@ public class Main { } boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); try { - m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, ignoreAttachments, new ReceiveMessageHandler(m)); + final Manager.ReceiveMessageHandler handler = ns.getBoolean("json") ? new JsonReceiveMessageHandler(m) : new ReceiveMessageHandler(m); + m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, ignoreAttachments, handler); } catch (IOException e) { System.err.println("Error while receiving messages: " + e.getMessage()); return 3; @@ -822,6 +830,9 @@ public class Main { parserReceive.addArgument("--ignore-attachments") .help("Don’t download attachments of received messages.") .action(Arguments.storeTrue()); + parserReceive.addArgument("--json") + .help("Output received messages in json format, one json object per line.") + .action(Arguments.storeTrue()); Subparser parserDaemon = subparsers.addParser("daemon"); parserDaemon.addArgument("--system") @@ -1116,7 +1127,37 @@ public class Main { } } } + } + private static class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler { + final Manager m; + final ObjectMapper jsonProcessor; + + public JsonReceiveMessageHandler(Manager m) { + this.m = m; + this.jsonProcessor = new ObjectMapper(); + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect + jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); + jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + } + + @Override + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { + ObjectNode result = jsonProcessor.createObjectNode(); + if (exception != null) { + result.putPOJO("error", new JsonError(exception)); + } + if (envelope != null) { + result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content)); + } + try { + jsonProcessor.writeValue(System.out, result); + System.out.println(); + } catch (IOException e) { + e.printStackTrace(); + } + } } private static String formatTimestamp(long timestamp) { diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 885fdfb3..0085f0c7 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -8,7 +8,10 @@ import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.SignalProtocolAddress; -import org.whispersystems.libsignal.state.*; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SessionRecord; +import org.whispersystems.libsignal.state.SignalProtocolStore; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; import java.util.List; import java.util.Map; From ee1e52aa4c46c60007627e0dacdf14823d0c2407 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 18 Jun 2017 12:04:56 +0200 Subject: [PATCH 0221/2005] Update man page --- man/signal-cli.1.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index f1e7fca1..1e412ebb 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -136,6 +136,8 @@ attachments are downloaded to the config directory. Default is 5 seconds. *--ignore-attachments*:: Don’t download attachments of received messages. +*--json*:: + Output received messages in json format, one object per line. updateGroup ~~~~~~~~~~~ From 804949ddea13e4201310b219bf9c1fd9f4a2fe56 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 18 Jun 2017 12:53:06 +0200 Subject: [PATCH 0222/2005] Send dbus signal, when receipt is received Fixes #84 --- src/main/java/org/asamk/Signal.java | 19 +++++++++++++++++++ src/main/java/org/asamk/signal/Main.java | 19 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 9eb9b4cd..ab926d3f 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -65,4 +65,23 @@ public interface Signal extends DBusInterface { return attachments; } } + + class ReceiptReceived extends DBusSignal { + private long timestamp; + private String sender; + + public ReceiptReceived(String objectpath, long timestamp, String sender) throws DBusException { + super(objectpath, timestamp, sender); + this.timestamp = timestamp; + this.sender = sender; + } + + public long getTimestamp() { + return timestamp; + } + + public String getSender() { + return sender; + } + } } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index afd5a515..45adf11f 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -397,6 +397,13 @@ public class Main { System.out.println(); } }); + dBusConn.addSigHandler(Signal.ReceiptReceived.class, new DBusSigHandler() { + @Override + public void handle(Signal.ReceiptReceived s) { + System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", + s.getSender(), formatTimestamp(s.getTimestamp()))); + } + }); } catch (UnsatisfiedLinkError e) { System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); return 1; @@ -1098,7 +1105,17 @@ public class Main { public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { super.handleMessage(envelope, content, exception); - if (!envelope.isReceipt() && content != null && content.getDataMessage().isPresent()) { + if (envelope.isReceipt()) { + try { + conn.sendSignal(new Signal.ReceiptReceived( + SIGNAL_OBJECTPATH, + envelope.getTimestamp(), + envelope.getSource() + )); + } catch (DBusException e) { + e.printStackTrace(); + } + } else if (content != null && content.getDataMessage().isPresent()) { SignalServiceDataMessage message = content.getDataMessage().get(); if (!message.isEndSession() && From 9b56aa625959f3bc8d0fa6bedc4be52b4862b053 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 17 Aug 2017 21:31:07 +0200 Subject: [PATCH 0223/2005] Update libsignal-service --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 54783 -> 54712 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 6 +- src/main/java/org/asamk/signal/Main.java | 11 ++-- src/main/java/org/asamk/signal/Manager.java | 64 ++++++-------------- 6 files changed, 30 insertions(+), 57 deletions(-) diff --git a/build.gradle b/build.gradle index 5885ce5b..5c1470ee 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.5.11_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.5.17_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 88731cec4ecf6571ec00dc24d749961d2d3347b4..88a5f3eb505fa1aa2905305128984dc872f1c1f6 100644 GIT binary patch delta 17164 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 It|K5Z0O~y7kN^Mx diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 29edf730..41d45baf 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat May 20 11:26:11 CEST 2017 +#Thu Aug 17 20:18:16 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0.2-bin.zip diff --git a/gradlew b/gradlew index 4453ccea..cccdd3d5 100755 --- a/gradlew +++ b/gradlew @@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -155,7 +155,7 @@ if $cygwin ; then fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 45adf11f..de076a87 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -1012,13 +1012,10 @@ public class Main { } if (syncMessage.getVerified().isPresent()) { System.out.println("Received sync message with verified identities:"); - final List verifiedList = syncMessage.getVerified().get(); - for (VerifiedMessage v : verifiedList) { - System.out.println(" - " + v.getDestination() + ": " + v.getVerified()); - String safetyNumber = formatSafetyNumber(m.computeSafetyNumber(v.getDestination(), v.getIdentityKey())); - System.out.println(" " + safetyNumber); - } - + final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); + System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified()); + String safetyNumber = formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey())); + System.out.println(" " + safetyNumber); } } } diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index dc979930..f159e412 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -368,7 +368,7 @@ class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false, true); + accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true); } public void unregister() throws IOException { @@ -481,24 +481,6 @@ class Manager implements Signal { return records; } - private PreKeyRecord getOrGenerateLastResortPreKey() { - if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) { - try { - return signalProtocolStore.loadPreKey(Medium.MAX_VALUE); - } catch (InvalidKeyIdException e) { - signalProtocolStore.removePreKey(Medium.MAX_VALUE); - } - } - - ECKeyPair keyPair = Curve.generateKeyPair(); - PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair); - - signalProtocolStore.storePreKey(Medium.MAX_VALUE, record); - save(); - - return record; - } - private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) { try { ECKeyPair keyPair = Curve.generateKeyPair(); @@ -518,7 +500,7 @@ class Manager implements Signal { public void verifyAccount(String verificationCode) throws IOException { verificationCode = verificationCode.replace("-", ""); signalingKey = Util.getSecret(52); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false, true); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; @@ -529,10 +511,9 @@ class Manager implements Signal { private void refreshPreKeys() throws IOException { List oneTimePreKeys = generatePreKeys(); - PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey(); SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair()); - accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); + accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), signedPreKeyRecord, oneTimePreKeys); } @@ -1203,7 +1184,6 @@ class Manager implements Signal { if (rm.isContactsRequest()) { try { sendContacts(); - sendVerifiedMessage(); } catch (UntrustedIdentityException | IOException e) { e.printStackTrace(); } @@ -1298,10 +1278,8 @@ class Manager implements Signal { } } if (syncMessage.getVerified().isPresent()) { - final List verifiedList = syncMessage.getVerified().get(); - for (VerifiedMessage v : verifiedList) { - signalProtocolStore.saveIdentity(v.getDestination(), v.getIdentityKey(), TrustLevel.fromVerifiedState(v.getVerified())); - } + final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); + signalProtocolStore.saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } } } @@ -1513,8 +1491,21 @@ class Manager implements Signal { try (OutputStream fos = new FileOutputStream(contactsFile)) { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : contactStore.getContacts()) { + VerifiedMessage verifiedMessage = null; + if (getIdentities().containsKey(record.number)) { + JsonIdentityKeyStore.Identity currentIdentity = null; + for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) { + if (currentIdentity == null || id.getDateAdded().after(currentIdentity.getDateAdded())) { + currentIdentity = id; + } + } + if (currentIdentity != null) { + verifiedMessage = new VerifiedMessage(record.number, currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); + } + } + out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), - createContactAvatarAttachment(record.number), Optional.fromNullable(record.color))); + createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), Optional.fromNullable(verifiedMessage))); } } @@ -1538,23 +1529,8 @@ class Manager implements Signal { } } - private void sendVerifiedMessage() throws IOException, UntrustedIdentityException { - List verifiedMessages = new LinkedList<>(); - for (Map.Entry> x : getIdentities().entrySet()) { - final String name = x.getKey(); - for (JsonIdentityKeyStore.Identity id : x.getValue()) { - if (id.getTrustLevel() == TrustLevel.TRUSTED_UNVERIFIED) { - continue; - } - VerifiedMessage verifiedMessage = new VerifiedMessage(name, id.getIdentityKey(), id.getTrustLevel().toVerifiedState()); - verifiedMessages.add(verifiedMessage); - } - } - sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessages)); - } - private void sendVerifiedMessage(String destination, IdentityKey identityKey, TrustLevel trustLevel) throws IOException, UntrustedIdentityException { - VerifiedMessage verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState()); + VerifiedMessage verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState(), System.currentTimeMillis()); sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } From e36a2f862c996825944927c6e24bf8521edd6edb Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Sep 2017 22:18:08 +0200 Subject: [PATCH 0224/2005] Update libsignal-service-java --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 54712 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- src/main/java/org/asamk/signal/Manager.java | 33 +++++++++++++------- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index 5c1470ee..9b9e208a 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.5.17_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.6.5_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 88a5f3eb505fa1aa2905305128984dc872f1c1f6..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 100644 GIT binary patch delta 673 zcmYL{T}YEr9LC@CS2iPM?-E7dDatC-SEkOe=9HWi7A|cvi!7}mR_kk$kr%=)BqA>g zwTIC+k+YNsInN&s|1%lPmm+E8Eh%)}BF_bAu%@Q_3^@dEz<l3g#bM?1)#;Dy%C z*08Rfp_Kwr&n_$#^@k<$_J3tb8p5lYQ&ern_>2i&vk|sgm5ME*;j$Po#HGGU$o4ut zVzaems!Pipl+CHkkB+&cBs+!K)|La8vj8h|Dk*JgD;Jh|m0CK5^vxfpvMzy<`9q9g zTsTe!4=kSDxVNA(A0`(X>6r&_J$qqD%BYVhRPN=n5NEx+gf+&k=v55*itr`UOy_+Z zD@AfLs!*sG-+Ig8NXU4}<)fRpKDZOVqE@=yFHq}OaQd@KrU4Wmd>#aSeGwT?5UXcak+u^JQr9RKSG`{j%M7A*Dx9H<8Jl?#zYtUjVpqWOon)o1DZFX^1&JE}ZvX%Q delta 808 zcmYk4Ur3Wt7{ArK}A1sdDbHrMF`|aanBvx>N?QNSF>+*V zC=D*noS1KZE-ewcr#;3D%6w+SW&V8ZRM#jzg)m`<(Z=Lu`3nccM15$ zr9;~Xg?75REkLc?B#vOM(eKzguq2uNk$Q49h=SJ08M1nXoQs^I^vgmPMxA76hP}B6 z52FgZjX>1FMz9{e!W7iaU#IvDyy`IH(|k2G-xBg~!h(jFOyM?Rdt-;`0qiKN=DJ>phnahc9{i;%y&2sfAXcpaD7PrQ$}lCf8S zx`oq}+mF(ICt{p`U67d;Y_W+h2ZZzt=tSG}!PH^>c_CosU=H6vp`{_w zHhb8>`zd4_5!SryqLxu~k6O{U%qKd5tOTFvgi190{VsK)-;tT{(vK-absent(), verificationCode); } private List generatePreKeys() { @@ -856,7 +863,7 @@ class Manager implements Signal { private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password, + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.absent()); try { messageSender.sendMessage(message); @@ -873,7 +880,7 @@ class Manager implements Signal { SignalServiceDataMessage message = null; try { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password, + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password, deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.absent()); message = messageBuilder.build(); @@ -1104,7 +1111,7 @@ class Manager implements Signal { public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT); try { if (messagePipe == null) { @@ -1406,7 +1413,7 @@ class Manager implements Signal { } } - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT); File tmpFile = Util.createTempFile(); try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) { @@ -1432,7 +1439,7 @@ class Manager implements Signal { } private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT); return messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE); } @@ -1504,8 +1511,10 @@ class Manager implements Signal { } } + // TODO include profile key out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), - createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), Optional.fromNullable(verifiedMessage))); + createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), + Optional.fromNullable(verifiedMessage), Optional.absent())); } } From d12cfca155ebc39ac159d8f611bfdd8df645922b Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 29 Dec 2017 14:47:18 +0100 Subject: [PATCH 0225/2005] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 54708 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7a3265ee94c0ab25cf079ac8ccdf87f41d455d42..01b8bf6b1f99cad9213fc495b33ad5bbab8efd20 100644 GIT binary patch delta 15814 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f16d2666..933b6473 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-bin.zip From c5d91933b880ac18d1c88fe6f8bbed32bd5a8aa1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 29 Dec 2017 14:51:58 +0100 Subject: [PATCH 0226/2005] Update codeStyle config --- .gitignore | 4 ++-- .idea/codeStyleSettings.xml | 15 --------------- .idea/codeStyles/Project.xml | 11 +++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 +++++ 4 files changed, 18 insertions(+), 17 deletions(-) delete mode 100644 .idea/codeStyleSettings.xml create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml diff --git a/.gitignore b/.gitignore index 96f13754..32ed5933 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .gradle/ -.idea/ +.idea/* +!.idea/codeStyles/ build/ *~ *.swp @@ -8,4 +9,3 @@ local.properties .classpath .project .settings/ - diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml deleted file mode 100644 index f62dae59..00000000 --- a/.idea/codeStyleSettings.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..1c207237 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file From e63e6e1fa2b90ba68a5351029a365bf477393301 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 29 Dec 2017 14:56:03 +0100 Subject: [PATCH 0227/2005] Update libsignal-service-java --- build.gradle | 2 +- src/main/java/org/asamk/signal/Manager.java | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 9b9e208a..115c03f8 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.6.5_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.6.12_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index adc31f70..585da2e8 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1111,7 +1111,7 @@ class Manager implements Signal { public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null); try { if (messagePipe == null) { @@ -1413,7 +1413,7 @@ class Manager implements Signal { } } - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null); File tmpFile = Util.createTempFile(); try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) { @@ -1439,7 +1439,7 @@ class Manager implements Signal { } private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null); return messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE); } From d5fb37a416c1688a2c219f8e722ad9a87406fe70 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 29 Dec 2017 15:01:21 +0100 Subject: [PATCH 0228/2005] Update argparse4j --- build.gradle | 2 +- src/main/java/org/asamk/signal/Main.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 115c03f8..00f2c041 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ repositories { dependencies { compile 'com.github.turasa:signal-service-java:2.6.12_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' - compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' + compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index de076a87..69a46193 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -720,7 +720,8 @@ public class Main { } private static Namespace parseArgs(String[] args) { - ArgumentParser parser = ArgumentParsers.newArgumentParser("signal-cli") + ArgumentParser parser = ArgumentParsers.newFor("signal-cli") + .build() .defaultHelp(true) .description("Commandline interface for Signal.") .version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION); From 139fc358a2b6abd8df602a3fb84c2212a0b96352 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 21 Jan 2018 23:16:57 +0100 Subject: [PATCH 0229/2005] Add output for new message fields --- src/main/java/org/asamk/signal/Main.java | 53 +++++++++++++++++++++ src/main/java/org/asamk/signal/Manager.java | 6 +-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 69a46193..57a25345 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -39,6 +39,7 @@ import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.calls.*; import org.whispersystems.signalservice.api.messages.multidevice.*; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; @@ -1018,6 +1019,54 @@ public class Main { String safetyNumber = formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey())); System.out.println(" " + safetyNumber); } + if (syncMessage.getConfiguration().isPresent()) { + System.out.println("Received sync message with configuration:"); + final ConfigurationMessage configurationMessage = syncMessage.getConfiguration().get(); + if (configurationMessage.getReadReceipts().isPresent()) { + System.out.println(" - Read receipts: " + (configurationMessage.getReadReceipts().get() ? "enabled" : "disabled")); + } + } + } + if (content.getCallMessage().isPresent()) { + System.out.println("Received a call message"); + SignalServiceCallMessage callMessage = content.getCallMessage().get(); + if (callMessage.getAnswerMessage().isPresent()) { + AnswerMessage answerMessage = callMessage.getAnswerMessage().get(); + System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getDescription()); + } + if (callMessage.getBusyMessage().isPresent()) { + BusyMessage busyMessage = callMessage.getBusyMessage().get(); + System.out.println("Busy message: " + busyMessage.getId()); + } + if (callMessage.getHangupMessage().isPresent()) { + HangupMessage hangupMessage = callMessage.getHangupMessage().get(); + System.out.println("Hangup message: " + hangupMessage.getId()); + } + if (callMessage.getIceUpdateMessages().isPresent()) { + List iceUpdateMessages = callMessage.getIceUpdateMessages().get(); + for (IceUpdateMessage iceUpdateMessage : iceUpdateMessages) { + System.out.println("Ice update message: " + iceUpdateMessage.getId() + ", sdp: " + iceUpdateMessage.getSdp()); + } + } + if (callMessage.getOfferMessage().isPresent()) { + OfferMessage offerMessage = callMessage.getOfferMessage().get(); + System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getDescription()); + } + } + if (content.getReceiptMessage().isPresent()) { + System.out.println("Received a receipt message"); + SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get(); + System.out.println(" - When: " + formatTimestamp(receiptMessage.getWhen())); + if (receiptMessage.isDeliveryReceipt()) { + System.out.println(" - Is delivery receipt"); + } + if (receiptMessage.isReadReceipt()) { + System.out.println(" - Is read receipt"); + } + System.out.println(" - Timestamps:"); + for (long timestamp : receiptMessage.getTimestamps()) { + System.out.println(" " + formatTimestamp(timestamp)); + } } } } else { @@ -1066,6 +1115,9 @@ public class Main { if (message.getExpiresInSeconds() > 0) { System.out.println("Expires in: " + message.getExpiresInSeconds() + " seconds"); } + if (message.isProfileKeyUpdate() && message.getProfileKey().isPresent()) { + System.out.println("Profile key update, key length:" + message.getProfileKey().get().length); + } if (message.getAttachments().isPresent()) { System.out.println("Attachments: "); @@ -1083,6 +1135,7 @@ public class Main { System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-")); System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); System.out.println(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no")); + System.out.println(" Dimensions: " + pointer.getWidth() + "x" + pointer.getHeight()); File file = m.getAttachmentFile(pointer.getId()); if (file.exists()) { System.out.println(" Stored plaintext in: " + file); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 585da2e8..5d5b45d4 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -547,7 +547,7 @@ class Manager implements Signal { mime = "application/octet-stream"; } // TODO mabybe add a parameter to set the voiceNote and preview option - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, Optional.absent(), null); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, Optional.absent(), 0, 0, null); } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -1467,7 +1467,7 @@ class Manager implements Signal { for (GroupInfo record : groupStore.getGroups()) { out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId), - record.active)); + record.active, Optional.absent())); } } @@ -1514,7 +1514,7 @@ class Manager implements Signal { // TODO include profile key out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), - Optional.fromNullable(verifiedMessage), Optional.absent())); + Optional.fromNullable(verifiedMessage), Optional.absent(), false, Optional.absent())); } } From 161ecc877d4d669f78553b4b9837ec877746b483 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 2 Feb 2018 22:27:43 +0100 Subject: [PATCH 0230/2005] Update dependencies --- .gitignore | 1 + build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 32ed5933..3dc9875b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ local.properties .classpath .project .settings/ +out/ diff --git a/build.gradle b/build.gradle index 00f2c041..95717c68 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.6.12_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.7.1_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.55' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 01b8bf6b1f99cad9213fc495b33ad5bbab8efd20..a5fe1cb94b9ee5ce57e6113458225bcba12d83e3 100644 GIT binary patch delta 63 zcmdnFf_di(<_YF3Kg`qiPqdC?`&9I?h>>A})W$9U4sx+F1bDM^1n1Aqn!M+bKUmFX P{=@eKSinLbFM9w0VoDpr delta 63 zcmdnFf_di(<_YF39&&6=6RjiJJ`{Z{Vq};gwQ Date: Sat, 31 Mar 2018 23:41:58 +0200 Subject: [PATCH 0231/2005] Support registration lock PIN --- build.gradle | 4 +- src/main/java/org/asamk/signal/Main.java | 51 ++++++++++++++++++++- src/main/java/org/asamk/signal/Manager.java | 20 ++++++-- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/build.gradle b/build.gradle index 95717c68..39612ba8 100644 --- a/build.gradle +++ b/build.gradle @@ -19,8 +19,8 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.7.1_unofficial_1' - compile 'org.bouncycastle:bcprov-jdk15on:1.55' + compile 'com.github.turasa:signal-service-java:2.7.3_unofficial_1' + compile 'org.bouncycastle:bcprov-jdk15on:1.59' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 57a25345..59689049 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -37,6 +37,7 @@ import org.freedesktop.dbus.DBusSigHandler; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; import org.whispersystems.signalservice.api.messages.calls.*; @@ -46,6 +47,7 @@ import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptio import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.internal.push.LockedException; import org.whispersystems.signalservice.internal.util.Base64; import java.io.File; @@ -183,6 +185,39 @@ public class Main { return 3; } break; + case "setPin": + if (dBusConn != null) { + System.err.println("setPin is not yet implemented via dbus"); + return 1; + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + String registrationLockPin = ns.getString("registrationLockPin"); + m.setRegistrationLockPin(Optional.of(registrationLockPin)); + } catch (IOException e) { + System.err.println("Set pin error: " + e.getMessage()); + return 3; + } + break; + case "removePin": + if (dBusConn != null) { + System.err.println("removePin is not yet implemented via dbus"); + return 1; + } + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + m.setRegistrationLockPin(Optional.absent()); + } catch (IOException e) { + System.err.println("Remove pin error: " + e.getMessage()); + return 3; + } + break; case "verify": if (dBusConn != null) { System.err.println("verify is not yet implemented via dbus"); @@ -197,7 +232,13 @@ public class Main { return 1; } try { - m.verifyAccount(ns.getString("verificationCode")); + String verificationCode = ns.getString("verificationCode"); + String pin = ns.getString("pin"); + m.verifyAccount(verificationCode, pin); + } catch (LockedException e) { + System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: " + (e.getTimeRemaining() / 1000 / 60 / 60)); + System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN"); + return 3; } catch (IOException e) { System.err.println("Verify error: " + e.getMessage()); return 3; @@ -777,9 +818,17 @@ public class Main { Subparser parserUpdateAccount = subparsers.addParser("updateAccount"); parserUpdateAccount.help("Update the account attributes on the signal server."); + Subparser parserSetPin = subparsers.addParser("setPin"); + parserSetPin.addArgument("registrationLockPin") + .help("The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity)"); + + Subparser parserRemovePin = subparsers.addParser("removePin"); + Subparser parserVerify = subparsers.addParser("verify"); parserVerify.addArgument("verificationCode") .help("The verification code you received via sms or voice call."); + parserVerify.addArgument("-p", "--pin") + .help("The registration lock PIN, that was set by the user (Optional)"); Subparser parserSend = subparsers.addParser("send"); parserSend.addArgument("-g", "--group") diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 5d5b45d4..46c2e64c 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -116,6 +116,7 @@ class Manager implements Signal { private String username; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private String password; + private String registrationLockPin; private String signalingKey; private int preKeyIdOffset; private int nextSignedPreKeyId; @@ -258,6 +259,8 @@ class Manager implements Signal { } username = getNotNullNode(rootNode, "username").asText(); password = getNotNullNode(rootNode, "password").asText(); + JsonNode pinNode = rootNode.get("registrationLockPin"); + registrationLockPin = pinNode == null ? null : pinNode.asText(); if (rootNode.has("signalingKey")) { signalingKey = getNotNullNode(rootNode, "signalingKey").asText(); } @@ -326,6 +329,7 @@ class Manager implements Signal { rootNode.put("username", username) .put("deviceId", deviceId) .put("password", password) + .put("registrationLockPin", registrationLockPin) .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) @@ -374,7 +378,7 @@ class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true); + accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true, registrationLockPin); } public void unregister() throws IOException { @@ -504,18 +508,28 @@ class Manager implements Signal { } } - public void verifyAccount(String verificationCode) throws IOException { + public void verifyAccount(String verificationCode, String pin) throws IOException { verificationCode = verificationCode.replace("-", ""); signalingKey = Util.getSecret(52); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true, pin); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; + registrationLockPin = pin; refreshPreKeys(); save(); } + public void setRegistrationLockPin(Optional pin) throws IOException { + accountManager.setPin(pin); + if (pin.isPresent()) { + registrationLockPin = pin.get(); + } else { + registrationLockPin = null; + } + } + private void refreshPreKeys() throws IOException { List oneTimePreKeys = generatePreKeys(); SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair()); From 925d8db468ce39c0e2b164cc1ab464ea2edf4e86 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 31 Mar 2018 23:50:33 +0200 Subject: [PATCH 0232/2005] Update man page --- man/signal-cli.1.adoc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 1e412ebb..27f9c82e 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -66,6 +66,9 @@ Verify the number using the code received via SMS or voice. VERIFICATIONCODE:: The verification code. +*-p* PIN, *--pin* PIN:: + The registration lock PIN, that was set by the user. Only required if a PIN was set. + unregister ~~~~~~~~~~ Disable push support for this device, i.e. this device won't receive any more messages. @@ -78,6 +81,17 @@ updateAccount Update the account attributes on the signal server. Can fix problems with receiving messages. +setPin +~~~~~~ +Set a registration lock pin, to prevent others from registering this number. + +REGISTRATION_LOCK_PIN:: + The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity) + +removePin +~~~~~~ +Remove the registration lock pin. + link ~~~~ Link to an existing device, instead of registering a new number. This shows a From 8127eaaf9ddd04163e5dc9276921cd695e0be3b9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 3 May 2018 21:49:52 +0200 Subject: [PATCH 0233/2005] Update dependencies, gradle wrapper --- build.gradle | 39 ++++++++++++++++++++++- gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 54413 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 39612ba8..b2f93e27 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ version = '0.5.6' compileJava.options.encoding = 'UTF-8' repositories { + mavenLocal() maven { url "https://raw.github.com/AsamK/maven/master/releases/" } @@ -19,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.7.3_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.7.5_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.59' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' @@ -34,3 +35,39 @@ jar { ) } } + +// Find any 3rd party libraries which have released new versions +// to the central Maven repo since we last upgraded. +// http://daniel.gredler.net/2011/08/08/gradle-keeping-libraries-up-to-date/ +task checkLibVersions << { + def checked = [:] + allprojects { + configurations.each { configuration -> + configuration.allDependencies.each { dependency -> + def version = dependency.version + if (!version.contains('SNAPSHOT') && !checked[dependency]) { + def group = dependency.group + def path = group.replace('.', '/') + def name = dependency.name + def url = "http://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" + try { + def metadata = new XmlSlurper().parseText(url.toURL().text) + def versions = metadata.versioning.versions.version.collect { it.text() } + versions.removeAll { it.toLowerCase().contains('alpha') } + versions.removeAll { it.toLowerCase().contains('beta') } + versions.removeAll { it.toLowerCase().contains('rc') } + def newest = versions.max() + if (version != newest) { + println "$group:$name $version -> $newest" + } + } catch (FileNotFoundException e) { + logger.debug "Unable to download $url: $e.message" + } catch (org.xml.sax.SAXParseException e) { + logger.debug "Unable to parse $url: $e.message" + } + checked[dependency] = true + } + } + } + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a5fe1cb94b9ee5ce57e6113458225bcba12d83e3..91ca28c8b802289c3a438766657a5e98f20eff03 100644 GIT binary patch delta 7397 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 delta 7284 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 Date: Thu, 3 May 2018 21:53:04 +0200 Subject: [PATCH 0234/2005] Print quotes from messages --- src/main/java/org/asamk/signal/Main.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 59689049..2c955753 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -1168,6 +1168,22 @@ public class Main { System.out.println("Profile key update, key length:" + message.getProfileKey().get().length); } + if (message.getQuote().isPresent()) { + SignalServiceDataMessage.Quote quote = message.getQuote().get(); + System.out.println("Quote: (" + quote.getId() + ")"); + System.out.println(" Author: " + quote.getAuthor().getNumber()); + System.out.println(" Text: " + quote.getText()); + if (quote.getAttachments().size() > 0) { + System.out.println(" Attachments: "); + for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : quote.getAttachments()) { + System.out.println(" Filename: " + attachment.getFileName()); + System.out.println(" Type: " + attachment.getContentType()); + System.out.println(" Thumbnail:"); + printAttachment(attachment.getThumbnail()); + } + } + } + if (message.getAttachments().isPresent()) { System.out.println("Attachments: "); for (SignalServiceAttachment attachment : message.getAttachments().get()) { From 86f5c9947b13dc25cff958fc73736f3ff565b744 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 3 May 2018 22:01:49 +0200 Subject: [PATCH 0235/2005] Version 0.6.0 - Simple json output - dbus signal for receiving messages - Registration lock PIN - Output quoted message --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b2f93e27..cc1376e7 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.5.6' +version = '0.6.0' compileJava.options.encoding = 'UTF-8' From bdffcffd7aa791449bf7fe4af0084882dfd1425d Mon Sep 17 00:00:00 2001 From: nico Date: Fri, 22 Sep 2017 01:17:40 +0200 Subject: [PATCH 0236/2005] Add getGroupIds() to DBUS getGroupIds() returns a list of group ids (byte arrays) --- src/main/java/org/asamk/Signal.java | 2 ++ src/main/java/org/asamk/signal/Manager.java | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index ab926d3f..88b15926 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -23,6 +23,8 @@ public interface Signal extends DBusInterface { void setContactName(String number, String name); + List getGroupIds(); + String getGroupName(byte[] groupId); List getGroupMembers(byte[] groupId); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 46c2e64c..51acaf1d 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -818,6 +818,16 @@ class Manager implements Signal { save(); } + @Override + public List getGroupIds() { + List groups = getGroups(); + List ids = new ArrayList(groups.size()); + for (GroupInfo group : groups) { + ids.add(group.groupId); + } + return ids; + } + @Override public String getGroupName(byte[] groupId) { GroupInfo group = getGroup(groupId); From 3bcc2fa6212e757dfa3e1385d39a7888487c4a87 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 8 May 2018 22:43:18 +0200 Subject: [PATCH 0237/2005] Add travis build --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..367c1ef7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: java +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ From a44034a79e6a98e7d77bbde0af1483d8c30a22c6 Mon Sep 17 00:00:00 2001 From: Riamse Date: Sun, 6 May 2018 14:21:09 -0400 Subject: [PATCH 0238/2005] Add command line argument for JSON output in daemon --- src/main/java/org/asamk/signal/Main.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 2c955753..3b2fbadb 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -899,6 +899,9 @@ public class Main { parserDaemon.addArgument("--ignore-attachments") .help("Don’t download attachments of received messages.") .action(Arguments.storeTrue()); + parserDaemon.addArgument("--json") + .help("Output received messages in json format, one json object per line.") + .action(Arguments.storeTrue()); try { Namespace ns = parser.parseArgs(args); From afe18ea5acfc5cbc6d84e916c4267df8fc9f2d1c Mon Sep 17 00:00:00 2001 From: Riamse Date: Sun, 6 May 2018 14:37:55 -0400 Subject: [PATCH 0239/2005] Gradle option to pass command line arguments to Java application --- build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/build.gradle b/build.gradle index cc1376e7..37f97ce5 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,12 @@ jar { } } +run { + if (project.hasProperty("appArgs")) { + args Eval.me(appArgs) + } +} + // Find any 3rd party libraries which have released new versions // to the central Maven repo since we last upgraded. // http://daniel.gredler.net/2011/08/08/gradle-keeping-libraries-up-to-date/ From fbbd194e40d0051989431b01dfe7c38ddd50203e Mon Sep 17 00:00:00 2001 From: Riamse Date: Sun, 6 May 2018 14:38:15 -0400 Subject: [PATCH 0240/2005] Duct tape solution to make daemon actually output JSON --- src/main/java/org/asamk/signal/Main.java | 55 +++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 3b2fbadb..d50b4395 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -683,7 +683,7 @@ public class Main { } ignoreAttachments = ns.getBoolean("ignore_attachments"); try { - m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, new DbusReceiveMessageHandler(m, conn)); + m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, ns.getBoolean("json") ? new JsonDbusReceiveMessageHandler(m, conn) : new DbusReceiveMessageHandler(m, conn)); } catch (IOException e) { System.err.println("Error while receiving messages: " + e.getMessage()); return 3; @@ -1296,6 +1296,59 @@ public class Main { } } + private static class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { + final DBusConnection conn; + + public JsonDbusReceiveMessageHandler(Manager m, DBusConnection conn) { + super(m); + this.conn = conn; + } + + @Override + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { + super.handleMessage(envelope, content, exception); + + if (envelope.isReceipt()) { + try { + conn.sendSignal(new Signal.ReceiptReceived( + SIGNAL_OBJECTPATH, + envelope.getTimestamp(), + envelope.getSource() + )); + } catch (DBusException e) { + e.printStackTrace(); + } + } else if (content != null && content.getDataMessage().isPresent()) { + SignalServiceDataMessage message = content.getDataMessage().get(); + + if (!message.isEndSession() && + !(message.getGroupInfo().isPresent() && + message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { + List attachments = new ArrayList<>(); + if (message.getAttachments().isPresent()) { + for (SignalServiceAttachment attachment : message.getAttachments().get()) { + if (attachment.isPointer()) { + attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); + } + } + } + + try { + conn.sendSignal(new Signal.MessageReceived( + SIGNAL_OBJECTPATH, + message.getTimestamp(), + envelope.getSource(), + message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], + message.getBody().isPresent() ? message.getBody().get() : "", + attachments)); + } catch (DBusException e) { + e.printStackTrace(); + } + } + } + } + } + private static String formatTimestamp(long timestamp) { Date date = new Date(timestamp); final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); // Quoted "Z" to indicate UTC, no timezone offset From 70c810ff177ab6a1e2e37093655e6feb94796d04 Mon Sep 17 00:00:00 2001 From: Riamse Date: Tue, 15 May 2018 22:29:04 -0400 Subject: [PATCH 0241/2005] Short comment to explain how to pass arguments to main application --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index 37f97ce5..3354d30e 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,8 @@ jar { run { if (project.hasProperty("appArgs")) { + // allow passing command-line arguments to the main application e.g.: + // $ gradle run -PappArgs="['-u', '+...', 'daemon', '--json']" args Eval.me(appArgs) } } From bebe7bc51333aad1a752f70d091d74cae2adbf02 Mon Sep 17 00:00:00 2001 From: Benedikt Constantin Radtke Date: Thu, 17 May 2018 22:35:05 +0200 Subject: [PATCH 0242/2005] Send correct expiry value in group and contact syncs --- src/main/java/org/asamk/signal/Manager.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 51acaf1d..505ee12d 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -1489,9 +1489,10 @@ class Manager implements Signal { try (OutputStream fos = new FileOutputStream(groupsFile)) { DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos); for (GroupInfo record : groupStore.getGroups()) { + ThreadInfo info = threadStore.getThread(Base64.encodeBytes(record.groupId)); out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId), - record.active, Optional.absent())); + record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null))); } } @@ -1523,6 +1524,7 @@ class Manager implements Signal { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); for (ContactInfo record : contactStore.getContacts()) { VerifiedMessage verifiedMessage = null; + ThreadInfo info = threadStore.getThread(record.number); if (getIdentities().containsKey(record.number)) { JsonIdentityKeyStore.Identity currentIdentity = null; for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) { @@ -1538,7 +1540,7 @@ class Manager implements Signal { // TODO include profile key out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), - Optional.fromNullable(verifiedMessage), Optional.absent(), false, Optional.absent())); + Optional.fromNullable(verifiedMessage), Optional.absent(), false, Optional.fromNullable(info != null ? info.messageExpirationTime : null))); } } From 611d82425d04621a50122330fa8e0b4a04ac8028 Mon Sep 17 00:00:00 2001 From: mqus <8398165+mqus@users.noreply.github.com> Date: Sun, 12 Aug 2018 01:49:23 +0200 Subject: [PATCH 0243/2005] Fix manpage --- man/signal-cli.1.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 27f9c82e..67e55bf5 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -89,7 +89,7 @@ REGISTRATION_LOCK_PIN:: The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity) removePin -~~~~~~ +~~~~~~~~~ Remove the registration lock pin. link From 8c2cd57a4fa4b42980513d4457c5c014c5518a68 Mon Sep 17 00:00:00 2001 From: kllp <33160282+kllp@users.noreply.github.com> Date: Thu, 9 Aug 2018 12:06:04 +0000 Subject: [PATCH 0244/2005] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d95b2601..3beb9572 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal- sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ ``` +[Install on Ubuntu](https://github.com/AsamK/signal-cli/wiki/HowToUbuntu) +[Use DBus Service](https://github.com/AsamK/signal-cli/wiki/DBus-service) ## Usage From 50fb5587ecf0d372343d32e05f2bda99db758b24 Mon Sep 17 00:00:00 2001 From: kllp <33160282+kllp@users.noreply.github.com> Date: Thu, 9 Aug 2018 12:07:14 +0000 Subject: [PATCH 0245/2005] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3beb9572..398c1bdf 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ wget https://github.com/AsamK/signal-cli/releases/download/v"${VERSION}"/signal- sudo tar xf signal-cli-"${VERSION}".tar.gz -C /opt sudo ln -sf /opt/signal-cli-"${VERSION}"/bin/signal-cli /usr/local/bin/ ``` -[Install on Ubuntu](https://github.com/AsamK/signal-cli/wiki/HowToUbuntu) -[Use DBus Service](https://github.com/AsamK/signal-cli/wiki/DBus-service) +You can find further instructions on the Wiki: +- [Install on Ubuntu](https://github.com/AsamK/signal-cli/wiki/HowToUbuntu) +- [DBus Service](https://github.com/AsamK/signal-cli/wiki/DBus-service) ## Usage From 12c296f9ec45cad9b81f1425b9fd30793a1a7d8f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Aug 2018 20:18:07 +0200 Subject: [PATCH 0246/2005] Add missing null check Fixes #142 --- src/main/java/org/asamk/signal/Main.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index d50b4395..8cbaa669 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -1182,7 +1182,9 @@ public class Main { System.out.println(" Filename: " + attachment.getFileName()); System.out.println(" Type: " + attachment.getContentType()); System.out.println(" Thumbnail:"); - printAttachment(attachment.getThumbnail()); + if (attachment.getThumbnail() != null) { + printAttachment(attachment.getThumbnail()); + } } } } From cafba8579f5f2f85951740d99914a6a1d6a6560b Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Aug 2018 20:18:18 +0200 Subject: [PATCH 0247/2005] Reduce duplicate code --- src/main/java/org/asamk/signal/Main.java | 43 +++--------------------- 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 8cbaa669..7fc520b9 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -1226,44 +1226,7 @@ public class Main { public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { super.handleMessage(envelope, content, exception); - if (envelope.isReceipt()) { - try { - conn.sendSignal(new Signal.ReceiptReceived( - SIGNAL_OBJECTPATH, - envelope.getTimestamp(), - envelope.getSource() - )); - } catch (DBusException e) { - e.printStackTrace(); - } - } else if (content != null && content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); - - if (!message.isEndSession() && - !(message.getGroupInfo().isPresent() && - message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { - List attachments = new ArrayList<>(); - if (message.getAttachments().isPresent()) { - for (SignalServiceAttachment attachment : message.getAttachments().get()) { - if (attachment.isPointer()) { - attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); - } - } - } - - try { - conn.sendSignal(new Signal.MessageReceived( - SIGNAL_OBJECTPATH, - message.getTimestamp(), - envelope.getSource(), - message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], - message.getBody().isPresent() ? message.getBody().get() : "", - attachments)); - } catch (DBusException e) { - e.printStackTrace(); - } - } - } + JsonDbusReceiveMessageHandler.sendReceivedMessageToDbus(envelope, content, conn, m); } } @@ -1310,6 +1273,10 @@ public class Main { public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { super.handleMessage(envelope, content, exception); + sendReceivedMessageToDbus(envelope, content, conn, m); + } + + private static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, Manager m) { if (envelope.isReceipt()) { try { conn.sendSignal(new Signal.ReceiptReceived( From bb342babba6bf716375ef4ba65919f80258aa29f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Aug 2018 21:34:55 +0200 Subject: [PATCH 0248/2005] Update signal-service-java to 2.8.0 --- build.gradle | 2 +- src/main/java/org/asamk/signal/Manager.java | 22 +++++++++++++------ .../signal/storage/groups/GroupInfo.java | 6 ++++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 3354d30e..d2fddabe 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.7.5_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.8.0_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.59' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 505ee12d..8a606edd 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -61,6 +61,8 @@ import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.exceptions.*; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.util.SleepTimer; +import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; @@ -130,6 +132,8 @@ class Manager implements Signal { private JsonThreadStore threadStore; private SignalServiceMessagePipe messagePipe = null; + private SleepTimer timer = new UptimeSleepTimer(); + public Manager(String username, String settingsPath) { this.username = username; this.settingsPath = settingsPath; @@ -238,7 +242,7 @@ class Manager implements Signal { migrateLegacyConfigs(); - accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, deviceId, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, deviceId, USER_AGENT, timer); try { if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { refreshPreKeys(); @@ -366,7 +370,7 @@ class Manager implements Signal { public void register(boolean voiceVerification) throws IOException { password = Util.getSecret(18); - accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT, timer); if (voiceVerification) accountManager.requestVoiceVerificationCode(); @@ -391,7 +395,7 @@ class Manager implements Signal { public URI getDeviceLinkUri() throws TimeoutException, IOException { password = Util.getSecret(18); - accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT, timer); String uuid = accountManager.getNewDeviceUuid(); registered = false; @@ -1135,7 +1139,7 @@ class Manager implements Signal { public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer); try { if (messagePipe == null) { @@ -1244,6 +1248,9 @@ class Manager implements Signal { } syncGroup.members.addAll(g.getMembers()); syncGroup.active = g.isActive(); + if (g.getColor().isPresent()) { + syncGroup.color = g.getColor().get(); + } if (g.getAvatar().isPresent()) { retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); @@ -1437,7 +1444,7 @@ class Manager implements Signal { } } - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer); File tmpFile = Util.createTempFile(); try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) { @@ -1463,7 +1470,7 @@ class Manager implements Signal { } private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer); return messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE); } @@ -1492,7 +1499,8 @@ class Manager implements Signal { ThreadInfo info = threadStore.getThread(Base64.encodeBytes(record.groupId)); out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId), - record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null))); + record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null), + Optional.fromNullable(record.color))); } } diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index e28a5921..96147fe3 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -27,14 +27,18 @@ public class GroupInfo { @JsonProperty public boolean active; + @JsonProperty + public String color; + public GroupInfo(byte[] groupId) { this.groupId = groupId; } - public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId) { + public GroupInfo(@JsonProperty("groupId") byte[] groupId, @JsonProperty("name") String name, @JsonProperty("members") Collection members, @JsonProperty("avatarId") long avatarId, @JsonProperty("color") String color) { this.groupId = groupId; this.name = name; this.members.addAll(members); this.avatarId = avatarId; + this.color = color; } } From 35c00f1b2a7376a28714bd191a27fe75454d59aa Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 19 Oct 2018 23:39:25 +0200 Subject: [PATCH 0249/2005] Update libsignal-service-java to 2.9.0 --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 54413 -> 56177 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- src/main/java/org/asamk/signal/Manager.java | 8 +++++--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index d2fddabe..ffea41c3 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.8.0_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.9.0_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.59' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 91ca28c8b802289c3a438766657a5e98f20eff03..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch delta 49462 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

Gauk6VzRKaGELdj)1bl0ug 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 B@>Bo- diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 16d28051..e0b3fb8d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 8a606edd..09677548 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -64,6 +64,7 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; @@ -95,7 +96,8 @@ class Manager implements Signal { private final static TrustStore TRUST_STORE = new WhisperTrustStore(); private final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, - new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)} + new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, + new SignalContactDiscoveryUrl[0] ); public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); @@ -827,7 +829,7 @@ class Manager implements Signal { List groups = getGroups(); List ids = new ArrayList(groups.size()); for (GroupInfo group : groups) { - ids.add(group.groupId); + ids.add(group.groupId); } return ids; } @@ -1500,7 +1502,7 @@ class Manager implements Signal { out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId), record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null), - Optional.fromNullable(record.color))); + Optional.fromNullable(record.color), false)); } } From 69185a937f52cab3950d4a4b254be456a6f442e5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 17 Nov 2018 21:16:32 +0100 Subject: [PATCH 0250/2005] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 398c1bdf..e703a4ef 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Important: The USERNAME (your phone number) must include the country calling cod * Register a number (with SMS verification) signal-cli -u USERNAME register + + You can register Signal using a land line number. In this case you can skip SMS verification process and jump directly to the voice call verification by adding the --voice switch at the end of above register command. * Verify the number using the code received via SMS or voice From 4ab904b88e0b445c5e886fe91add69fb215dd455 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 17 Nov 2018 23:23:49 +0100 Subject: [PATCH 0251/2005] Update signal-service-java dependency --- build.gradle | 4 +- src/main/java/org/asamk/signal/Main.java | 23 +- src/main/java/org/asamk/signal/Manager.java | 233 ++++++++++++++---- .../signal/storage/contacts/ContactInfo.java | 3 + .../protocol/JsonIdentityKeyStore.java | 19 ++ .../protocol/JsonSignalProtocolStore.java | 5 + .../java/org/asamk/signal/util/KeyUtils.java | 47 ++++ src/main/java/org/asamk/signal/util/Util.java | 23 -- 8 files changed, 275 insertions(+), 82 deletions(-) create mode 100644 src/main/java/org/asamk/signal/util/KeyUtils.java diff --git a/build.gradle b/build.gradle index ffea41c3..80242e8c 100644 --- a/build.gradle +++ b/build.gradle @@ -20,8 +20,8 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.9.0_unofficial_1' - compile 'org.bouncycastle:bcprov-jdk15on:1.59' + compile 'com.github.turasa:signal-service-java:2.12.2_unofficial_1' + compile 'org.bouncycastle:bcprov-jdk15on:1.60' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 7fc520b9..7f85f280 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -100,7 +100,7 @@ public class Main { busType = DBusConnection.SESSION; } dBusConn = DBusConnection.getConnection(busType); - ts = (Signal) dBusConn.getRemoteObject( + ts = dBusConn.getRemoteObject( SIGNAL_BUSNAME, SIGNAL_OBJECTPATH, Signal.class); } catch (UnsatisfiedLinkError e) { @@ -984,6 +984,9 @@ public class Main { System.out.println("Relayed by: " + source.getRelay().get()); } System.out.println("Timestamp: " + formatTimestamp(envelope.getTimestamp())); + if (envelope.isUnidentifiedSender()) { + System.out.println("Sent by unidentified/sealed sender"); + } if (envelope.isReceipt()) { System.out.println("Got receipt."); @@ -1120,6 +1123,20 @@ public class Main { System.out.println(" " + formatTimestamp(timestamp)); } } + if (content.getTypingMessage().isPresent()) { + System.out.println("Received a typing message"); + SignalServiceTypingMessage typingMessage = content.getTypingMessage().get(); + System.out.println(" - Action: " + typingMessage.getAction()); + System.out.println(" - Timestamp: " + formatTimestamp(typingMessage.getTimestamp())); + if (typingMessage.getGroupId().isPresent()) { + GroupInfo group = m.getGroup(typingMessage.getGroupId().get()); + if (group != null) { + System.out.println(" Name: " + group.name); + } else { + System.out.println(" Name: "); + } + } + } } } else { System.out.println("Unknown message received."); @@ -1167,7 +1184,7 @@ public class Main { if (message.getExpiresInSeconds() > 0) { System.out.println("Expires in: " + message.getExpiresInSeconds() + " seconds"); } - if (message.isProfileKeyUpdate() && message.getProfileKey().isPresent()) { + if (message.getProfileKey().isPresent()) { System.out.println("Profile key update, key length:" + message.getProfileKey().get().length); } @@ -1201,7 +1218,7 @@ public class Main { System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); if (attachment.isPointer()) { final SignalServiceAttachmentPointer pointer = attachment.asPointer(); - System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : "")); + System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length); System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-")); System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); System.out.println(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no")); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 09677548..8e478d78 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -35,7 +35,10 @@ import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; import org.asamk.signal.storage.threads.JsonThreadStore; import org.asamk.signal.storage.threads.ThreadInfo; +import org.asamk.signal.util.KeyUtils; import org.asamk.signal.util.Util; +import org.signal.libsignal.metadata.*; +import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; @@ -52,13 +55,18 @@ import org.whispersystems.signalservice.api.SignalServiceMessagePipe; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; +import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; import org.whispersystems.signalservice.api.messages.multidevice.*; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.TrustStore; -import org.whispersystems.signalservice.api.push.exceptions.*; +import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.SleepTimer; @@ -99,6 +107,7 @@ class Manager implements Signal { new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalContactDiscoveryUrl[0] ); + private final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); @@ -119,9 +128,11 @@ class Manager implements Signal { private final ObjectMapper jsonProcessor = new ObjectMapper(); private String username; private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; + private boolean isMultiDevice = false; private String password; private String registrationLockPin; private String signalingKey; + private byte[] profileKey; private int preKeyIdOffset; private int nextSignedPreKeyId; @@ -133,6 +144,7 @@ class Manager implements Signal { private JsonContactsStore contactStore; private JsonThreadStore threadStore; private SignalServiceMessagePipe messagePipe = null; + private SignalServiceMessagePipe unidentifiedMessagePipe = null; private SleepTimer timer = new UptimeSleepTimer(); @@ -280,6 +292,13 @@ class Manager implements Signal { } else { nextSignedPreKeyId = 0; } + if (rootNode.has("profileKey")) { + profileKey = Base64.decode(getNotNullNode(rootNode, "profileKey").asText()); + } else { + // Old config file, creating new profile key + profileKey = KeyUtils.createProfileKey(); + } + signalProtocolStore = jsonProcessor.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); registered = getNotNullNode(rootNode, "registered").asBoolean(); JsonNode groupStoreNode = rootNode.get("groupStore"); @@ -370,7 +389,7 @@ class Manager implements Signal { } public void register(boolean voiceVerification) throws IOException { - password = Util.getSecret(18); + password = KeyUtils.createPassword(); accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT, timer); @@ -384,7 +403,7 @@ class Manager implements Signal { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true, registrationLockPin); + accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true, registrationLockPin, getSelfUnidentifiedAccessKey(), false); } public void unregister() throws IOException { @@ -395,7 +414,7 @@ class Manager implements Signal { } public URI getDeviceLinkUri() throws TimeoutException, IOException { - password = Util.getSecret(18); + password = KeyUtils.createPassword(); accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT, timer); String uuid = accountManager.getNewDeviceUuid(); @@ -410,7 +429,7 @@ class Manager implements Signal { } public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { - signalingKey = Util.getSecret(52); + signalingKey = KeyUtils.createSignalingKey(); SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName); deviceId = ret.getDeviceId(); username = ret.getNumber(); @@ -421,6 +440,7 @@ class Manager implements Signal { signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId()); registered = true; + isMultiDevice = true; refreshPreKeys(); requestSyncGroups(); @@ -430,7 +450,9 @@ class Manager implements Signal { } public List getLinkedDevices() throws IOException { - return accountManager.getDevices(); + List devices = accountManager.getDevices(); + isMultiDevice = devices.size() > 1; + return devices; } public void removeLinkedDevices(int deviceId) throws IOException { @@ -442,14 +464,15 @@ class Manager implements Signal { Map map = new HashMap<>(); for (String param : params) { String name = null; + final String[] paramParts = param.split("="); try { - name = URLDecoder.decode(param.split("=")[0], "utf-8"); + name = URLDecoder.decode(paramParts[0], "utf-8"); } catch (UnsupportedEncodingException e) { // Impossible } String value = null; try { - value = URLDecoder.decode(param.split("=")[1], "utf-8"); + value = URLDecoder.decode(paramParts[1], "utf-8"); } catch (UnsupportedEncodingException e) { // Impossible } @@ -476,8 +499,8 @@ class Manager implements Signal { IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair(); String verificationCode = accountManager.getNewDeviceVerificationCode(); - // TODO send profile key - accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.absent(), verificationCode); + accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(profileKey), verificationCode); + isMultiDevice = true; } private List generatePreKeys() { @@ -516,8 +539,8 @@ class Manager implements Signal { public void verifyAccount(String verificationCode, String pin) throws IOException { verificationCode = verificationCode.replace("-", ""); - signalingKey = Util.getSecret(52); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true, pin); + signalingKey = KeyUtils.createSignalingKey(); + accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); registered = true; @@ -566,8 +589,10 @@ class Manager implements Signal { if (mime == null) { mime = "application/octet-stream"; } - // TODO mabybe add a parameter to set the voiceNote and preview option - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, Optional.absent(), 0, 0, null); + // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option + Optional preview = Optional.absent(); + Optional caption = Optional.absent(); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, null); } private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { @@ -629,7 +654,7 @@ class Manager implements Signal { // Don't send group message to ourself final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); - sendMessage(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, membersSend); } public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions { @@ -644,7 +669,7 @@ class Manager implements Signal { g.members.remove(this.username); groupStore.updateGroup(g); - sendMessage(messageBuilder, g.members); + sendMessageLegacy(messageBuilder, g.members); } private static String join(CharSequence separator, Iterable list) { @@ -663,7 +688,7 @@ class Manager implements Signal { GroupInfo g; if (groupId == null) { // Create new group - g = new GroupInfo(Util.getSecretBytes(16)); + g = new GroupInfo(KeyUtils.createGroupId()); g.members.add(username); } else { g = getGroupForSending(groupId); @@ -714,7 +739,7 @@ class Manager implements Signal { // Don't send group message to ourself final List membersSend = new ArrayList<>(g.members); membersSend.remove(this.username); - sendMessage(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, membersSend); return g.groupId; } @@ -733,7 +758,7 @@ class Manager implements Signal { // Send group message only to the recipient who requested it final List membersSend = new ArrayList<>(); membersSend.add(recipient); - sendMessage(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, membersSend); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) { @@ -769,7 +794,7 @@ class Manager implements Signal { // Send group info request message to the recipient who sent us a message with this groupId final List membersSend = new ArrayList<>(); membersSend.add(recipient); - sendMessage(messageBuilder, membersSend); + sendMessageLegacy(messageBuilder, membersSend); } @Override @@ -788,7 +813,7 @@ class Manager implements Signal { if (attachments != null) { messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); } - sendMessage(messageBuilder, recipients); + sendMessageLegacy(messageBuilder, recipients); } @Override @@ -796,7 +821,7 @@ class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asEndSessionMessage(); - sendMessage(messageBuilder, recipients); + sendMessageLegacy(messageBuilder, recipients); } @Override @@ -891,42 +916,97 @@ class Manager implements Signal { } } + private byte[] getSelfUnidentifiedAccessKey() { + return UnidentifiedAccess.deriveAccessKeyFrom(profileKey); + } + + private static byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { + // TODO implement + return null; + } + + public Optional getAccessForSync() { + // TODO implement + return Optional.absent(); + } + + public List> getAccessFor(Collection recipients) { + List> result = new ArrayList<>(recipients.size()); + for (SignalServiceAddress recipient : recipients) { + result.add(Optional.absent()); + } + return result; + } + + public Optional getAccessFor(SignalServiceAddress recipient) { + // TODO implement + return Optional.absent(); + } + private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password, - deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.absent()); + deviceId, signalProtocolStore, USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); try { - messageSender.sendMessage(message); + messageSender.sendMessage(message, getAccessForSync()); } catch (UntrustedIdentityException e) { signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); throw e; } } - private void sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) + /** + * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult. + */ + private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) throws EncapsulatedExceptions, IOException { + List results = sendMessage(messageBuilder, recipients); + + List untrustedIdentities = new LinkedList<>(); + List unregisteredUsers = new LinkedList<>(); + List networkExceptions = new LinkedList<>(); + + for (SendMessageResult result : results) { + if (result.isUnregisteredFailure()) { + unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getNumber(), null)); + } else if (result.isNetworkFailure()) { + networkExceptions.add(new NetworkFailureException(result.getAddress().getNumber(), null)); + } else if (result.getIdentityFailure() != null) { + untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getNumber(), result.getIdentityFailure().getIdentityKey())); + } + } + if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { + throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions); + } + } + + private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) + throws IOException { Set recipientsTS = getSignalServiceAddresses(recipients); - if (recipientsTS == null) return; + if (recipientsTS == null) return Collections.emptyList(); SignalServiceDataMessage message = null; try { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password, - deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.absent()); + deviceId, signalProtocolStore, USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); message = messageBuilder.build(); if (message.getGroupInfo().isPresent()) { try { - messageSender.sendMessage(new ArrayList<>(recipientsTS), message); - } catch (EncapsulatedExceptions encapsulatedExceptions) { - for (UntrustedIdentityException e : encapsulatedExceptions.getUntrustedIdentityExceptions()) { - signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + List result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), message); + for (SendMessageResult r : result) { + if (r.getIdentityFailure() != null) { + signalProtocolStore.saveIdentity(r.getAddress().getNumber(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); + } } + return result; + } catch (UntrustedIdentityException e) { + signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + return Collections.emptyList(); } } else { // Send to all individually, so sync messages are sent correctly - List untrustedIdentities = new LinkedList<>(); - List unregisteredUsers = new LinkedList<>(); - List networkExceptions = new LinkedList<>(); + List results = new ArrayList<>(recipientsTS.size()); for (SignalServiceAddress address : recipientsTS) { ThreadInfo thread = threadStore.getThread(address.getNumber()); if (thread != null) { @@ -936,19 +1016,14 @@ class Manager implements Signal { } message = messageBuilder.build(); try { - messageSender.sendMessage(address, message); + SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message); + results.add(result); } catch (UntrustedIdentityException e) { signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); - untrustedIdentities.add(e); - } catch (UnregisteredUserException e) { - unregisteredUsers.add(e); - } catch (PushNetworkException e) { - networkExceptions.add(new NetworkFailureException(address.getNumber(), e)); + results.add(SendMessageResult.identityFailure(address, e.getIdentityKey())); } } - if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) { - throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions); - } + return results; } } finally { if (message != null && message.isEndSession()) { @@ -975,12 +1050,22 @@ class Manager implements Signal { return recipientsTS; } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException { - SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore); + public static CertificateValidator getCertificateValidator() { + try { + ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(UNIDENTIFIED_SENDER_TRUST_ROOT), 0); + return new CertificateValidator(unidentifiedSenderTrustRoot); + } catch (InvalidKeyException | IOException e) { + throw new AssertionError(e); + } + } + + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws org.whispersystems.libsignal.UntrustedIdentityException, InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException { + SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore, getCertificateValidator()); try { return cipher.decrypt(envelope); - } catch (org.whispersystems.libsignal.UntrustedIdentityException e) { - signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); + } catch (ProtocolUntrustedIdentityException e) { + // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else +// signalProtocolStore.saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); throw e; } } @@ -1091,6 +1176,14 @@ class Manager implements Signal { } } } + if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { + ContactInfo contact = contactStore.getContact(source); + if (contact == null) { + contact = new ContactInfo(); + contact.number = source; + } + contact.profileKey = Base64.encodeBytes(message.getProfileKey().get()); + } } public void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { @@ -1211,6 +1304,7 @@ class Manager implements Signal { handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments); } if (content.getSyncMessage().isPresent()) { + isMultiDevice = true; SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { SignalServiceDataMessage message = syncMessage.getSent().get().getMessage(); @@ -1298,6 +1392,21 @@ class Manager implements Signal { if (c.getColor().isPresent()) { contact.color = c.getColor().get(); } + if (c.getProfileKey().isPresent()) { + contact.profileKey = Base64.encodeBytes(c.getProfileKey().get()); + } + if (c.getVerified().isPresent()) { + final VerifiedMessage verifiedMessage = c.getVerified().get(); + signalProtocolStore.saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + } + if (c.getExpirationTimer().isPresent()) { + ThreadInfo thread = threadStore.getThread(c.getNumber()); + thread.messageExpirationTime = c.getExpirationTimer().get(); + threadStore.updateThread(thread); + } + if (c.isBlocked()) { + // TODO store list of blocked numbers + } contactStore.updateContact(contact); if (c.getAvatar().isPresent()) { @@ -1329,13 +1438,16 @@ class Manager implements Signal { try (FileInputStream f = new FileInputStream(file)) { DataInputStream in = new DataInputStream(f); int version = in.readInt(); - if (version != 1) { + if (version > 2) { return null; } int type = in.readInt(); String source = in.readUTF(); int sourceDevice = in.readInt(); - String relay = in.readUTF(); + if (version == 1) { + // read legacy relay field + in.readUTF(); + } long timestamp = in.readLong(); byte[] content = null; int contentLen = in.readInt(); @@ -1349,18 +1461,26 @@ class Manager implements Signal { legacyMessage = new byte[legacyMessageLen]; in.readFully(legacyMessage); } - return new SignalServiceEnvelope(type, source, sourceDevice, relay, timestamp, legacyMessage, content); + long serverTimestamp = 0; + String uuid = null; + if (version == 2) { + serverTimestamp = in.readLong(); + uuid = in.readUTF(); + if ("".equals(uuid)) { + uuid = null; + } + } + return new SignalServiceEnvelope(type, source, sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); } } private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { try (FileOutputStream f = new FileOutputStream(file)) { try (DataOutputStream out = new DataOutputStream(f)) { - out.writeInt(1); // version + out.writeInt(2); // version out.writeInt(envelope.getType()); out.writeUTF(envelope.getSource()); out.writeInt(envelope.getSourceDevice()); - out.writeUTF(envelope.getRelay()); out.writeLong(envelope.getTimestamp()); if (envelope.hasContent()) { out.writeInt(envelope.getContent().length); @@ -1374,6 +1494,9 @@ class Manager implements Signal { } else { out.writeInt(0); } + out.writeLong(envelope.getServerTimestamp()); + String uuid = envelope.getUuid(); + out.writeUTF(uuid == null ? "" : uuid); } } } @@ -1547,10 +1670,12 @@ class Manager implements Signal { } } - // TODO include profile key + byte[] profileKey = record.profileKey == null ? null : Base64.decode(record.profileKey); + // TODO store list of blocked numbers + boolean blocked = false; out.write(new DeviceContact(record.number, Optional.fromNullable(record.name), createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), - Optional.fromNullable(verifiedMessage), Optional.absent(), false, Optional.fromNullable(info != null ? info.messageExpirationTime : null))); + Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), blocked, Optional.fromNullable(info != null ? info.messageExpirationTime : null))); } } diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java index f66792b2..03b076cc 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java +++ b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java @@ -11,4 +11,7 @@ public class ContactInfo { @JsonProperty public String color; + + @JsonProperty + public String profileKey; } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index 7c069c8b..922a88f5 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -92,6 +92,25 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { return false; } + @Override + public IdentityKey getIdentity(SignalProtocolAddress address) { + List identities = trustedKeys.get(address.getName()); + if (identities == null) { + return null; + } + + long maxDate = 0; + Identity maxIdentity = null; + for (Identity id : identities) { + final long time = id.getDateAdded().getTime(); + if (maxDate <= time) { + maxDate = time; + maxIdentity = id; + } + } + return maxIdentity.getIdentityKey(); + } + public Map> getIdentities() { // TODO deep copy return trustedKeys; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 0085f0c7..12522432 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -87,6 +87,11 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { return identityKeyStore.isTrustedIdentity(address, identityKey, direction); } + @Override + public IdentityKey getIdentity(SignalProtocolAddress address) { + return identityKeyStore.getIdentity(address); + } + @Override public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { return preKeyStore.loadPreKey(preKeyId); diff --git a/src/main/java/org/asamk/signal/util/KeyUtils.java b/src/main/java/org/asamk/signal/util/KeyUtils.java new file mode 100644 index 00000000..ab421384 --- /dev/null +++ b/src/main/java/org/asamk/signal/util/KeyUtils.java @@ -0,0 +1,47 @@ +package org.asamk.signal.util; + +import org.whispersystems.signalservice.internal.util.Base64; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class KeyUtils { + + private KeyUtils() { + } + + public static String createSignalingKey() { + return getSecret(52); + } + + public static byte[] createProfileKey() { + return getSecretBytes(32); + } + + public static String createPassword() { + return getSecret(18); + } + + public static byte[] createGroupId() { + return getSecretBytes(16); + } + + private static String getSecret(int size) { + byte[] secret = getSecretBytes(size); + return Base64.encodeBytes(secret); + } + + private static byte[] getSecretBytes(int size) { + byte[] secret = new byte[size]; + getSecureRandom().nextBytes(secret); + return secret; + } + + private static SecureRandom getSecureRandom() { + try { + return SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } +} diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 19695ec6..c0efd271 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -1,32 +1,9 @@ package org.asamk.signal.util; -import org.whispersystems.signalservice.internal.util.Base64; - import java.io.File; import java.io.IOException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; public class Util { - public static String getSecret(int size) { - byte[] secret = getSecretBytes(size); - return Base64.encodeBytes(secret); - } - - public static byte[] getSecretBytes(int size) { - byte[] secret = new byte[size]; - getSecureRandom().nextBytes(secret); - return secret; - } - - private static SecureRandom getSecureRandom() { - try { - return SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } - } - public static File createTempFile() throws IOException { return File.createTempFile("signal_tmp_", ".tmp"); } From 7443225d96fd830c8abb2afd49381f6b38ce5aec Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 18 Nov 2018 10:45:26 +0100 Subject: [PATCH 0252/2005] Extract util methods to separate classes --- src/main/java/org/asamk/signal/Main.java | 73 +++++---------- src/main/java/org/asamk/signal/Manager.java | 93 ++++--------------- .../java/org/asamk/signal/util/DateUtils.java | 21 +++++ src/main/java/org/asamk/signal/util/Hex.java | 3 + .../java/org/asamk/signal/util/IOUtils.java | 55 +++++++++++ src/main/java/org/asamk/signal/util/Util.java | 54 ++++++++++- 6 files changed, 168 insertions(+), 131 deletions(-) create mode 100644 src/main/java/org/asamk/signal/util/DateUtils.java create mode 100644 src/main/java/org/asamk/signal/util/IOUtils.java diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 7f85f280..03d1c3cc 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -31,7 +31,10 @@ import org.asamk.Signal; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; +import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.Hex; +import org.asamk.signal.util.IOUtils; +import org.asamk.signal.util.Util; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.DBusSigHandler; import org.freedesktop.dbus.exceptions.DBusException; @@ -52,15 +55,14 @@ import org.whispersystems.signalservice.internal.util.Base64; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.StringWriter; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.security.Security; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -69,8 +71,6 @@ public class Main { public static final String SIGNAL_BUSNAME = "org.asamk.Signal"; public static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal"; - private static final TimeZone tzUTC = TimeZone.getTimeZone("UTC"); - public static void main(String[] args) { // Workaround for BKS truststore Security.insertProviderAt(new org.bouncycastle.jce.provider.BouncyCastleProvider(), 1); @@ -317,8 +317,8 @@ public class Main { for (DeviceInfo d : devices) { System.out.println("Device " + d.getId() + (d.getId() == m.getDeviceId() ? " (this device)" : "") + ":"); System.out.println(" Name: " + d.getName()); - System.out.println(" Created: " + formatTimestamp(d.getCreated())); - System.out.println(" Last seen: " + formatTimestamp(d.getLastSeen())); + System.out.println(" Created: " + DateUtils.formatTimestamp(d.getCreated())); + System.out.println(" Last seen: " + DateUtils.formatTimestamp(d.getLastSeen())); } } catch (IOException e) { e.printStackTrace(); @@ -373,7 +373,7 @@ public class Main { String messageText = ns.getString("message"); if (messageText == null) { try { - messageText = readAll(System.in); + messageText = IOUtils.readAll(System.in, Charset.defaultCharset()); } catch (IOException e) { System.err.println("Failed to read message from stdin: " + e.getMessage()); System.err.println("Aborting sending."); @@ -425,7 +425,7 @@ public class Main { @Override public void handle(Signal.MessageReceived s) { System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n", - s.getSender(), formatTimestamp(s.getTimestamp()), s.getMessage())); + s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()), s.getMessage())); if (s.getGroupId().length > 0) { System.out.println("Group info:"); System.out.println(" Id: " + Base64.encodeBytes(s.getGroupId())); @@ -443,7 +443,7 @@ public class Main { @Override public void handle(Signal.ReceiptReceived s) { System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", - s.getSender(), formatTimestamp(s.getTimestamp()))); + s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()))); } }); } catch (UnsatisfiedLinkError e) { @@ -708,7 +708,7 @@ public class Main { } private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) { - String digits = formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey())); + String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey())); System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits)); } @@ -723,16 +723,6 @@ public class Main { } } - private static String formatSafetyNumber(String digits) { - final int partCount = 12; - int partSize = digits.length() / partCount; - StringBuilder f = new StringBuilder(digits.length() + partCount); - for (int i = 0; i < partCount; i++) { - f.append(digits.substring(i * partSize, (i * partSize) + partSize)).append(" "); - } - return f.toString(); - } - private static void handleGroupNotFoundException(GroupNotFoundException e) { System.err.println("Failed to send to group: " + e.getMessage()); System.err.println("Aborting sending."); @@ -956,18 +946,6 @@ public class Main { System.err.println("Failed to send message: " + e.getMessage()); } - private static String readAll(InputStream in) throws IOException { - StringWriter output = new StringWriter(); - byte[] buffer = new byte[4096]; - long count = 0; - int n; - while (-1 != (n = System.in.read(buffer))) { - output.write(new String(buffer, 0, n, Charset.defaultCharset())); - count += n; - } - return output.toString(); - } - private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final Manager m; @@ -983,7 +961,7 @@ public class Main { if (source.getRelay().isPresent()) { System.out.println("Relayed by: " + source.getRelay().get()); } - System.out.println("Timestamp: " + formatTimestamp(envelope.getTimestamp())); + System.out.println("Timestamp: " + DateUtils.formatTimestamp(envelope.getTimestamp())); if (envelope.isUnidentifiedSender()) { System.out.println("Sent by unidentified/sealed sender"); } @@ -1029,7 +1007,7 @@ public class Main { System.out.println("Received sync read messages list"); for (ReadMessage rm : syncMessage.getRead().get()) { ContactInfo fromContact = m.getContact(rm.getSender()); - System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender() + " Message timestamp: " + formatTimestamp(rm.getTimestamp())); + System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp())); } } if (syncMessage.getRequest().isPresent()) { @@ -1052,9 +1030,9 @@ public class Main { } else { to = "Unknown"; } - System.out.println("To: " + to + " , Message timestamp: " + formatTimestamp(sentTranscriptMessage.getTimestamp())); + System.out.println("To: " + to + " , Message timestamp: " + DateUtils.formatTimestamp(sentTranscriptMessage.getTimestamp())); if (sentTranscriptMessage.getExpirationStartTimestamp() > 0) { - System.out.println("Expiration started at: " + formatTimestamp(sentTranscriptMessage.getExpirationStartTimestamp())); + System.out.println("Expiration started at: " + DateUtils.formatTimestamp(sentTranscriptMessage.getExpirationStartTimestamp())); } SignalServiceDataMessage message = sentTranscriptMessage.getMessage(); handleSignalServiceDataMessage(message); @@ -1071,7 +1049,7 @@ public class Main { System.out.println("Received sync message with verified identities:"); final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified()); - String safetyNumber = formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey())); + String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey())); System.out.println(" " + safetyNumber); } if (syncMessage.getConfiguration().isPresent()) { @@ -1111,7 +1089,7 @@ public class Main { if (content.getReceiptMessage().isPresent()) { System.out.println("Received a receipt message"); SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get(); - System.out.println(" - When: " + formatTimestamp(receiptMessage.getWhen())); + System.out.println(" - When: " + DateUtils.formatTimestamp(receiptMessage.getWhen())); if (receiptMessage.isDeliveryReceipt()) { System.out.println(" - Is delivery receipt"); } @@ -1120,14 +1098,14 @@ public class Main { } System.out.println(" - Timestamps:"); for (long timestamp : receiptMessage.getTimestamps()) { - System.out.println(" " + formatTimestamp(timestamp)); + System.out.println(" " + DateUtils.formatTimestamp(timestamp)); } } if (content.getTypingMessage().isPresent()) { System.out.println("Received a typing message"); SignalServiceTypingMessage typingMessage = content.getTypingMessage().get(); System.out.println(" - Action: " + typingMessage.getAction()); - System.out.println(" - Timestamp: " + formatTimestamp(typingMessage.getTimestamp())); + System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp())); if (typingMessage.getGroupId().isPresent()) { GroupInfo group = m.getGroup(typingMessage.getGroupId().get()); if (group != null) { @@ -1145,7 +1123,7 @@ public class Main { } private void handleSignalServiceDataMessage(SignalServiceDataMessage message) { - System.out.println("Message timestamp: " + formatTimestamp(message.getTimestamp())); + System.out.println("Message timestamp: " + DateUtils.formatTimestamp(message.getTimestamp())); if (message.getBody().isPresent()) { System.out.println("Body: " + message.getBody().get()); @@ -1334,11 +1312,4 @@ public class Main { } } } - - private static String formatTimestamp(long timestamp) { - Date date = new Date(timestamp); - final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); // Quoted "Z" to indicate UTC, no timezone offset - df.setTimeZone(tzUTC); - return timestamp + " (" + df.format(date) + ")"; - } } diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index 8e478d78..dc3916de 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -35,6 +35,7 @@ import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; import org.asamk.signal.storage.threads.JsonThreadStore; import org.asamk.signal.storage.threads.ThreadInfo; +import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.KeyUtils; import org.asamk.signal.util.Util; import org.signal.libsignal.metadata.*; @@ -81,23 +82,17 @@ import org.whispersystems.signalservice.internal.util.Base64; import java.io.*; import java.net.URI; import java.net.URISyntaxException; -import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.PosixFilePermission; -import java.nio.file.attribute.PosixFilePermissions; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import static java.nio.file.attribute.PosixFilePermission.*; - class Manager implements Signal { private final static String URL = "https://textsecure-service.whispersystems.org"; private final static String CDN_URL = "https://cdn.signal.org"; @@ -189,30 +184,10 @@ class Manager implements Signal { private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException { String cachePath = getMessageCachePath(sender); - createPrivateDirectories(cachePath); + IOUtils.createPrivateDirectories(cachePath); return new File(cachePath + "/" + now + "_" + timestamp); } - private static void createPrivateDirectories(String path) throws IOException { - final Path file = new File(path).toPath(); - try { - Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); - Files.createDirectories(file, PosixFilePermissions.asFileAttribute(perms)); - } catch (UnsupportedOperationException e) { - Files.createDirectories(file); - } - } - - private static void createPrivateFile(String path) throws IOException { - final Path file = new File(path).toPath(); - try { - Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); - Files.createFile(file, PosixFilePermissions.asFileAttribute(perms)); - } catch (UnsupportedOperationException e) { - Files.createFile(file); - } - } - public boolean userExists() { if (username == null) { return false; @@ -238,9 +213,9 @@ class Manager implements Signal { if (fileChannel != null) return; - createPrivateDirectories(dataPath); + IOUtils.createPrivateDirectories(dataPath); if (!new File(getFileName()).exists()) { - createPrivateFile(getFileName()); + IOUtils.createPrivateFile(getFileName()); } fileChannel = new RandomAccessFile(new File(getFileName()), "rw").getChannel(); lock = fileChannel.tryLock(); @@ -334,7 +309,7 @@ class Manager implements Signal { File attachmentFile = getAttachmentFile(g.getAvatarId()); if (!avatarFile.exists() && attachmentFile.exists()) { try { - createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(avatarsPath); Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (Exception e) { // Ignore @@ -459,30 +434,8 @@ class Manager implements Signal { accountManager.removeDevice(deviceId); } - public static Map getQueryMap(String query) { - String[] params = query.split("&"); - Map map = new HashMap<>(); - for (String param : params) { - String name = null; - final String[] paramParts = param.split("="); - try { - name = URLDecoder.decode(paramParts[0], "utf-8"); - } catch (UnsupportedEncodingException e) { - // Impossible - } - String value = null; - try { - value = URLDecoder.decode(paramParts[1], "utf-8"); - } catch (UnsupportedEncodingException e) { - // Impossible - } - map.put(name, value); - } - return map; - } - public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - Map query = getQueryMap(linkUri.getRawQuery()); + Map query = Util.getQueryMap(linkUri.getRawQuery()); String deviceIdentifier = query.get("uuid"); String publicKeyEncoded = query.get("pub_key"); @@ -672,18 +625,6 @@ class Manager implements Signal { sendMessageLegacy(messageBuilder, g.members); } - private static String join(CharSequence separator, Iterable list) { - StringBuilder buf = new StringBuilder(); - for (CharSequence str : list) { - if (buf.length() > 0) { - buf.append(separator); - } - buf.append(str); - } - - return buf.toString(); - } - public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { GroupInfo g; if (groupId == null) { @@ -720,14 +661,14 @@ class Manager implements Signal { for (ContactTokenDetails contact : contacts) { newMembers.remove(contact.getNumber()); } - System.err.println("Failed to add members " + join(", ", newMembers) + " to group: Not registered on Signal"); + System.err.println("Failed to add members " + Util.join(", ", newMembers) + " to group: Not registered on Signal"); System.err.println("Aborting…"); System.exit(1); } } if (avatarFile != null) { - createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(avatarsPath); File aFile = getGroupAvatarFile(g.groupId); Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } @@ -920,7 +861,7 @@ class Manager implements Signal { return UnidentifiedAccess.deriveAccessKeyFrom(profileKey); } - private static byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { + private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { // TODO implement return null; } @@ -1330,7 +1271,7 @@ class Manager implements Signal { if (syncMessage.getGroups().isPresent()) { File tmpFile = null; try { - tmpFile = Util.createTempFile(); + tmpFile = IOUtils.createTempFile(); try (InputStream attachmentAsStream = retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile)) { DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroup g; @@ -1372,7 +1313,7 @@ class Manager implements Signal { if (syncMessage.getContacts().isPresent()) { File tmpFile = null; try { - tmpFile = Util.createTempFile(); + tmpFile = IOUtils.createTempFile(); final ContactsMessage contactsMessage = syncMessage.getContacts().get(); try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)) { DeviceContactsInputStream s = new DeviceContactsInputStream(attachmentAsStream); @@ -1506,7 +1447,7 @@ class Manager implements Signal { } private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException { - createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getContactAvatarFile(number), false); @@ -1521,7 +1462,7 @@ class Manager implements Signal { } private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException { - createPrivateDirectories(avatarsPath); + IOUtils.createPrivateDirectories(avatarsPath); if (attachment.isPointer()) { SignalServiceAttachmentPointer pointer = attachment.asPointer(); return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); @@ -1536,7 +1477,7 @@ class Manager implements Signal { } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - createPrivateDirectories(attachmentsPath); + IOUtils.createPrivateDirectories(attachmentsPath); return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); } @@ -1571,7 +1512,7 @@ class Manager implements Signal { final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer); - File tmpFile = Util.createTempFile(); + File tmpFile = IOUtils.createTempFile(); try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) { try (OutputStream output = new FileOutputStream(outputFile)) { byte[] buffer = new byte[4096]; @@ -1615,7 +1556,7 @@ class Manager implements Signal { } private void sendGroups() throws IOException, UntrustedIdentityException { - File groupsFile = Util.createTempFile(); + File groupsFile = IOUtils.createTempFile(); try { try (OutputStream fos = new FileOutputStream(groupsFile)) { @@ -1650,7 +1591,7 @@ class Manager implements Signal { } private void sendContacts() throws IOException, UntrustedIdentityException { - File contactsFile = Util.createTempFile(); + File contactsFile = IOUtils.createTempFile(); try { try (OutputStream fos = new FileOutputStream(contactsFile)) { diff --git a/src/main/java/org/asamk/signal/util/DateUtils.java b/src/main/java/org/asamk/signal/util/DateUtils.java new file mode 100644 index 00000000..c9b92529 --- /dev/null +++ b/src/main/java/org/asamk/signal/util/DateUtils.java @@ -0,0 +1,21 @@ +package org.asamk.signal.util; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.TimeZone; + +public class DateUtils { + + private static final TimeZone tzUTC = TimeZone.getTimeZone("UTC"); + + private DateUtils() { + } + + public static String formatTimestamp(long timestamp) { + Date date = new Date(timestamp); + final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); // Quoted "Z" to indicate UTC, no timezone offset + df.setTimeZone(tzUTC); + return timestamp + " (" + df.format(date) + ")"; + } +} diff --git a/src/main/java/org/asamk/signal/util/Hex.java b/src/main/java/org/asamk/signal/util/Hex.java index 623c5cf8..9f885791 100644 --- a/src/main/java/org/asamk/signal/util/Hex.java +++ b/src/main/java/org/asamk/signal/util/Hex.java @@ -6,6 +6,9 @@ public class Hex { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + private Hex() { + } + public static String toStringCondensed(byte[] bytes) { StringBuffer buf = new StringBuffer(); for (int i = 0; i < bytes.length; i++) { diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java new file mode 100644 index 00000000..69128d01 --- /dev/null +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -0,0 +1,55 @@ +package org.asamk.signal.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; +import java.util.Set; + +import static java.nio.file.attribute.PosixFilePermission.*; + +public class IOUtils { + + private IOUtils() { + } + + public static File createTempFile() throws IOException { + return File.createTempFile("signal_tmp_", ".tmp"); + } + + public static String readAll(InputStream in, Charset charset) throws IOException { + StringWriter output = new StringWriter(); + byte[] buffer = new byte[4096]; + int n; + while (-1 != (n = in.read(buffer))) { + output.write(new String(buffer, 0, n, charset)); + } + return output.toString(); + } + + public static void createPrivateDirectories(String path) throws IOException { + final Path file = new File(path).toPath(); + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); + Files.createDirectories(file, PosixFilePermissions.asFileAttribute(perms)); + } catch (UnsupportedOperationException e) { + Files.createDirectories(file); + } + } + + public static void createPrivateFile(String path) throws IOException { + final Path file = new File(path).toPath(); + try { + Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE); + Files.createFile(file, PosixFilePermissions.asFileAttribute(perms)); + } catch (UnsupportedOperationException e) { + Files.createFile(file); + } + } +} diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index c0efd271..eec7d2f7 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -1,10 +1,56 @@ package org.asamk.signal.util; -import java.io.File; -import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Map; public class Util { - public static File createTempFile() throws IOException { - return File.createTempFile("signal_tmp_", ".tmp"); + + private Util() { + } + + public static String formatSafetyNumber(String digits) { + final int partCount = 12; + int partSize = digits.length() / partCount; + StringBuilder f = new StringBuilder(digits.length() + partCount); + for (int i = 0; i < partCount; i++) { + f.append(digits, i * partSize, (i * partSize) + partSize).append(" "); + } + return f.toString(); + } + + public static Map getQueryMap(String query) { + String[] params = query.split("&"); + Map map = new HashMap<>(); + for (String param : params) { + String name = null; + final String[] paramParts = param.split("="); + try { + name = URLDecoder.decode(paramParts[0], "utf-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + } + String value = null; + try { + value = URLDecoder.decode(paramParts[1], "utf-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + } + map.put(name, value); + } + return map; + } + + public static String join(CharSequence separator, Iterable list) { + StringBuilder buf = new StringBuilder(); + for (CharSequence str : list) { + if (buf.length() > 0) { + buf.append(separator); + } + buf.append(str); + } + + return buf.toString(); } } From 701328b8c26d6ad4f98cf8213dd57e4b06aa1281 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 18 Nov 2018 11:08:24 +0100 Subject: [PATCH 0253/2005] Move Manager to sub package --- src/main/java/org/asamk/signal/Main.java | 34 ++--- .../org/asamk/signal/manager/BaseConfig.java | 33 +++++ .../asamk/signal/{ => manager}/Manager.java | 134 ++++++++---------- .../{ => manager}/WhisperTrustStore.java | 2 +- 4 files changed, 115 insertions(+), 88 deletions(-) create mode 100644 src/main/java/org/asamk/signal/manager/BaseConfig.java rename src/main/java/org/asamk/signal/{ => manager}/Manager.java (92%) rename src/main/java/org/asamk/signal/{ => manager}/WhisperTrustStore.java (91%) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 03d1c3cc..e534f05e 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -1,18 +1,18 @@ -/** - * Copyright (C) 2015 AsamK - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . +/* + Copyright (C) 2015-2018 AsamK + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ package org.asamk.signal; @@ -28,6 +28,8 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; import org.apache.http.util.TextUtils; import org.asamk.Signal; +import org.asamk.signal.manager.BaseConfig; +import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; @@ -756,7 +758,7 @@ public class Main { .build() .defaultHelp(true) .description("Commandline interface for Signal.") - .version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION); + .version(BaseConfig.PROJECT_NAME + " " + BaseConfig.PROJECT_VERSION); parser.addArgument("-v", "--version") .help("Show package version.") diff --git a/src/main/java/org/asamk/signal/manager/BaseConfig.java b/src/main/java/org/asamk/signal/manager/BaseConfig.java new file mode 100644 index 00000000..19d43fc8 --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/BaseConfig.java @@ -0,0 +1,33 @@ +package org.asamk.signal.manager; + +import org.whispersystems.signalservice.api.push.TrustStore; +import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; +import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; +import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; + +public class BaseConfig { + + private BaseConfig() { + } + + public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); + public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); + final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION; + + private final static String URL = "https://textsecure-service.whispersystems.org"; + private final static String CDN_URL = "https://cdn.signal.org"; + + private final static TrustStore TRUST_STORE = new WhisperTrustStore(); + final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( + new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, + new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, + new SignalContactDiscoveryUrl[0] + ); + + final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; + + final static int PREKEY_MINIMUM_COUNT = 20; + static final int PREKEY_BATCH_SIZE = 100; + static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; +} diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java similarity index 92% rename from src/main/java/org/asamk/signal/Manager.java rename to src/main/java/org/asamk/signal/manager/Manager.java index dc3916de..a97c11ea 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,20 +1,20 @@ -/** - * Copyright (C) 2015 AsamK - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . +/* + Copyright (C) 2015-2018 AsamK + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ -package org.asamk.signal; +package org.asamk.signal.manager; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; @@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.http.util.TextUtils; import org.asamk.Signal; +import org.asamk.signal.*; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.groups.GroupInfo; @@ -63,7 +64,6 @@ import org.whispersystems.signalservice.api.messages.*; import org.whispersystems.signalservice.api.messages.multidevice.*; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; @@ -72,10 +72,6 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; -import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; -import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; -import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration; -import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.util.Base64; @@ -93,24 +89,7 @@ import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -class Manager implements Signal { - private final static String URL = "https://textsecure-service.whispersystems.org"; - private final static String CDN_URL = "https://cdn.signal.org"; - private final static TrustStore TRUST_STORE = new WhisperTrustStore(); - private final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( - new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, - new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, - new SignalContactDiscoveryUrl[0] - ); - private final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; - - public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); - public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); - private final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION; - - private final static int PREKEY_MINIMUM_COUNT = 20; - private static final int PREKEY_BATCH_SIZE = 100; - private static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; +public class Manager implements Signal { private final String settingsPath; private final String dataPath; @@ -231,9 +210,9 @@ class Manager implements Signal { migrateLegacyConfigs(); - accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, deviceId, USER_AGENT, timer); + accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, password, deviceId, BaseConfig.USER_AGENT, timer); try { - if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { + if (registered && accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { refreshPreKeys(); save(); } @@ -366,7 +345,7 @@ class Manager implements Signal { public void register(boolean voiceVerification) throws IOException { password = KeyUtils.createPassword(); - accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT, timer); + accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, password, BaseConfig.USER_AGENT, timer); if (voiceVerification) accountManager.requestVoiceVerificationCode(); @@ -391,7 +370,7 @@ class Manager implements Signal { public URI getDeviceLinkUri() throws TimeoutException, IOException { password = KeyUtils.createPassword(); - accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT, timer); + accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, password, BaseConfig.USER_AGENT, timer); String uuid = accountManager.getNewDeviceUuid(); registered = false; @@ -459,7 +438,7 @@ class Manager implements Signal { private List generatePreKeys() { List records = new LinkedList<>(); - for (int i = 0; i < PREKEY_BATCH_SIZE; i++) { + for (int i = 0; i < BaseConfig.PREKEY_BATCH_SIZE; i++) { int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE; ECKeyPair keyPair = Curve.generateKeyPair(); PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); @@ -468,7 +447,7 @@ class Manager implements Signal { records.add(record); } - preKeyIdOffset = (preKeyIdOffset + PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE; + preKeyIdOffset = (preKeyIdOffset + BaseConfig.PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE; save(); return records; @@ -625,7 +604,7 @@ class Manager implements Signal { sendMessageLegacy(messageBuilder, g.members); } - public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { + private byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { GroupInfo g; if (groupId == null) { // Create new group @@ -793,7 +772,7 @@ class Manager implements Signal { @Override public List getGroupIds() { List groups = getGroups(); - List ids = new ArrayList(groups.size()); + List ids = new ArrayList<>(groups.size()); for (GroupInfo group : groups) { ids.add(group.groupId); } @@ -814,9 +793,9 @@ class Manager implements Signal { public List getGroupMembers(byte[] groupId) { GroupInfo group = getGroup(groupId); if (group == null) { - return new ArrayList(); + return new ArrayList<>(); } else { - return new ArrayList(group.members); + return new ArrayList<>(group.members); } } @@ -837,6 +816,19 @@ class Manager implements Signal { return sendUpdateGroupMessage(groupId, name, members, avatar); } + + /** + * Change the expiration timer for a thread (number of groupId) + * + * @param numberOrGroupId + * @param messageExpirationTimer + */ + public void setExpirationTimer(String numberOrGroupId, int messageExpirationTimer) { + ThreadInfo thread = threadStore.getThread(numberOrGroupId); + thread.messageExpirationTime = messageExpirationTimer; + threadStore.updateThread(thread); + } + private void requestSyncGroups() throws IOException { SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build(); SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); @@ -866,12 +858,12 @@ class Manager implements Signal { return null; } - public Optional getAccessForSync() { + private Optional getAccessForSync() { // TODO implement return Optional.absent(); } - public List> getAccessFor(Collection recipients) { + private List> getAccessFor(Collection recipients) { List> result = new ArrayList<>(recipients.size()); for (SignalServiceAddress recipient : recipients) { result.add(Optional.absent()); @@ -879,15 +871,15 @@ class Manager implements Signal { return result; } - public Optional getAccessFor(SignalServiceAddress recipient) { + private Optional getAccessFor(SignalServiceAddress recipient) { // TODO implement return Optional.absent(); } private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password, - deviceId, signalProtocolStore, USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(BaseConfig.serviceConfiguration, username, password, + deviceId, signalProtocolStore, BaseConfig.USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); try { messageSender.sendMessage(message, getAccessForSync()); } catch (UntrustedIdentityException e) { @@ -928,8 +920,8 @@ class Manager implements Signal { SignalServiceDataMessage message = null; try { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password, - deviceId, signalProtocolStore, USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(BaseConfig.serviceConfiguration, username, password, + deviceId, signalProtocolStore, BaseConfig.USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); message = messageBuilder.build(); if (message.getGroupInfo().isPresent()) { @@ -991,16 +983,16 @@ class Manager implements Signal { return recipientsTS; } - public static CertificateValidator getCertificateValidator() { + private static CertificateValidator getCertificateValidator() { try { - ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(UNIDENTIFIED_SENDER_TRUST_ROOT), 0); + ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); return new CertificateValidator(unidentifiedSenderTrustRoot); } catch (InvalidKeyException | IOException e) { throw new AssertionError(e); } } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws org.whispersystems.libsignal.UntrustedIdentityException, InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException { + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException { SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore, getCertificateValidator()); try { return cipher.decrypt(envelope); @@ -1127,17 +1119,17 @@ class Manager implements Signal { } } - public void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { + private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { final File cachePath = new File(getMessageCachePath()); if (!cachePath.exists()) { return; } - for (final File dir : cachePath.listFiles()) { + for (final File dir : Objects.requireNonNull(cachePath.listFiles())) { if (!dir.isDirectory()) { continue; } - for (final File fileEntry : dir.listFiles()) { + for (final File fileEntry : Objects.requireNonNull(dir.listFiles())) { if (!fileEntry.isFile()) { continue; } @@ -1175,7 +1167,7 @@ class Manager implements Signal { public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, password, deviceId, signalingKey, BaseConfig.USER_AGENT, null, timer); try { if (messagePipe == null) { @@ -1218,7 +1210,7 @@ class Manager implements Signal { } save(); handler.handleMessage(envelope, content, exception); - if (exception == null || !(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { + if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { File cacheFile = null; try { cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp()); @@ -1442,7 +1434,7 @@ class Manager implements Signal { } } - public File getContactAvatarFile(String number) { + private File getContactAvatarFile(String number) { return new File(avatarsPath, "contact-" + number); } @@ -1457,7 +1449,7 @@ class Manager implements Signal { } } - public File getGroupAvatarFile(byte[] groupId) { + private File getGroupAvatarFile(byte[] groupId) { return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_")); } @@ -1481,7 +1473,7 @@ class Manager implements Signal { return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); } - private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException { + private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException { InputStream input = stream.getInputStream(); try (OutputStream output = new FileOutputStream(outputFile)) { @@ -1510,10 +1502,10 @@ class Manager implements Signal { } } - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, password, deviceId, signalingKey, BaseConfig.USER_AGENT, null, timer); File tmpFile = IOUtils.createTempFile(); - try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) { + try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE)) { try (OutputStream output = new FileOutputStream(outputFile)) { byte[] buffer = new byte[4096]; int read; @@ -1536,8 +1528,8 @@ class Manager implements Signal { } private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer); - return messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, password, deviceId, signalingKey, BaseConfig.USER_AGENT, null, timer); + return messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE); } private String canonicalizeNumber(String number) throws InvalidNumberException { diff --git a/src/main/java/org/asamk/signal/WhisperTrustStore.java b/src/main/java/org/asamk/signal/manager/WhisperTrustStore.java similarity index 91% rename from src/main/java/org/asamk/signal/WhisperTrustStore.java rename to src/main/java/org/asamk/signal/manager/WhisperTrustStore.java index e9468c2e..185ab599 100644 --- a/src/main/java/org/asamk/signal/WhisperTrustStore.java +++ b/src/main/java/org/asamk/signal/manager/WhisperTrustStore.java @@ -1,4 +1,4 @@ -package org.asamk.signal; +package org.asamk.signal.manager; import org.whispersystems.signalservice.api.push.TrustStore; From 35c72f692f13b12594ecdbe8f59f31d3b396d356 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 18 Nov 2018 15:34:10 +0100 Subject: [PATCH 0254/2005] Extract SignalAccount from Manager --- .idea/codeStyles/Project.xml | 8 + build.gradle | 2 +- src/main/java/org/asamk/Signal.java | 3 + .../signal/AttachmentInvalidException.java | 1 + .../java/org/asamk/signal/JsonAttachment.java | 1 + .../org/asamk/signal/JsonCallMessage.java | 1 + .../org/asamk/signal/JsonDataMessage.java | 1 + src/main/java/org/asamk/signal/JsonError.java | 1 + .../java/org/asamk/signal/JsonGroupInfo.java | 1 + .../org/asamk/signal/JsonMessageEnvelope.java | 1 + .../org/asamk/signal/JsonSyncMessage.java | 1 + src/main/java/org/asamk/signal/Main.java | 37 +- .../org/asamk/signal/UserAlreadyExists.java | 1 + .../org/asamk/signal/manager/BaseConfig.java | 17 +- .../signal/{util => manager}/KeyUtils.java | 12 +- .../org/asamk/signal/manager/Manager.java | 857 ++++++++---------- .../asamk/signal/storage/SignalAccount.java | 321 +++++++ .../signal/storage/contacts/ContactInfo.java | 1 + .../storage/contacts/JsonContactsStore.java | 6 +- .../signal/storage/groups/GroupInfo.java | 17 +- .../signal/storage/groups/JsonGroupStore.java | 11 +- .../protocol/JsonIdentityKeyStore.java | 3 +- .../storage/protocol/JsonPreKeyStore.java | 2 - .../storage/protocol/JsonSessionStore.java | 1 - .../protocol/JsonSignedPreKeyStore.java | 2 - .../storage/threads/JsonThreadStore.java | 7 +- .../signal/storage/threads/ThreadInfo.java | 1 + .../java/org/asamk/signal/util/IOUtils.java | 13 +- src/main/java/org/asamk/signal/util/Util.java | 12 + .../asamk/signal/{ => manager}/whisper.store | Bin 30 files changed, 793 insertions(+), 549 deletions(-) rename src/main/java/org/asamk/signal/{util => manager}/KeyUtils.java (78%) create mode 100644 src/main/java/org/asamk/signal/storage/SignalAccount.java rename src/main/resources/org/asamk/signal/{ => manager}/whisper.store (100%) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 1c207237..2bf5e0cf 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -2,10 +2,18 @@ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 80242e8c..66b2510a 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.12.2_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.12.2_unofficial_2' compile 'org.bouncycastle:bcprov-jdk15on:1.60' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 88b15926..7c311813 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -11,6 +11,7 @@ import java.io.IOException; import java.util.List; public interface Signal extends DBusInterface { + void sendMessage(String message, List attachments, String recipient) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; void sendMessage(String message, List attachments, List recipients) throws EncapsulatedExceptions, AttachmentInvalidException, IOException; @@ -32,6 +33,7 @@ public interface Signal extends DBusInterface { byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException; class MessageReceived extends DBusSignal { + private long timestamp; private String sender; private byte[] groupId; @@ -69,6 +71,7 @@ public interface Signal extends DBusInterface { } class ReceiptReceived extends DBusSignal { + private long timestamp; private String sender; diff --git a/src/main/java/org/asamk/signal/AttachmentInvalidException.java b/src/main/java/org/asamk/signal/AttachmentInvalidException.java index 8a023f62..839c7940 100644 --- a/src/main/java/org/asamk/signal/AttachmentInvalidException.java +++ b/src/main/java/org/asamk/signal/AttachmentInvalidException.java @@ -3,6 +3,7 @@ package org.asamk.signal; import org.freedesktop.dbus.exceptions.DBusExecutionException; public class AttachmentInvalidException extends DBusExecutionException { + public AttachmentInvalidException(String message) { super(message); } diff --git a/src/main/java/org/asamk/signal/JsonAttachment.java b/src/main/java/org/asamk/signal/JsonAttachment.java index 53946df9..29e8592e 100644 --- a/src/main/java/org/asamk/signal/JsonAttachment.java +++ b/src/main/java/org/asamk/signal/JsonAttachment.java @@ -4,6 +4,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; class JsonAttachment { + String contentType; long id; int size; diff --git a/src/main/java/org/asamk/signal/JsonCallMessage.java b/src/main/java/org/asamk/signal/JsonCallMessage.java index 98a01b29..b10e6f7b 100644 --- a/src/main/java/org/asamk/signal/JsonCallMessage.java +++ b/src/main/java/org/asamk/signal/JsonCallMessage.java @@ -5,6 +5,7 @@ import org.whispersystems.signalservice.api.messages.calls.*; import java.util.List; class JsonCallMessage { + OfferMessage offerMessage; AnswerMessage answerMessage; BusyMessage busyMessage; diff --git a/src/main/java/org/asamk/signal/JsonDataMessage.java b/src/main/java/org/asamk/signal/JsonDataMessage.java index eda54025..34f6249e 100644 --- a/src/main/java/org/asamk/signal/JsonDataMessage.java +++ b/src/main/java/org/asamk/signal/JsonDataMessage.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; class JsonDataMessage { + long timestamp; String message; int expiresInSeconds; diff --git a/src/main/java/org/asamk/signal/JsonError.java b/src/main/java/org/asamk/signal/JsonError.java index 05fe3ae6..5ef2cd7c 100644 --- a/src/main/java/org/asamk/signal/JsonError.java +++ b/src/main/java/org/asamk/signal/JsonError.java @@ -1,6 +1,7 @@ package org.asamk.signal; class JsonError { + String message; JsonError(Throwable exception) { diff --git a/src/main/java/org/asamk/signal/JsonGroupInfo.java b/src/main/java/org/asamk/signal/JsonGroupInfo.java index 89c5515f..073ad3ff 100644 --- a/src/main/java/org/asamk/signal/JsonGroupInfo.java +++ b/src/main/java/org/asamk/signal/JsonGroupInfo.java @@ -6,6 +6,7 @@ import org.whispersystems.signalservice.internal.util.Base64; import java.util.List; class JsonGroupInfo { + String groupId; List members; String name; diff --git a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/JsonMessageEnvelope.java index 2ce39cc5..9971b011 100644 --- a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/JsonMessageEnvelope.java @@ -5,6 +5,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; class JsonMessageEnvelope { + String source; int sourceDevice; String relay; diff --git a/src/main/java/org/asamk/signal/JsonSyncMessage.java b/src/main/java/org/asamk/signal/JsonSyncMessage.java index 92ad1cc5..febf64a4 100644 --- a/src/main/java/org/asamk/signal/JsonSyncMessage.java +++ b/src/main/java/org/asamk/signal/JsonSyncMessage.java @@ -6,6 +6,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy import java.util.List; class JsonSyncMessage { + JsonDataMessage sentMessage; List blockedNumbers; List readMessages; diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index e534f05e..810f1deb 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -129,13 +129,11 @@ public class Main { m = new Manager(username, settingsPath); ts = m; - if (m.userExists()) { - try { - m.init(); - } catch (Exception e) { - System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage()); - return 2; - } + try { + m.init(); + } catch (Exception e) { + System.err.println("Error loading state file: " + e.getMessage()); + return 2; } } @@ -145,9 +143,6 @@ public class Main { System.err.println("register is not yet implemented via dbus"); return 1; } - if (!m.userHasKeys()) { - m.createNewIdentity(); - } try { m.register(ns.getBoolean("voice")); } catch (IOException e) { @@ -252,9 +247,6 @@ public class Main { return 1; } - // When linking, username is null and we always have to create keys - m.createNewIdentity(); - String deviceName = ns.getString("name"); if (deviceName == null) { deviceName = "cli"; @@ -736,7 +728,6 @@ public class Main { System.err.println("Aborting sending."); } - private static void handleDBusExecutionException(DBusExecutionException e) { System.err.println("Cannot connect to dbus: " + e.getMessage()); System.err.println("Aborting."); @@ -949,6 +940,7 @@ public class Main { } private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { + final Manager m; public ReceiveMessageHandler(Manager m) { @@ -1212,6 +1204,7 @@ public class Main { } private static class DbusReceiveMessageHandler extends ReceiveMessageHandler { + final DBusConnection conn; public DbusReceiveMessageHandler(Manager m, DBusConnection conn) { @@ -1228,6 +1221,7 @@ public class Main { } private static class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler { + final Manager m; final ObjectMapper jsonProcessor; @@ -1259,6 +1253,7 @@ public class Main { } private static class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { + final DBusConnection conn; public JsonDbusReceiveMessageHandler(Manager m, DBusConnection conn) { @@ -1266,13 +1261,6 @@ public class Main { this.conn = conn; } - @Override - public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { - super.handleMessage(envelope, content, exception); - - sendReceivedMessageToDbus(envelope, content, conn, m); - } - private static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, Manager m) { if (envelope.isReceipt()) { try { @@ -1313,5 +1301,12 @@ public class Main { } } } + + @Override + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { + super.handleMessage(envelope, content, exception); + + sendReceivedMessageToDbus(envelope, content, conn, m); + } } } diff --git a/src/main/java/org/asamk/signal/UserAlreadyExists.java b/src/main/java/org/asamk/signal/UserAlreadyExists.java index 2c018ed9..047b5fc7 100644 --- a/src/main/java/org/asamk/signal/UserAlreadyExists.java +++ b/src/main/java/org/asamk/signal/UserAlreadyExists.java @@ -1,6 +1,7 @@ package org.asamk.signal; public class UserAlreadyExists extends Exception { + private String username; private String fileName; diff --git a/src/main/java/org/asamk/signal/manager/BaseConfig.java b/src/main/java/org/asamk/signal/manager/BaseConfig.java index 19d43fc8..0f503352 100644 --- a/src/main/java/org/asamk/signal/manager/BaseConfig.java +++ b/src/main/java/org/asamk/signal/manager/BaseConfig.java @@ -8,26 +8,25 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl; public class BaseConfig { - private BaseConfig() { - } - public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle(); public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion(); + final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION; + final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; + final static int PREKEY_MINIMUM_COUNT = 20; + final static int PREKEY_BATCH_SIZE = 100; + final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; private final static String URL = "https://textsecure-service.whispersystems.org"; private final static String CDN_URL = "https://cdn.signal.org"; - private final static TrustStore TRUST_STORE = new WhisperTrustStore(); + final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration( new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)}, new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)}, new SignalContactDiscoveryUrl[0] ); - final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF"; - - final static int PREKEY_MINIMUM_COUNT = 20; - static final int PREKEY_BATCH_SIZE = 100; - static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; + private BaseConfig() { + } } diff --git a/src/main/java/org/asamk/signal/util/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java similarity index 78% rename from src/main/java/org/asamk/signal/util/KeyUtils.java rename to src/main/java/org/asamk/signal/manager/KeyUtils.java index ab421384..225cf682 100644 --- a/src/main/java/org/asamk/signal/util/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -1,28 +1,28 @@ -package org.asamk.signal.util; +package org.asamk.signal.manager; import org.whispersystems.signalservice.internal.util.Base64; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -public class KeyUtils { +class KeyUtils { private KeyUtils() { } - public static String createSignalingKey() { + static String createSignalingKey() { return getSecret(52); } - public static byte[] createProfileKey() { + static byte[] createProfileKey() { return getSecretBytes(32); } - public static String createPassword() { + static String createPassword() { return getSecret(18); } - public static byte[] createGroupId() { + static byte[] createGroupId() { return getSecretBytes(16); } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index a97c11ea..e8860ef8 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -16,28 +16,16 @@ */ package org.asamk.signal.manager; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.asamk.signal.*; +import org.asamk.signal.storage.SignalAccount; import org.asamk.signal.storage.contacts.ContactInfo; -import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.groups.JsonGroupStore; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; -import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; -import org.asamk.signal.storage.threads.JsonThreadStore; import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; -import org.asamk.signal.util.KeyUtils; import org.asamk.signal.util.Util; import org.signal.libsignal.metadata.*; import org.signal.libsignal.metadata.certificate.CertificateValidator; @@ -79,9 +67,6 @@ import java.io.*; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.FileLock; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; @@ -96,27 +81,10 @@ public class Manager implements Signal { private final String attachmentsPath; private final String avatarsPath; - private FileChannel fileChannel; - private FileLock lock; + private SignalAccount account; - private final ObjectMapper jsonProcessor = new ObjectMapper(); private String username; - private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; - private boolean isMultiDevice = false; - private String password; - private String registrationLockPin; - private String signalingKey; - private byte[] profileKey; - private int preKeyIdOffset; - private int nextSignedPreKeyId; - - private boolean registered = false; - - private JsonSignalProtocolStore signalProtocolStore; private SignalServiceAccountManager accountManager; - private JsonGroupStore groupStore; - private JsonContactsStore contactStore; - private JsonThreadStore threadStore; private SignalServiceMessagePipe messagePipe = null; private SignalServiceMessagePipe unidentifiedMessagePipe = null; @@ -129,376 +97,8 @@ public class Manager implements Signal { this.attachmentsPath = this.settingsPath + "/attachments"; this.avatarsPath = this.settingsPath + "/avatars"; - jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect - jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. - jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); - jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); - jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); } - public String getUsername() { - return username; - } - - private IdentityKey getIdentity() { - return signalProtocolStore.getIdentityKeyPair().getPublicKey(); - } - - public int getDeviceId() { - return deviceId; - } - - public String getFileName() { - return dataPath + "/" + username; - } - - private String getMessageCachePath() { - return this.dataPath + "/" + username + ".d/msg-cache"; - } - - private String getMessageCachePath(String sender) { - return getMessageCachePath() + "/" + sender.replace("/", "_"); - } - - private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException { - String cachePath = getMessageCachePath(sender); - IOUtils.createPrivateDirectories(cachePath); - return new File(cachePath + "/" + now + "_" + timestamp); - } - - public boolean userExists() { - if (username == null) { - return false; - } - File f = new File(getFileName()); - return !(!f.exists() || f.isDirectory()); - } - - public boolean userHasKeys() { - return signalProtocolStore != null; - } - - private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException { - JsonNode node = parent.get(name); - if (node == null) { - throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name)); - } - - return node; - } - - private void openFileChannel() throws IOException { - if (fileChannel != null) - return; - - IOUtils.createPrivateDirectories(dataPath); - if (!new File(getFileName()).exists()) { - IOUtils.createPrivateFile(getFileName()); - } - fileChannel = new RandomAccessFile(new File(getFileName()), "rw").getChannel(); - lock = fileChannel.tryLock(); - if (lock == null) { - System.err.println("Config file is in use by another instance, waiting…"); - lock = fileChannel.lock(); - System.err.println("Config file lock acquired."); - } - } - - public void init() throws IOException { - load(); - - migrateLegacyConfigs(); - - accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, password, deviceId, BaseConfig.USER_AGENT, timer); - try { - if (registered && accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { - refreshPreKeys(); - save(); - } - } catch (AuthorizationFailedException e) { - System.err.println("Authorization failed, was the number registered elsewhere?"); - } - } - - private void load() throws IOException { - openFileChannel(); - JsonNode rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel)); - - JsonNode node = rootNode.get("deviceId"); - if (node != null) { - deviceId = node.asInt(); - } - username = getNotNullNode(rootNode, "username").asText(); - password = getNotNullNode(rootNode, "password").asText(); - JsonNode pinNode = rootNode.get("registrationLockPin"); - registrationLockPin = pinNode == null ? null : pinNode.asText(); - if (rootNode.has("signalingKey")) { - signalingKey = getNotNullNode(rootNode, "signalingKey").asText(); - } - if (rootNode.has("preKeyIdOffset")) { - preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0); - } else { - preKeyIdOffset = 0; - } - if (rootNode.has("nextSignedPreKeyId")) { - nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt(); - } else { - nextSignedPreKeyId = 0; - } - if (rootNode.has("profileKey")) { - profileKey = Base64.decode(getNotNullNode(rootNode, "profileKey").asText()); - } else { - // Old config file, creating new profile key - profileKey = KeyUtils.createProfileKey(); - } - - signalProtocolStore = jsonProcessor.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); - registered = getNotNullNode(rootNode, "registered").asBoolean(); - JsonNode groupStoreNode = rootNode.get("groupStore"); - if (groupStoreNode != null) { - groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class); - } - if (groupStore == null) { - groupStore = new JsonGroupStore(); - } - - JsonNode contactStoreNode = rootNode.get("contactStore"); - if (contactStoreNode != null) { - contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class); - } - if (contactStore == null) { - contactStore = new JsonContactsStore(); - } - JsonNode threadStoreNode = rootNode.get("threadStore"); - if (threadStoreNode != null) { - threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class); - } - if (threadStore == null) { - threadStore = new JsonThreadStore(); - } - } - - private void migrateLegacyConfigs() { - // Copy group avatars that were previously stored in the attachments folder - // to the new avatar folder - if (JsonGroupStore.groupsWithLegacyAvatarId.size() > 0) { - for (GroupInfo g : JsonGroupStore.groupsWithLegacyAvatarId) { - File avatarFile = getGroupAvatarFile(g.groupId); - File attachmentFile = getAttachmentFile(g.getAvatarId()); - if (!avatarFile.exists() && attachmentFile.exists()) { - try { - IOUtils.createPrivateDirectories(avatarsPath); - Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - } catch (Exception e) { - // Ignore - } - } - } - JsonGroupStore.groupsWithLegacyAvatarId.clear(); - save(); - } - } - - private void save() { - if (username == null) { - return; - } - ObjectNode rootNode = jsonProcessor.createObjectNode(); - rootNode.put("username", username) - .put("deviceId", deviceId) - .put("password", password) - .put("registrationLockPin", registrationLockPin) - .put("signalingKey", signalingKey) - .put("preKeyIdOffset", preKeyIdOffset) - .put("nextSignedPreKeyId", nextSignedPreKeyId) - .put("registered", registered) - .putPOJO("axolotlStore", signalProtocolStore) - .putPOJO("groupStore", groupStore) - .putPOJO("contactStore", contactStore) - .putPOJO("threadStore", threadStore) - ; - try { - openFileChannel(); - fileChannel.position(0); - jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode); - fileChannel.truncate(fileChannel.position()); - fileChannel.force(false); - } catch (Exception e) { - System.err.println(String.format("Error saving file: %s", e.getMessage())); - } - } - - public void createNewIdentity() { - IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); - int registrationId = KeyHelper.generateRegistrationId(false); - signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); - groupStore = new JsonGroupStore(); - registered = false; - save(); - } - - public boolean isRegistered() { - return registered; - } - - public void register(boolean voiceVerification) throws IOException { - password = KeyUtils.createPassword(); - - accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, password, BaseConfig.USER_AGENT, timer); - - if (voiceVerification) - accountManager.requestVoiceVerificationCode(); - else - accountManager.requestSmsVerificationCode(); - - registered = false; - save(); - } - - public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true, registrationLockPin, getSelfUnidentifiedAccessKey(), false); - } - - public void unregister() throws IOException { - // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. - // If this is the master device, other users can't send messages to this number anymore. - // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. - accountManager.setGcmId(Optional.absent()); - } - - public URI getDeviceLinkUri() throws TimeoutException, IOException { - password = KeyUtils.createPassword(); - - accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, password, BaseConfig.USER_AGENT, timer); - String uuid = accountManager.getNewDeviceUuid(); - - registered = false; - try { - return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8")); - } catch (URISyntaxException e) { - // Shouldn't happen - return null; - } - } - - public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { - signalingKey = KeyUtils.createSignalingKey(); - SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName); - deviceId = ret.getDeviceId(); - username = ret.getNumber(); - // TODO do this check before actually registering - if (userExists()) { - throw new UserAlreadyExists(username, getFileName()); - } - signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId()); - - registered = true; - isMultiDevice = true; - refreshPreKeys(); - - requestSyncGroups(); - requestSyncContacts(); - - save(); - } - - public List getLinkedDevices() throws IOException { - List devices = accountManager.getDevices(); - isMultiDevice = devices.size() > 1; - return devices; - } - - public void removeLinkedDevices(int deviceId) throws IOException { - accountManager.removeDevice(deviceId); - } - - public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - Map query = Util.getQueryMap(linkUri.getRawQuery()); - String deviceIdentifier = query.get("uuid"); - String publicKeyEncoded = query.get("pub_key"); - - if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) { - throw new RuntimeException("Invalid device link uri"); - } - - ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); - - addDevice(deviceIdentifier, deviceKey); - } - - private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { - IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair(); - String verificationCode = accountManager.getNewDeviceVerificationCode(); - - accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(profileKey), verificationCode); - isMultiDevice = true; - } - - private List generatePreKeys() { - List records = new LinkedList<>(); - - for (int i = 0; i < BaseConfig.PREKEY_BATCH_SIZE; i++) { - int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE; - ECKeyPair keyPair = Curve.generateKeyPair(); - PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); - - signalProtocolStore.storePreKey(preKeyId, record); - records.add(record); - } - - preKeyIdOffset = (preKeyIdOffset + BaseConfig.PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE; - save(); - - return records; - } - - private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) { - try { - ECKeyPair keyPair = Curve.generateKeyPair(); - byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize()); - SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature); - - signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record); - nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE; - save(); - - return record; - } catch (InvalidKeyException e) { - throw new AssertionError(e); - } - } - - public void verifyAccount(String verificationCode, String pin) throws IOException { - verificationCode = verificationCode.replace("-", ""); - signalingKey = KeyUtils.createSignalingKey(); - accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false); - - //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); - registered = true; - registrationLockPin = pin; - - refreshPreKeys(); - save(); - } - - public void setRegistrationLockPin(Optional pin) throws IOException { - accountManager.setPin(pin); - if (pin.isPresent()) { - registrationLockPin = pin.get(); - } else { - registrationLockPin = null; - } - } - - private void refreshPreKeys() throws IOException { - List oneTimePreKeys = generatePreKeys(); - SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair()); - - accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), signedPreKeyRecord, oneTimePreKeys); - } - - private static List getSignalServiceAttachments(List attachments) throws AttachmentInvalidException { List SignalServiceAttachments = null; if (attachments != null) { @@ -527,6 +127,268 @@ public class Manager implements Signal { return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, null); } + private static CertificateValidator getCertificateValidator() { + try { + ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); + return new CertificateValidator(unidentifiedSenderTrustRoot); + } catch (InvalidKeyException | IOException e) { + throw new AssertionError(e); + } + } + + public String getUsername() { + return username; + } + + private IdentityKey getIdentity() { + return account.getSignalProtocolStore().getIdentityKeyPair().getPublicKey(); + } + + public int getDeviceId() { + return account.getDeviceId(); + } + + private String getMessageCachePath() { + return this.dataPath + "/" + username + ".d/msg-cache"; + } + + private String getMessageCachePath(String sender) { + return getMessageCachePath() + "/" + sender.replace("/", "_"); + } + + private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException { + String cachePath = getMessageCachePath(sender); + IOUtils.createPrivateDirectories(cachePath); + return new File(cachePath + "/" + now + "_" + timestamp); + } + + public boolean userHasKeys() { + return account.getSignalProtocolStore() != null; + } + + public void init() throws IOException { + if (!SignalAccount.userExists(dataPath, username)) { + return; + } + account = SignalAccount.load(dataPath, username); + + migrateLegacyConfigs(); + + accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, account.getPassword(), account.getDeviceId(), BaseConfig.USER_AGENT, timer); + try { + if (account.isRegistered() && accountManager.getPreKeysCount() < BaseConfig.PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + account.save(); + } + } catch (AuthorizationFailedException e) { + System.err.println("Authorization failed, was the number registered elsewhere?"); + } + } + + private void migrateLegacyConfigs() { + // Copy group avatars that were previously stored in the attachments folder + // to the new avatar folder + if (JsonGroupStore.groupsWithLegacyAvatarId.size() > 0) { + for (GroupInfo g : JsonGroupStore.groupsWithLegacyAvatarId) { + File avatarFile = getGroupAvatarFile(g.groupId); + File attachmentFile = getAttachmentFile(g.getAvatarId()); + if (!avatarFile.exists() && attachmentFile.exists()) { + try { + IOUtils.createPrivateDirectories(avatarsPath); + Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + // Ignore + } + } + } + JsonGroupStore.groupsWithLegacyAvatarId.clear(); + account.save(); + } + if (account.getProfileKey() == null) { + // Old config file, creating new profile key + account.setProfileKey(KeyUtils.createProfileKey()); + } + } + + private void createNewIdentity() throws IOException { + IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); + int registrationId = KeyHelper.generateRegistrationId(false); + if (username == null) { + account = SignalAccount.createTemporaryAccount(identityKey, registrationId); + } else { + byte[] profileKey = KeyUtils.createProfileKey(); + account = SignalAccount.create(dataPath, username, identityKey, registrationId, profileKey); + account.save(); + } + } + + public boolean isRegistered() { + return account != null && account.isRegistered(); + } + + public void register(boolean voiceVerification) throws IOException { + if (account == null) { + createNewIdentity(); + } + account.setPassword(KeyUtils.createPassword()); + accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, account.getUsername(), account.getPassword(), BaseConfig.USER_AGENT, timer); + + if (voiceVerification) + accountManager.requestVoiceVerificationCode(); + else + accountManager.requestSmsVerificationCode(); + + account.setRegistered(false); + account.save(); + } + + public void updateAccountAttributes() throws IOException { + accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), getSelfUnidentifiedAccessKey(), false); + } + + public void unregister() throws IOException { + // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. + // If this is the master device, other users can't send messages to this number anymore. + // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. + accountManager.setGcmId(Optional.absent()); + } + + public URI getDeviceLinkUri() throws TimeoutException, IOException { + if (account == null) { + createNewIdentity(); + } + account.setPassword(KeyUtils.createPassword()); + accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, account.getPassword(), BaseConfig.USER_AGENT, timer); + String uuid = accountManager.getNewDeviceUuid(); + + try { + return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(getIdentity().serialize()), "utf-8")); + } catch (URISyntaxException e) { + // Shouldn't happen + return null; + } + } + + public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { + account.setSignalingKey(KeyUtils.createSignalingKey()); + SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(account.getSignalProtocolStore().getIdentityKeyPair(), account.getSignalingKey(), false, true, account.getSignalProtocolStore().getLocalRegistrationId(), deviceName); + + username = ret.getNumber(); + // TODO do this check before actually registering + if (SignalAccount.userExists(dataPath, username)) { + throw new UserAlreadyExists(username, SignalAccount.getFileName(dataPath, username)); + } + + // Create new account with the synced identity + byte[] profileKey = ret.getProfileKey(); + if (profileKey == null) { + profileKey = KeyUtils.createProfileKey(); + } + account = SignalAccount.createLinkedAccount(dataPath, username, account.getPassword(), ret.getDeviceId(), ret.getIdentity(), account.getSignalProtocolStore().getLocalRegistrationId(), account.getSignalingKey(), profileKey); + + refreshPreKeys(); + + requestSyncGroups(); + requestSyncContacts(); + + account.save(); + } + + public List getLinkedDevices() throws IOException { + List devices = accountManager.getDevices(); + account.setMultiDevice(devices.size() > 1); + return devices; + } + + public void removeLinkedDevices(int deviceId) throws IOException { + accountManager.removeDevice(deviceId); + } + + public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { + Map query = Util.getQueryMap(linkUri.getRawQuery()); + String deviceIdentifier = query.get("uuid"); + String publicKeyEncoded = query.get("pub_key"); + + if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) { + throw new RuntimeException("Invalid device link uri"); + } + + ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); + + addDevice(deviceIdentifier, deviceKey); + } + + private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { + IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair(); + String verificationCode = accountManager.getNewDeviceVerificationCode(); + + accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey()), verificationCode); + account.setMultiDevice(true); + } + + private List generatePreKeys() { + List records = new ArrayList<>(BaseConfig.PREKEY_BATCH_SIZE); + + final int offset = account.getPreKeyIdOffset(); + for (int i = 0; i < BaseConfig.PREKEY_BATCH_SIZE; i++) { + int preKeyId = (offset + i) % Medium.MAX_VALUE; + ECKeyPair keyPair = Curve.generateKeyPair(); + PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair); + + records.add(record); + } + + account.addPreKeys(records); + account.save(); + + return records; + } + + private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) { + try { + ECKeyPair keyPair = Curve.generateKeyPair(); + byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize()); + SignedPreKeyRecord record = new SignedPreKeyRecord(account.getNextSignedPreKeyId(), System.currentTimeMillis(), keyPair, signature); + + account.addSignedPreKey(record); + account.save(); + + return record; + } catch (InvalidKeyException e) { + throw new AssertionError(e); + } + } + + public void verifyAccount(String verificationCode, String pin) throws IOException { + verificationCode = verificationCode.replace("-", ""); + account.setSignalingKey(KeyUtils.createSignalingKey()); + accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false); + + //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); + account.setRegistered(true); + account.setRegistrationLockPin(pin); + + refreshPreKeys(); + account.save(); + } + + public void setRegistrationLockPin(Optional pin) throws IOException { + accountManager.setPin(pin); + if (pin.isPresent()) { + account.setRegistrationLockPin(pin.get()); + } else { + account.setRegistrationLockPin(null); + } + } + + private void refreshPreKeys() throws IOException { + List oneTimePreKeys = generatePreKeys(); + final IdentityKeyPair identityKeyPair = account.getSignalProtocolStore().getIdentityKeyPair(); + SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(identityKeyPair); + + accountManager.setPreKeys(getIdentity(), signedPreKeyRecord, oneTimePreKeys); + } + private Optional createGroupAvatarAttachment(byte[] groupId) throws IOException { File file = getGroupAvatarFile(groupId); if (!file.exists()) { @@ -546,7 +408,7 @@ public class Manager implements Signal { } private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException { - GroupInfo g = groupStore.getGroup(groupId); + GroupInfo g = account.getGroupStore().getGroup(groupId); if (g == null) { throw new GroupNotFoundException(groupId); } @@ -559,7 +421,7 @@ public class Manager implements Signal { } public List getGroups() { - return groupStore.getGroups(); + return account.getGroupStore().getGroups(); } @Override @@ -576,7 +438,7 @@ public class Manager implements Signal { .build(); messageBuilder.asGroupMessage(group); } - ThreadInfo thread = threadStore.getThread(Base64.encodeBytes(groupId)); + ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId)); if (thread != null) { messageBuilder.withExpiration(thread.messageExpirationTime); } @@ -599,7 +461,7 @@ public class Manager implements Signal { final GroupInfo g = getGroupForSending(groupId); g.members.remove(this.username); - groupStore.updateGroup(g); + account.getGroupStore().updateGroup(g); sendMessageLegacy(messageBuilder, g.members); } @@ -652,7 +514,7 @@ public class Manager implements Signal { Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } - groupStore.updateGroup(g); + account.getGroupStore().updateGroup(g); SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); @@ -746,7 +608,7 @@ public class Manager implements Signal { @Override public String getContactName(String number) { - ContactInfo contact = contactStore.getContact(number); + ContactInfo contact = account.getContactStore().getContact(number); if (contact == null) { return ""; } else { @@ -756,7 +618,7 @@ public class Manager implements Signal { @Override public void setContactName(String number, String name) { - ContactInfo contact = contactStore.getContact(number); + ContactInfo contact = account.getContactStore().getContact(number); if (contact == null) { contact = new ContactInfo(); contact.number = number; @@ -765,8 +627,8 @@ public class Manager implements Signal { System.err.println("Updating contact " + number + " name " + contact.name + " -> " + name); } contact.name = name; - contactStore.updateContact(contact); - save(); + account.getContactStore().updateContact(contact); + account.save(); } @Override @@ -816,7 +678,6 @@ public class Manager implements Signal { return sendUpdateGroupMessage(groupId, name, members, avatar); } - /** * Change the expiration timer for a thread (number of groupId) * @@ -824,9 +685,9 @@ public class Manager implements Signal { * @param messageExpirationTimer */ public void setExpirationTimer(String numberOrGroupId, int messageExpirationTimer) { - ThreadInfo thread = threadStore.getThread(numberOrGroupId); + ThreadInfo thread = account.getThreadStore().getThread(numberOrGroupId); thread.messageExpirationTime = messageExpirationTimer; - threadStore.updateThread(thread); + account.getThreadStore().updateThread(thread); } private void requestSyncGroups() throws IOException { @@ -849,8 +710,28 @@ public class Manager implements Signal { } } + private void requestSyncBlocked() throws IOException { + SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED).build(); + SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + try { + sendSyncMessage(message); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } + } + + private void requestSyncConfiguration() throws IOException { + SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION).build(); + SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + try { + sendSyncMessage(message); + } catch (UntrustedIdentityException e) { + e.printStackTrace(); + } + } + private byte[] getSelfUnidentifiedAccessKey() { - return UnidentifiedAccess.deriveAccessKeyFrom(profileKey); + return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey()); } private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) { @@ -878,12 +759,12 @@ public class Manager implements Signal { private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(BaseConfig.serviceConfiguration, username, password, - deviceId, signalProtocolStore, BaseConfig.USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(BaseConfig.serviceConfiguration, username, account.getPassword(), + account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); try { messageSender.sendMessage(message, getAccessForSync()); } catch (UntrustedIdentityException e) { - signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); throw e; } } @@ -920,8 +801,8 @@ public class Manager implements Signal { SignalServiceDataMessage message = null; try { - SignalServiceMessageSender messageSender = new SignalServiceMessageSender(BaseConfig.serviceConfiguration, username, password, - deviceId, signalProtocolStore, BaseConfig.USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); + SignalServiceMessageSender messageSender = new SignalServiceMessageSender(BaseConfig.serviceConfiguration, username, account.getPassword(), + account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent()); message = messageBuilder.build(); if (message.getGroupInfo().isPresent()) { @@ -929,19 +810,19 @@ public class Manager implements Signal { List result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), message); for (SendMessageResult r : result) { if (r.getIdentityFailure() != null) { - signalProtocolStore.saveIdentity(r.getAddress().getNumber(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(r.getAddress().getNumber(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); } } return result; } catch (UntrustedIdentityException e) { - signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); return Collections.emptyList(); } } else { // Send to all individually, so sync messages are sent correctly List results = new ArrayList<>(recipientsTS.size()); for (SignalServiceAddress address : recipientsTS) { - ThreadInfo thread = threadStore.getThread(address.getNumber()); + ThreadInfo thread = account.getThreadStore().getThread(address.getNumber()); if (thread != null) { messageBuilder.withExpiration(thread.messageExpirationTime); } else { @@ -952,7 +833,7 @@ public class Manager implements Signal { SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message); results.add(result); } catch (UntrustedIdentityException e) { - signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + account.getSignalProtocolStore().saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); results.add(SendMessageResult.identityFailure(address, e.getIdentityKey())); } } @@ -964,7 +845,7 @@ public class Manager implements Signal { handleEndSession(recipient.getNumber()); } } - save(); + account.save(); } } @@ -976,39 +857,26 @@ public class Manager implements Signal { } catch (InvalidNumberException e) { System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); System.err.println("Aborting sending."); - save(); + account.save(); return null; } } return recipientsTS; } - private static CertificateValidator getCertificateValidator() { - try { - ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); - return new CertificateValidator(unidentifiedSenderTrustRoot); - } catch (InvalidKeyException | IOException e) { - throw new AssertionError(e); - } - } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException { - SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore, getCertificateValidator()); + SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), account.getSignalProtocolStore(), getCertificateValidator()); try { return cipher.decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else -// signalProtocolStore.saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); +// account.getSignalProtocolStore().saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED); throw e; } } private void handleEndSession(String source) { - signalProtocolStore.deleteAllSessions(source); - } - - public interface ReceiveMessageHandler { - void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e); + account.getSignalProtocolStore().deleteAllSessions(source); } private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination, boolean ignoreAttachments) { @@ -1016,7 +884,7 @@ public class Manager implements Signal { if (message.getGroupInfo().isPresent()) { SignalServiceGroup groupInfo = message.getGroupInfo().get(); threadId = Base64.encodeBytes(groupInfo.getGroupId()); - GroupInfo group = groupStore.getGroup(groupInfo.getGroupId()); + GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId()); switch (groupInfo.getType()) { case UPDATE: if (group == null) { @@ -1042,7 +910,7 @@ public class Manager implements Signal { group.members.addAll(groupInfo.getMembers().get()); } - groupStore.updateGroup(group); + account.getGroupStore().updateGroup(group); break; case DELIVER: if (group == null) { @@ -1062,7 +930,7 @@ public class Manager implements Signal { } } else { group.members.remove(source); - groupStore.updateGroup(group); + account.getGroupStore().updateGroup(group); } break; case REQUEST_INFO: @@ -1088,14 +956,14 @@ public class Manager implements Signal { handleEndSession(isSync ? destination : source); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { - ThreadInfo thread = threadStore.getThread(threadId); + ThreadInfo thread = account.getThreadStore().getThread(threadId); if (thread == null) { thread = new ThreadInfo(); thread.id = threadId; } if (thread.messageExpirationTime != message.getExpiresInSeconds()) { thread.messageExpirationTime = message.getExpiresInSeconds(); - threadStore.updateThread(thread); + account.getThreadStore().updateThread(thread); } } if (message.getAttachments().isPresent() && !ignoreAttachments) { @@ -1110,7 +978,7 @@ public class Manager implements Signal { } } if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { - ContactInfo contact = contactStore.getContact(source); + ContactInfo contact = account.getContactStore().getContact(source); if (contact == null) { contact = new ContactInfo(); contact.number = source; @@ -1152,7 +1020,7 @@ public class Manager implements Signal { } handleMessage(envelope, content, ignoreAttachments); } - save(); + account.save(); handler.handleMessage(envelope, content, null); try { Files.delete(fileEntry.toPath()); @@ -1167,7 +1035,7 @@ public class Manager implements Signal { public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, password, deviceId, signalingKey, BaseConfig.USER_AGENT, null, timer); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer); try { if (messagePipe == null) { @@ -1208,9 +1076,9 @@ public class Manager implements Signal { } handleMessage(envelope, content, ignoreAttachments); } - save(); + account.save(); handler.handleMessage(envelope, content, exception); - if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { + if (!(exception instanceof ProtocolUntrustedIdentityException)) { File cacheFile = null; try { cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp()); @@ -1237,7 +1105,7 @@ public class Manager implements Signal { handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments); } if (content.getSyncMessage().isPresent()) { - isMultiDevice = true; + account.setMultiDevice(true); SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { SignalServiceDataMessage message = syncMessage.getSent().get().getMessage(); @@ -1259,6 +1127,7 @@ public class Manager implements Signal { e.printStackTrace(); } } + // TODO Handle rm.isBlockedListRequest(); rm.isConfigurationRequest(); } if (syncMessage.getGroups().isPresent()) { File tmpFile = null; @@ -1268,7 +1137,7 @@ public class Manager implements Signal { DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroup g; while ((g = s.read()) != null) { - GroupInfo syncGroup = groupStore.getGroup(g.getId()); + GroupInfo syncGroup = account.getGroupStore().getGroup(g.getId()); if (syncGroup == null) { syncGroup = new GroupInfo(g.getId()); } @@ -1284,7 +1153,7 @@ public class Manager implements Signal { if (g.getAvatar().isPresent()) { retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId); } - groupStore.updateGroup(syncGroup); + account.getGroupStore().updateGroup(syncGroup); } } } catch (Exception e) { @@ -1298,9 +1167,9 @@ public class Manager implements Signal { } } } - if (syncMessage.getBlockedList().isPresent()) { - // TODO store list of blocked numbers - } + } + if (syncMessage.getBlockedList().isPresent()) { + // TODO store list of blocked numbers } if (syncMessage.getContacts().isPresent()) { File tmpFile = null; @@ -1310,11 +1179,14 @@ public class Manager implements Signal { try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)) { DeviceContactsInputStream s = new DeviceContactsInputStream(attachmentAsStream); if (contactsMessage.isComplete()) { - contactStore.clear(); + account.getContactStore().clear(); } DeviceContact c; while ((c = s.read()) != null) { - ContactInfo contact = contactStore.getContact(c.getNumber()); + if (c.getNumber().equals(account.getUsername()) && c.getProfileKey().isPresent()) { + account.setProfileKey(c.getProfileKey().get()); + } + ContactInfo contact = account.getContactStore().getContact(c.getNumber()); if (contact == null) { contact = new ContactInfo(); contact.number = c.getNumber(); @@ -1330,17 +1202,17 @@ public class Manager implements Signal { } if (c.getVerified().isPresent()) { final VerifiedMessage verifiedMessage = c.getVerified().get(); - signalProtocolStore.saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (c.getExpirationTimer().isPresent()) { - ThreadInfo thread = threadStore.getThread(c.getNumber()); + ThreadInfo thread = account.getThreadStore().getThread(c.getNumber()); thread.messageExpirationTime = c.getExpirationTimer().get(); - threadStore.updateThread(thread); + account.getThreadStore().updateThread(thread); } if (c.isBlocked()) { // TODO store list of blocked numbers } - contactStore.updateContact(contact); + account.getContactStore().updateContact(contact); if (c.getAvatar().isPresent()) { retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number); @@ -1361,7 +1233,10 @@ public class Manager implements Signal { } if (syncMessage.getVerified().isPresent()) { final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); - signalProtocolStore.saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + account.getSignalProtocolStore().saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + } + if (syncMessage.getConfiguration().isPresent()) { + // TODO } } } @@ -1502,7 +1377,7 @@ public class Manager implements Signal { } } - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, password, deviceId, signalingKey, BaseConfig.USER_AGENT, null, timer); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer); File tmpFile = IOUtils.createTempFile(); try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE)) { @@ -1528,7 +1403,7 @@ public class Manager implements Signal { } private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, password, deviceId, signalingKey, BaseConfig.USER_AGENT, null, timer); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(BaseConfig.serviceConfiguration, username, account.getPassword(), account.getDeviceId(), account.getSignalingKey(), BaseConfig.USER_AGENT, null, timer); return messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE); } @@ -1553,8 +1428,8 @@ public class Manager implements Signal { try { try (OutputStream fos = new FileOutputStream(groupsFile)) { DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos); - for (GroupInfo record : groupStore.getGroups()) { - ThreadInfo info = threadStore.getThread(Base64.encodeBytes(record.groupId)); + for (GroupInfo record : account.getGroupStore().getGroups()) { + ThreadInfo info = account.getThreadStore().getThread(Base64.encodeBytes(record.groupId)); out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name), new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId), record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null), @@ -1588,9 +1463,9 @@ public class Manager implements Signal { try { try (OutputStream fos = new FileOutputStream(contactsFile)) { DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); - for (ContactInfo record : contactStore.getContacts()) { + for (ContactInfo record : account.getContactStore().getContacts()) { VerifiedMessage verifiedMessage = null; - ThreadInfo info = threadStore.getThread(record.number); + ThreadInfo info = account.getThreadStore().getThread(record.number); if (getIdentities().containsKey(record.number)) { JsonIdentityKeyStore.Identity currentIdentity = null; for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) { @@ -1610,6 +1485,15 @@ public class Manager implements Signal { createContactAvatarAttachment(record.number), Optional.fromNullable(record.color), Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), blocked, Optional.fromNullable(info != null ? info.messageExpirationTime : null))); } + + if (account.getProfileKey() != null) { + // Send our own profile key as well + out.write(new DeviceContact(account.getUsername(), + Optional.absent(), Optional.absent(), + Optional.absent(), Optional.absent(), + Optional.of(account.getProfileKey()), + false, Optional.absent())); + } } if (contactsFile.exists() && contactsFile.length() > 0) { @@ -1638,19 +1522,19 @@ public class Manager implements Signal { } public ContactInfo getContact(String number) { - return contactStore.getContact(number); + return account.getContactStore().getContact(number); } public GroupInfo getGroup(byte[] groupId) { - return groupStore.getGroup(groupId); + return account.getGroupStore().getGroup(groupId); } public Map> getIdentities() { - return signalProtocolStore.getIdentities(); + return account.getSignalProtocolStore().getIdentities(); } public List getIdentities(String number) { - return signalProtocolStore.getIdentities(number); + return account.getSignalProtocolStore().getIdentities(number); } /** @@ -1660,7 +1544,7 @@ public class Manager implements Signal { * @param fingerprint Fingerprint */ public boolean trustIdentityVerified(String name, byte[] fingerprint) { - List ids = signalProtocolStore.getIdentities(name); + List ids = account.getSignalProtocolStore().getIdentities(name); if (ids == null) { return false; } @@ -1669,13 +1553,13 @@ public class Manager implements Signal { continue; } - signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); try { sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } - save(); + account.save(); return true; } return false; @@ -1688,7 +1572,7 @@ public class Manager implements Signal { * @param safetyNumber Safety number */ public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) { - List ids = signalProtocolStore.getIdentities(name); + List ids = account.getSignalProtocolStore().getIdentities(name); if (ids == null) { return false; } @@ -1697,13 +1581,13 @@ public class Manager implements Signal { continue; } - signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); + account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); try { sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); } catch (IOException | UntrustedIdentityException e) { e.printStackTrace(); } - save(); + account.save(); return true; } return false; @@ -1715,13 +1599,13 @@ public class Manager implements Signal { * @param name username of the identity */ public boolean trustIdentityAllKeys(String name) { - List ids = signalProtocolStore.getIdentities(name); + List ids = account.getSignalProtocolStore().getIdentities(name); if (ids == null) { return false; } for (JsonIdentityKeyStore.Identity id : ids) { if (id.getTrustLevel() == TrustLevel.UNTRUSTED) { - signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); + account.getSignalProtocolStore().saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); try { sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); } catch (IOException | UntrustedIdentityException e) { @@ -1729,7 +1613,7 @@ public class Manager implements Signal { } } } - save(); + account.save(); return true; } @@ -1737,4 +1621,9 @@ public class Manager implements Signal { Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey); return fingerprint.getDisplayableFingerprint().getDisplayText(); } + + public interface ReceiveMessageHandler { + + void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e); + } } diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java new file mode 100644 index 00000000..cdc3efa5 --- /dev/null +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -0,0 +1,321 @@ +package org.asamk.signal.storage; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.asamk.signal.storage.contacts.JsonContactsStore; +import org.asamk.signal.storage.groups.JsonGroupStore; +import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; +import org.asamk.signal.storage.threads.JsonThreadStore; +import org.asamk.signal.util.IOUtils; +import org.asamk.signal.util.Util; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; +import org.whispersystems.libsignal.util.Medium; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.internal.util.Base64; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.util.Collection; + +public class SignalAccount { + + private final ObjectMapper jsonProcessor = new ObjectMapper(); + private FileChannel fileChannel; + private FileLock lock; + private String username; + private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; + private boolean isMultiDevice = false; + private String password; + private String registrationLockPin; + private String signalingKey; + private byte[] profileKey; + private int preKeyIdOffset; + private int nextSignedPreKeyId; + + private boolean registered = false; + + private JsonSignalProtocolStore signalProtocolStore; + private JsonGroupStore groupStore; + private JsonContactsStore contactStore; + private JsonThreadStore threadStore; + + private SignalAccount() { + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect + jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it. + jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); + jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE); + jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + } + + public static SignalAccount load(String dataPath, String username) throws IOException { + SignalAccount account = new SignalAccount(); + IOUtils.createPrivateDirectories(dataPath); + account.openFileChannel(getFileName(dataPath, username)); + account.load(); + return account; + } + + public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, byte[] profileKey) throws IOException { + IOUtils.createPrivateDirectories(dataPath); + + SignalAccount account = new SignalAccount(); + account.openFileChannel(getFileName(dataPath, username)); + + account.username = username; + account.profileKey = profileKey; + account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); + account.groupStore = new JsonGroupStore(); + account.threadStore = new JsonThreadStore(); + account.contactStore = new JsonContactsStore(); + account.registered = false; + + return account; + } + + public static SignalAccount createLinkedAccount(String dataPath, String username, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, byte[] profileKey) throws IOException { + IOUtils.createPrivateDirectories(dataPath); + + SignalAccount account = new SignalAccount(); + account.openFileChannel(getFileName(dataPath, username)); + + account.username = username; + account.password = password; + account.profileKey = profileKey; + account.deviceId = deviceId; + account.signalingKey = signalingKey; + account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); + account.groupStore = new JsonGroupStore(); + account.threadStore = new JsonThreadStore(); + account.contactStore = new JsonContactsStore(); + account.registered = true; + account.isMultiDevice = true; + + return account; + } + + public static SignalAccount createTemporaryAccount(IdentityKeyPair identityKey, int registrationId) { + SignalAccount account = new SignalAccount(); + + account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId); + account.registered = false; + + return account; + } + + public static String getFileName(String dataPath, String username) { + return dataPath + "/" + username; + } + + public static boolean userExists(String dataPath, String username) { + if (username == null) { + return false; + } + File f = new File(getFileName(dataPath, username)); + return !(!f.exists() || f.isDirectory()); + } + + private void load() throws IOException { + JsonNode rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel)); + + JsonNode node = rootNode.get("deviceId"); + if (node != null) { + deviceId = node.asInt(); + } + username = Util.getNotNullNode(rootNode, "username").asText(); + password = Util.getNotNullNode(rootNode, "password").asText(); + JsonNode pinNode = rootNode.get("registrationLockPin"); + registrationLockPin = pinNode == null ? null : pinNode.asText(); + if (rootNode.has("signalingKey")) { + signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText(); + } + if (rootNode.has("preKeyIdOffset")) { + preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0); + } else { + preKeyIdOffset = 0; + } + if (rootNode.has("nextSignedPreKeyId")) { + nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt(); + } else { + nextSignedPreKeyId = 0; + } + if (rootNode.has("profileKey")) { + profileKey = Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()); + } + + signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class); + registered = Util.getNotNullNode(rootNode, "registered").asBoolean(); + JsonNode groupStoreNode = rootNode.get("groupStore"); + if (groupStoreNode != null) { + groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class); + } + if (groupStore == null) { + groupStore = new JsonGroupStore(); + } + + JsonNode contactStoreNode = rootNode.get("contactStore"); + if (contactStoreNode != null) { + contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class); + } + if (contactStore == null) { + contactStore = new JsonContactsStore(); + } + JsonNode threadStoreNode = rootNode.get("threadStore"); + if (threadStoreNode != null) { + threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class); + } + if (threadStore == null) { + threadStore = new JsonThreadStore(); + } + } + + public void save() { + if (fileChannel == null) { + return; + } + ObjectNode rootNode = jsonProcessor.createObjectNode(); + rootNode.put("username", username) + .put("deviceId", deviceId) + .put("password", password) + .put("registrationLockPin", registrationLockPin) + .put("signalingKey", signalingKey) + .put("preKeyIdOffset", preKeyIdOffset) + .put("nextSignedPreKeyId", nextSignedPreKeyId) + .put("registered", registered) + .putPOJO("axolotlStore", signalProtocolStore) + .putPOJO("groupStore", groupStore) + .putPOJO("contactStore", contactStore) + .putPOJO("threadStore", threadStore) + ; + try { + fileChannel.position(0); + jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode); + fileChannel.truncate(fileChannel.position()); + fileChannel.force(false); + } catch (Exception e) { + System.err.println(String.format("Error saving file: %s", e.getMessage())); + } + } + + private void openFileChannel(String fileName) throws IOException { + if (fileChannel != null) { + return; + } + + if (!new File(fileName).exists()) { + IOUtils.createPrivateFile(fileName); + } + fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel(); + lock = fileChannel.tryLock(); + if (lock == null) { + System.err.println("Config file is in use by another instance, waiting…"); + lock = fileChannel.lock(); + System.err.println("Config file lock acquired."); + } + } + + public void addPreKeys(Collection records) { + for (PreKeyRecord record : records) { + signalProtocolStore.storePreKey(record.getId(), record); + } + preKeyIdOffset = (preKeyIdOffset + records.size()) % Medium.MAX_VALUE; + } + + public void addSignedPreKey(SignedPreKeyRecord record) { + signalProtocolStore.storeSignedPreKey(record.getId(), record); + nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE; + } + + public JsonSignalProtocolStore getSignalProtocolStore() { + return signalProtocolStore; + } + + public JsonGroupStore getGroupStore() { + return groupStore; + } + + public JsonContactsStore getContactStore() { + return contactStore; + } + + public JsonThreadStore getThreadStore() { + return threadStore; + } + + public String getUsername() { + return username; + } + + public int getDeviceId() { + return deviceId; + } + + public String getPassword() { + return password; + } + + public void setPassword(final String password) { + this.password = password; + } + + public String getRegistrationLockPin() { + return registrationLockPin; + } + + public void setRegistrationLockPin(final String registrationLockPin) { + this.registrationLockPin = registrationLockPin; + } + + public String getSignalingKey() { + return signalingKey; + } + + public void setSignalingKey(final String signalingKey) { + this.signalingKey = signalingKey; + } + + public byte[] getProfileKey() { + return profileKey; + } + + public void setProfileKey(final byte[] profileKey) { + this.profileKey = profileKey; + } + + public int getPreKeyIdOffset() { + return preKeyIdOffset; + } + + public int getNextSignedPreKeyId() { + return nextSignedPreKeyId; + } + + public boolean isRegistered() { + return registered; + } + + public void setRegistered(final boolean registered) { + this.registered = registered; + } + + public boolean isMultiDevice() { + return isMultiDevice; + } + + public void setMultiDevice(final boolean multiDevice) { + isMultiDevice = multiDevice; + } +} diff --git a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java index 03b076cc..1d754968 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java +++ b/src/main/java/org/asamk/signal/storage/contacts/ContactInfo.java @@ -3,6 +3,7 @@ package org.asamk.signal.storage.contacts; import com.fasterxml.jackson.annotation.JsonProperty; public class ContactInfo { + @JsonProperty public String name; diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java index 2024b86d..45e35e99 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java @@ -14,13 +14,13 @@ import java.util.List; import java.util.Map; public class JsonContactsStore { + + private static final ObjectMapper jsonProcessor = new ObjectMapper(); @JsonProperty("contacts") @JsonSerialize(using = JsonContactsStore.MapToListSerializer.class) @JsonDeserialize(using = ContactsDeserializer.class) private Map contacts = new HashMap<>(); - private static final ObjectMapper jsonProcessor = new ObjectMapper(); - public void updateContact(ContactInfo contact) { contacts.put(contact.number, contact); } @@ -41,6 +41,7 @@ public class JsonContactsStore { } public static class MapToListSerializer extends JsonSerializer> { + @Override public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { jgen.writeObject(value.values()); @@ -48,6 +49,7 @@ public class JsonContactsStore { } public static class ContactsDeserializer extends JsonDeserializer> { + @Override public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { Map contacts = new HashMap<>(); diff --git a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java index 96147fe3..f614c87c 100644 --- a/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java +++ b/src/main/java/org/asamk/signal/storage/groups/GroupInfo.java @@ -8,6 +8,7 @@ import java.util.HashSet; import java.util.Set; public class GroupInfo { + @JsonProperty public final byte[] groupId; @@ -16,20 +17,13 @@ public class GroupInfo { @JsonProperty public Set members = new HashSet<>(); - - private long avatarId; - - @JsonIgnore - public long getAvatarId() { - return avatarId; - } - @JsonProperty public boolean active; - @JsonProperty public String color; + private long avatarId; + public GroupInfo(byte[] groupId) { this.groupId = groupId; } @@ -41,4 +35,9 @@ public class GroupInfo { this.avatarId = avatarId; this.color = color; } + + @JsonIgnore + public long getAvatarId() { + return avatarId; + } } diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index 1cc9f151..e8e97309 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -15,15 +15,16 @@ import java.util.List; import java.util.Map; public class JsonGroupStore { + + private static final ObjectMapper jsonProcessor = new ObjectMapper(); + + public static List groupsWithLegacyAvatarId = new ArrayList<>(); + @JsonProperty("groups") @JsonSerialize(using = JsonGroupStore.MapToListSerializer.class) @JsonDeserialize(using = JsonGroupStore.GroupsDeserializer.class) private Map groups = new HashMap<>(); - public static List groupsWithLegacyAvatarId = new ArrayList<>(); - - private static final ObjectMapper jsonProcessor = new ObjectMapper(); - public void updateGroup(GroupInfo group) { groups.put(Base64.encodeBytes(group.groupId), group); } @@ -38,6 +39,7 @@ public class JsonGroupStore { } public static class MapToListSerializer extends JsonSerializer> { + @Override public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { jgen.writeObject(value.values()); @@ -45,6 +47,7 @@ public class JsonGroupStore { } public static class GroupsDeserializer extends JsonDeserializer> { + @Override public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { Map groups = new HashMap<>(); diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index 922a88f5..e086b929 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -22,7 +22,6 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { private final IdentityKeyPair identityKeyPair; private final int localRegistrationId; - public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) { this.identityKeyPair = identityKeyPair; this.localRegistrationId = localRegistrationId; @@ -131,7 +130,6 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { int localRegistrationId = node.get("registrationId").asInt(); IdentityKeyPair identityKeyPair = new IdentityKeyPair(Base64.decode(node.get("identityKey").asText())); - JsonIdentityKeyStore keyStore = new JsonIdentityKeyStore(identityKeyPair, localRegistrationId); JsonNode trustedKeysNode = node.get("trustedKeys"); @@ -180,6 +178,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { } public class Identity { + IdentityKey identityKey; TrustLevel trustLevel; Date added; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java index d6bc02fa..184c084b 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java @@ -17,7 +17,6 @@ class JsonPreKeyStore implements PreKeyStore { private final Map store = new HashMap<>(); - public JsonPreKeyStore() { } @@ -60,7 +59,6 @@ class JsonPreKeyStore implements PreKeyStore { public JsonPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - Map preKeyMap = new HashMap<>(); if (node.isArray()) { for (JsonNode preKey : node) { diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index 1ae2e5df..bf6891c8 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -24,7 +24,6 @@ class JsonSessionStore implements SessionStore { this.sessions.putAll(sessions); } - @Override public synchronized SessionRecord loadSession(SignalProtocolAddress remoteAddress) { try { diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java index d7d2a265..a8c400ce 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java @@ -23,7 +23,6 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { } - public void addSignedPreKeys(Map preKeys) { store.putAll(preKeys); } @@ -77,7 +76,6 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { public JsonSignedPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - Map preKeyMap = new HashMap<>(); if (node.isArray()) { for (JsonNode preKey : node) { diff --git a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java b/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java index b32d629d..a9ce6fb6 100644 --- a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java +++ b/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java @@ -14,13 +14,14 @@ import java.util.List; import java.util.Map; public class JsonThreadStore { + + private static final ObjectMapper jsonProcessor = new ObjectMapper(); + @JsonProperty("threads") @JsonSerialize(using = JsonThreadStore.MapToListSerializer.class) @JsonDeserialize(using = ThreadsDeserializer.class) private Map threads = new HashMap<>(); - private static final ObjectMapper jsonProcessor = new ObjectMapper(); - public void updateThread(ThreadInfo thread) { threads.put(thread.id, thread); } @@ -34,6 +35,7 @@ public class JsonThreadStore { } public static class MapToListSerializer extends JsonSerializer> { + @Override public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { jgen.writeObject(value.values()); @@ -41,6 +43,7 @@ public class JsonThreadStore { } public static class ThreadsDeserializer extends JsonDeserializer> { + @Override public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { Map threads = new HashMap<>(); diff --git a/src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java b/src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java index 3fc28405..67e6b474 100644 --- a/src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java +++ b/src/main/java/org/asamk/signal/storage/threads/ThreadInfo.java @@ -3,6 +3,7 @@ package org.asamk.signal.storage.threads; import com.fasterxml.jackson.annotation.JsonProperty; public class ThreadInfo { + @JsonProperty public String id; diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index 69128d01..9b8c3b5b 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -33,13 +33,18 @@ public class IOUtils { return output.toString(); } - public static void createPrivateDirectories(String path) throws IOException { - final Path file = new File(path).toPath(); + public static void createPrivateDirectories(String directoryPath) throws IOException { + final File file = new File(directoryPath); + if (file.exists()) { + return; + } + + final Path path = file.toPath(); try { Set perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE); - Files.createDirectories(file, PosixFilePermissions.asFileAttribute(perms)); + Files.createDirectories(path, PosixFilePermissions.asFileAttribute(perms)); } catch (UnsupportedOperationException e) { - Files.createDirectories(file); + Files.createDirectories(path); } } diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index eec7d2f7..93a595d1 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -1,5 +1,8 @@ package org.asamk.signal.util; +import com.fasterxml.jackson.databind.JsonNode; + +import java.io.InvalidObjectException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.HashMap; @@ -53,4 +56,13 @@ public class Util { return buf.toString(); } + + public static JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException { + JsonNode node = parent.get(name); + if (node == null) { + throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name)); + } + + return node; + } } diff --git a/src/main/resources/org/asamk/signal/whisper.store b/src/main/resources/org/asamk/signal/manager/whisper.store similarity index 100% rename from src/main/resources/org/asamk/signal/whisper.store rename to src/main/resources/org/asamk/signal/manager/whisper.store From 184354ffb71ea643b62c01c8406402ea4f492ac1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 18 Nov 2018 19:51:21 +0100 Subject: [PATCH 0255/2005] Extract utils methods --- src/main/java/org/asamk/signal/Main.java | 21 +- .../org/asamk/signal/manager/Manager.java | 215 +++------------- .../java/org/asamk/signal/manager/Utils.java | 234 ++++++++++++++++++ src/main/java/org/asamk/signal/util/Util.java | 26 -- 4 files changed, 272 insertions(+), 224 deletions(-) create mode 100644 src/main/java/org/asamk/signal/manager/Utils.java diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 810f1deb..70a02738 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -70,8 +70,8 @@ import java.util.concurrent.TimeoutException; public class Main { - public static final String SIGNAL_BUSNAME = "org.asamk.Signal"; - public static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal"; + private static final String SIGNAL_BUSNAME = "org.asamk.Signal"; + private static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal"; public static void main(String[] args) { // Workaround for BKS truststore @@ -286,15 +286,12 @@ public class Main { } catch (IOException e) { e.printStackTrace(); return 3; - } catch (InvalidKeyException e) { + } catch (InvalidKeyException | URISyntaxException e) { e.printStackTrace(); return 2; } catch (AssertionError e) { handleAssertionError(e); return 1; - } catch (URISyntaxException e) { - e.printStackTrace(); - return 2; } break; case "listDevices": @@ -528,9 +525,9 @@ public class Main { if (groupName == null) { groupName = ""; } - List groupMembers = ns.getList("member"); + List groupMembers = ns.getList("member"); if (groupMembers == null) { - groupMembers = new ArrayList(); + groupMembers = new ArrayList<>(); } String groupAvatar = ns.getString("avatar"); if (groupAvatar == null) { @@ -943,7 +940,7 @@ public class Main { final Manager m; - public ReceiveMessageHandler(Manager m) { + ReceiveMessageHandler(Manager m) { this.m = m; } @@ -1207,7 +1204,7 @@ public class Main { final DBusConnection conn; - public DbusReceiveMessageHandler(Manager m, DBusConnection conn) { + DbusReceiveMessageHandler(Manager m, DBusConnection conn) { super(m); this.conn = conn; } @@ -1225,7 +1222,7 @@ public class Main { final Manager m; final ObjectMapper jsonProcessor; - public JsonReceiveMessageHandler(Manager m) { + JsonReceiveMessageHandler(Manager m) { this.m = m; this.jsonProcessor = new ObjectMapper(); jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect @@ -1256,7 +1253,7 @@ public class Main { final DBusConnection conn; - public JsonDbusReceiveMessageHandler(Manager m, DBusConnection conn) { + JsonDbusReceiveMessageHandler(Manager m, DBusConnection conn) { super(m); this.conn = conn; } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index e8860ef8..1b703e42 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -16,7 +16,6 @@ */ package org.asamk.signal.manager; -import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.asamk.signal.*; import org.asamk.signal.storage.SignalAccount; @@ -28,13 +27,10 @@ import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; import org.signal.libsignal.metadata.*; -import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.whispersystems.libsignal.*; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.fingerprint.Fingerprint; -import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.KeyHelper; @@ -57,7 +53,6 @@ import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptio import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; -import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; @@ -65,8 +60,6 @@ import org.whispersystems.signalservice.internal.util.Base64; import java.io.*; import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; @@ -99,43 +92,6 @@ public class Manager implements Signal { } - private static List getSignalServiceAttachments(List attachments) throws AttachmentInvalidException { - List SignalServiceAttachments = null; - if (attachments != null) { - SignalServiceAttachments = new ArrayList<>(attachments.size()); - for (String attachment : attachments) { - try { - SignalServiceAttachments.add(createAttachment(new File(attachment))); - } catch (IOException e) { - throw new AttachmentInvalidException(attachment, e); - } - } - } - return SignalServiceAttachments; - } - - private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { - InputStream attachmentStream = new FileInputStream(attachmentFile); - final long attachmentSize = attachmentFile.length(); - String mime = Files.probeContentType(attachmentFile.toPath()); - if (mime == null) { - mime = "application/octet-stream"; - } - // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option - Optional preview = Optional.absent(); - Optional caption = Optional.absent(); - return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, null); - } - - private static CertificateValidator getCertificateValidator() { - try { - ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); - return new CertificateValidator(unidentifiedSenderTrustRoot); - } catch (InvalidKeyException | IOException e) { - throw new AssertionError(e); - } - } - public String getUsername() { return username; } @@ -163,7 +119,7 @@ public class Manager implements Signal { } public boolean userHasKeys() { - return account.getSignalProtocolStore() != null; + return account != null && account.getSignalProtocolStore() != null; } public void init() throws IOException { @@ -253,7 +209,7 @@ public class Manager implements Signal { accountManager.setGcmId(Optional.absent()); } - public URI getDeviceLinkUri() throws TimeoutException, IOException { + public String getDeviceLinkUri() throws TimeoutException, IOException { if (account == null) { createNewIdentity(); } @@ -261,12 +217,7 @@ public class Manager implements Signal { accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, username, account.getPassword(), BaseConfig.USER_AGENT, timer); String uuid = accountManager.getNewDeviceUuid(); - try { - return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(getIdentity().serialize()), "utf-8")); - } catch (URISyntaxException e) { - // Shouldn't happen - return null; - } + return Utils.createDeviceLinkUri(new Utils.DeviceLinkInfo(uuid, getIdentity().getPublicKey())); } public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { @@ -290,6 +241,8 @@ public class Manager implements Signal { requestSyncGroups(); requestSyncContacts(); + requestSyncBlocked(); + requestSyncConfiguration(); account.save(); } @@ -305,17 +258,9 @@ public class Manager implements Signal { } public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - Map query = Util.getQueryMap(linkUri.getRawQuery()); - String deviceIdentifier = query.get("uuid"); - String publicKeyEncoded = query.get("pub_key"); + Utils.DeviceLinkInfo info = Utils.parseDeviceLinkUri(linkUri); - if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) { - throw new RuntimeException("Invalid device link uri"); - } - - ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); - - addDevice(deviceIdentifier, deviceKey); + addDevice(info.deviceIdentifier, info.deviceKey); } private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { @@ -362,6 +307,7 @@ public class Manager implements Signal { public void verifyAccount(String verificationCode, String pin) throws IOException { verificationCode = verificationCode.replace("-", ""); account.setSignalingKey(KeyUtils.createSignalingKey()); + // TODO make unrestricted unidentified access configurable accountManager.verifyAccountWithCode(verificationCode, account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false); //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); @@ -379,6 +325,7 @@ public class Manager implements Signal { } else { account.setRegistrationLockPin(null); } + account.save(); } private void refreshPreKeys() throws IOException { @@ -395,7 +342,7 @@ public class Manager implements Signal { return Optional.absent(); } - return Optional.of(createAttachment(file)); + return Optional.of(Utils.createAttachment(file)); } private Optional createContactAvatarAttachment(String number) throws IOException { @@ -404,7 +351,7 @@ public class Manager implements Signal { return Optional.absent(); } - return Optional.of(createAttachment(file)); + return Optional.of(Utils.createAttachment(file)); } private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException { @@ -430,7 +377,7 @@ public class Manager implements Signal { throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { - messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); + messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); } if (groupId != null) { SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER) @@ -484,7 +431,7 @@ public class Manager implements Signal { Set newMembers = new HashSet<>(); for (String member : members) { try { - member = canonicalizeNumber(member); + member = Utils.canonicalizeNumber(member, username); } catch (InvalidNumberException e) { System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage()); System.err.println("Aborting…"); @@ -552,7 +499,7 @@ public class Manager implements Signal { File aFile = getGroupAvatarFile(g.groupId); if (aFile.exists()) { try { - group.withAvatar(createAttachment(aFile)); + group.withAvatar(Utils.createAttachment(aFile)); } catch (IOException e) { throw new AttachmentInvalidException(aFile.toString(), e); } @@ -593,7 +540,7 @@ public class Manager implements Signal { throws IOException, EncapsulatedExceptions, AttachmentInvalidException { final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); if (attachments != null) { - messageBuilder.withAttachments(getSignalServiceAttachments(attachments)); + messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); } sendMessageLegacy(messageBuilder, recipients); } @@ -796,8 +743,11 @@ public class Manager implements Signal { private List sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection recipients) throws IOException { - Set recipientsTS = getSignalServiceAddresses(recipients); - if (recipientsTS == null) return Collections.emptyList(); + Set recipientsTS = Utils.getSignalServiceAddresses(recipients, username); + if (recipientsTS == null) { + account.save(); + return Collections.emptyList(); + } SignalServiceDataMessage message = null; try { @@ -849,23 +799,8 @@ public class Manager implements Signal { } } - private Set getSignalServiceAddresses(Collection recipients) { - Set recipientsTS = new HashSet<>(recipients.size()); - for (String recipient : recipients) { - try { - recipientsTS.add(getPushAddress(recipient)); - } catch (InvalidNumberException e) { - System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - account.save(); - return null; - } - } - return recipientsTS; - } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException { - SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), account.getSignalProtocolStore(), getCertificateValidator()); + SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), account.getSignalProtocolStore(), Utils.getCertificateValidator()); try { return cipher.decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { @@ -978,6 +913,9 @@ public class Manager implements Signal { } } if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { + if (source.equals(username)) { + this.account.setProfileKey(message.getProfileKey().get()); + } ContactInfo contact = account.getContactStore().getContact(source); if (contact == null) { contact = new ContactInfo(); @@ -1003,7 +941,7 @@ public class Manager implements Signal { } SignalServiceEnvelope envelope; try { - envelope = loadEnvelope(fileEntry); + envelope = Utils.loadEnvelope(fileEntry); if (envelope == null) { continue; } @@ -1054,7 +992,7 @@ public class Manager implements Signal { // store message on disk, before acknowledging receipt to the server try { File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp()); - storeEnvelope(envelope, cacheFile); + Utils.storeEnvelope(envelope, cacheFile); } catch (IOException e) { System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage()); } @@ -1242,73 +1180,6 @@ public class Manager implements Signal { } } - private SignalServiceEnvelope loadEnvelope(File file) throws IOException { - try (FileInputStream f = new FileInputStream(file)) { - DataInputStream in = new DataInputStream(f); - int version = in.readInt(); - if (version > 2) { - return null; - } - int type = in.readInt(); - String source = in.readUTF(); - int sourceDevice = in.readInt(); - if (version == 1) { - // read legacy relay field - in.readUTF(); - } - long timestamp = in.readLong(); - byte[] content = null; - int contentLen = in.readInt(); - if (contentLen > 0) { - content = new byte[contentLen]; - in.readFully(content); - } - byte[] legacyMessage = null; - int legacyMessageLen = in.readInt(); - if (legacyMessageLen > 0) { - legacyMessage = new byte[legacyMessageLen]; - in.readFully(legacyMessage); - } - long serverTimestamp = 0; - String uuid = null; - if (version == 2) { - serverTimestamp = in.readLong(); - uuid = in.readUTF(); - if ("".equals(uuid)) { - uuid = null; - } - } - return new SignalServiceEnvelope(type, source, sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); - } - } - - private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { - try (FileOutputStream f = new FileOutputStream(file)) { - try (DataOutputStream out = new DataOutputStream(f)) { - out.writeInt(2); // version - out.writeInt(envelope.getType()); - out.writeUTF(envelope.getSource()); - out.writeInt(envelope.getSourceDevice()); - out.writeLong(envelope.getTimestamp()); - if (envelope.hasContent()) { - out.writeInt(envelope.getContent().length); - out.write(envelope.getContent()); - } else { - out.writeInt(0); - } - if (envelope.hasLegacyMessage()) { - out.writeInt(envelope.getLegacyMessage().length); - out.write(envelope.getLegacyMessage()); - } else { - out.writeInt(0); - } - out.writeLong(envelope.getServerTimestamp()); - String uuid = envelope.getUuid(); - out.writeUTF(uuid == null ? "" : uuid); - } - } - } - private File getContactAvatarFile(String number) { return new File(avatarsPath, "contact-" + number); } @@ -1320,7 +1191,7 @@ public class Manager implements Signal { return retrieveAttachment(pointer, getContactAvatarFile(number), false); } else { SignalServiceAttachmentStream stream = attachment.asStream(); - return retrieveAttachment(stream, getContactAvatarFile(number)); + return Utils.retrieveAttachment(stream, getContactAvatarFile(number)); } } @@ -1335,7 +1206,7 @@ public class Manager implements Signal { return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false); } else { SignalServiceAttachmentStream stream = attachment.asStream(); - return retrieveAttachment(stream, getGroupAvatarFile(groupId)); + return Utils.retrieveAttachment(stream, getGroupAvatarFile(groupId)); } } @@ -1348,23 +1219,6 @@ public class Manager implements Signal { return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true); } - private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException { - InputStream input = stream.getInputStream(); - - try (OutputStream output = new FileOutputStream(outputFile)) { - byte[] buffer = new byte[4096]; - int read; - - while ((read = input.read(buffer)) != -1) { - output.write(buffer, 0, read); - } - } catch (FileNotFoundException e) { - e.printStackTrace(); - return null; - } - return outputFile; - } - private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException { if (storePreview && pointer.getPreview().isPresent()) { File previewFile = new File(outputFile + ".preview"); @@ -1407,16 +1261,6 @@ public class Manager implements Signal { return messageReceiver.retrieveAttachment(pointer, tmpFile, BaseConfig.MAX_ATTACHMENT_SIZE); } - private String canonicalizeNumber(String number) throws InvalidNumberException { - String localNumber = username; - return PhoneNumberFormatter.formatNumber(number, localNumber); - } - - private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException { - String e164number = canonicalizeNumber(number); - return new SignalServiceAddress(e164number); - } - @Override public boolean isRemote() { return false; @@ -1618,8 +1462,7 @@ public class Manager implements Signal { } public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) { - Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey); - return fingerprint.getDisplayableFingerprint().getDisplayText(); + return Utils.computeSafetyNumber(username, getIdentity(), theirUsername, theirIdentityKey); } public interface ReceiveMessageHandler { diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java new file mode 100644 index 00000000..f47dc1ce --- /dev/null +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -0,0 +1,234 @@ +package org.asamk.signal.manager; + +import org.apache.http.util.TextUtils; +import org.asamk.signal.AttachmentInvalidException; +import org.signal.libsignal.metadata.certificate.CertificateValidator; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; +import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.internal.util.Base64; + +import java.io.*; +import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.file.Files; +import java.util.*; + +class Utils { + + static List getSignalServiceAttachments(List attachments) throws AttachmentInvalidException { + List SignalServiceAttachments = null; + if (attachments != null) { + SignalServiceAttachments = new ArrayList<>(attachments.size()); + for (String attachment : attachments) { + try { + SignalServiceAttachments.add(createAttachment(new File(attachment))); + } catch (IOException e) { + throw new AttachmentInvalidException(attachment, e); + } + } + } + return SignalServiceAttachments; + } + + static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException { + InputStream attachmentStream = new FileInputStream(attachmentFile); + final long attachmentSize = attachmentFile.length(); + String mime = Files.probeContentType(attachmentFile.toPath()); + if (mime == null) { + mime = "application/octet-stream"; + } + // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option + Optional preview = Optional.absent(); + Optional caption = Optional.absent(); + return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, null); + } + + static CertificateValidator getCertificateValidator() { + try { + ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); + return new CertificateValidator(unidentifiedSenderTrustRoot); + } catch (InvalidKeyException | IOException e) { + throw new AssertionError(e); + } + } + + private static Map getQueryMap(String query) { + String[] params = query.split("&"); + Map map = new HashMap<>(); + for (String param : params) { + String name = null; + final String[] paramParts = param.split("="); + try { + name = URLDecoder.decode(paramParts[0], "utf-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + } + String value = null; + try { + value = URLDecoder.decode(paramParts[1], "utf-8"); + } catch (UnsupportedEncodingException e) { + // Impossible + } + map.put(name, value); + } + return map; + } + + static String createDeviceLinkUri(DeviceLinkInfo info) { + try { + return "tsdevice:/?uuid=" + URLEncoder.encode(info.deviceIdentifier, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(info.deviceKey.serialize()), "utf-8"); + } catch (UnsupportedEncodingException e) { + // Shouldn't happen + return null; + } + } + + static DeviceLinkInfo parseDeviceLinkUri(URI linkUri) throws IOException, InvalidKeyException { + Map query = getQueryMap(linkUri.getRawQuery()); + String deviceIdentifier = query.get("uuid"); + String publicKeyEncoded = query.get("pub_key"); + + if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) { + throw new RuntimeException("Invalid device link uri"); + } + + ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0); + + return new DeviceLinkInfo(deviceIdentifier, deviceKey); + } + + static Set getSignalServiceAddresses(Collection recipients, String localNumber) { + Set recipientsTS = new HashSet<>(recipients.size()); + for (String recipient : recipients) { + try { + recipientsTS.add(getPushAddress(recipient, localNumber)); + } catch (InvalidNumberException e) { + System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage()); + System.err.println("Aborting sending."); + return null; + } + } + return recipientsTS; + } + + static String canonicalizeNumber(String number, String localNumber) throws InvalidNumberException { + return PhoneNumberFormatter.formatNumber(number, localNumber); + } + + private static SignalServiceAddress getPushAddress(String number, String localNumber) throws InvalidNumberException { + String e164number = canonicalizeNumber(number, localNumber); + return new SignalServiceAddress(e164number); + } + + static SignalServiceEnvelope loadEnvelope(File file) throws IOException { + try (FileInputStream f = new FileInputStream(file)) { + DataInputStream in = new DataInputStream(f); + int version = in.readInt(); + if (version > 2) { + return null; + } + int type = in.readInt(); + String source = in.readUTF(); + int sourceDevice = in.readInt(); + if (version == 1) { + // read legacy relay field + in.readUTF(); + } + long timestamp = in.readLong(); + byte[] content = null; + int contentLen = in.readInt(); + if (contentLen > 0) { + content = new byte[contentLen]; + in.readFully(content); + } + byte[] legacyMessage = null; + int legacyMessageLen = in.readInt(); + if (legacyMessageLen > 0) { + legacyMessage = new byte[legacyMessageLen]; + in.readFully(legacyMessage); + } + long serverTimestamp = 0; + String uuid = null; + if (version == 2) { + serverTimestamp = in.readLong(); + uuid = in.readUTF(); + if ("".equals(uuid)) { + uuid = null; + } + } + return new SignalServiceEnvelope(type, source, sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid); + } + } + + static void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException { + try (FileOutputStream f = new FileOutputStream(file)) { + try (DataOutputStream out = new DataOutputStream(f)) { + out.writeInt(2); // version + out.writeInt(envelope.getType()); + out.writeUTF(envelope.getSource()); + out.writeInt(envelope.getSourceDevice()); + out.writeLong(envelope.getTimestamp()); + if (envelope.hasContent()) { + out.writeInt(envelope.getContent().length); + out.write(envelope.getContent()); + } else { + out.writeInt(0); + } + if (envelope.hasLegacyMessage()) { + out.writeInt(envelope.getLegacyMessage().length); + out.write(envelope.getLegacyMessage()); + } else { + out.writeInt(0); + } + out.writeLong(envelope.getServerTimestamp()); + String uuid = envelope.getUuid(); + out.writeUTF(uuid == null ? "" : uuid); + } + } + } + + static File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException { + InputStream input = stream.getInputStream(); + + try (OutputStream output = new FileOutputStream(outputFile)) { + byte[] buffer = new byte[4096]; + int read; + + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } + return outputFile; + } + + static String computeSafetyNumber(String ownUsername, IdentityKey ownIdentityKey, String theirUsername, IdentityKey theirIdentityKey) { + Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(ownUsername, ownIdentityKey, theirUsername, theirIdentityKey); + return fingerprint.getDisplayableFingerprint().getDisplayText(); + } + + static class DeviceLinkInfo { + + String deviceIdentifier; + ECPublicKey deviceKey; + + DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) { + this.deviceIdentifier = deviceIdentifier; + this.deviceKey = deviceKey; + } + } +} diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 93a595d1..5a1dcdda 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -3,10 +3,6 @@ package org.asamk.signal.util; import com.fasterxml.jackson.databind.JsonNode; import java.io.InvalidObjectException; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.util.HashMap; -import java.util.Map; public class Util { @@ -23,28 +19,6 @@ public class Util { return f.toString(); } - public static Map getQueryMap(String query) { - String[] params = query.split("&"); - Map map = new HashMap<>(); - for (String param : params) { - String name = null; - final String[] paramParts = param.split("="); - try { - name = URLDecoder.decode(paramParts[0], "utf-8"); - } catch (UnsupportedEncodingException e) { - // Impossible - } - String value = null; - try { - value = URLDecoder.decode(paramParts[1], "utf-8"); - } catch (UnsupportedEncodingException e) { - // Impossible - } - map.put(name, value); - } - return map; - } - public static String join(CharSequence separator, Iterable list) { StringBuilder buf = new StringBuilder(); for (CharSequence str : list) { From 860ec6f5dcda56b55e0e756e862c8c55865ccd19 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 19 Nov 2018 22:40:16 +0100 Subject: [PATCH 0256/2005] Extract static methods from Main --- .../signal/DbusReceiveMessageHandler.java | 25 + .../asamk/signal/GroupIdFormatException.java | 10 + .../signal/JsonDbusReceiveMessageHandler.java | 71 +++ .../signal/JsonReceiveMessageHandler.java | 46 ++ src/main/java/org/asamk/signal/Main.java | 456 +----------------- .../asamk/signal/ReceiveMessageHandler.java | 279 +++++++++++ .../org/asamk/signal/util/ErrorUtils.java | 62 +++ src/main/java/org/asamk/signal/util/Util.java | 11 + 8 files changed, 520 insertions(+), 440 deletions(-) create mode 100644 src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java create mode 100644 src/main/java/org/asamk/signal/GroupIdFormatException.java create mode 100644 src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java create mode 100644 src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java create mode 100644 src/main/java/org/asamk/signal/ReceiveMessageHandler.java create mode 100644 src/main/java/org/asamk/signal/util/ErrorUtils.java diff --git a/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java new file mode 100644 index 00000000..2ea51e2e --- /dev/null +++ b/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java @@ -0,0 +1,25 @@ +package org.asamk.signal; + +import org.asamk.signal.manager.Manager; +import org.freedesktop.dbus.DBusConnection; +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; + +class DbusReceiveMessageHandler extends ReceiveMessageHandler { + + private final DBusConnection conn; + private final String objectPath; + + DbusReceiveMessageHandler(Manager m, DBusConnection conn, final String objectPath) { + super(m); + this.conn = conn; + this.objectPath = objectPath; + } + + @Override + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { + super.handleMessage(envelope, content, exception); + + JsonDbusReceiveMessageHandler.sendReceivedMessageToDbus(envelope, content, conn, objectPath, m); + } +} diff --git a/src/main/java/org/asamk/signal/GroupIdFormatException.java b/src/main/java/org/asamk/signal/GroupIdFormatException.java new file mode 100644 index 00000000..62add535 --- /dev/null +++ b/src/main/java/org/asamk/signal/GroupIdFormatException.java @@ -0,0 +1,10 @@ +package org.asamk.signal; + +import java.io.IOException; + +public class GroupIdFormatException extends Exception { + + public GroupIdFormatException(String groupId, IOException e) { + super("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage()); + } +} diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java new file mode 100644 index 00000000..533a1aed --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -0,0 +1,71 @@ +package org.asamk.signal; + +import org.asamk.Signal; +import org.asamk.signal.manager.Manager; +import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.exceptions.DBusException; +import org.whispersystems.signalservice.api.messages.*; + +import java.util.ArrayList; +import java.util.List; + +class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { + + private final DBusConnection conn; + + private final String objectPath; + + JsonDbusReceiveMessageHandler(Manager m, DBusConnection conn, final String objectPath) { + super(m); + this.conn = conn; + this.objectPath = objectPath; + } + + static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, final String objectPath, Manager m) { + if (envelope.isReceipt()) { + try { + conn.sendSignal(new Signal.ReceiptReceived( + objectPath, + envelope.getTimestamp(), + envelope.getSource() + )); + } catch (DBusException e) { + e.printStackTrace(); + } + } else if (content != null && content.getDataMessage().isPresent()) { + SignalServiceDataMessage message = content.getDataMessage().get(); + + if (!message.isEndSession() && + !(message.getGroupInfo().isPresent() && + message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { + List attachments = new ArrayList<>(); + if (message.getAttachments().isPresent()) { + for (SignalServiceAttachment attachment : message.getAttachments().get()) { + if (attachment.isPointer()) { + attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); + } + } + } + + try { + conn.sendSignal(new Signal.MessageReceived( + objectPath, + message.getTimestamp(), + envelope.getSource(), + message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], + message.getBody().isPresent() ? message.getBody().get() : "", + attachments)); + } catch (DBusException e) { + e.printStackTrace(); + } + } + } + } + + @Override + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { + super.handleMessage(envelope, content, exception); + + sendReceivedMessageToDbus(envelope, content, conn, objectPath, m); + } +} diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java new file mode 100644 index 00000000..7ae5d45d --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -0,0 +1,46 @@ +package org.asamk.signal; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; + +import java.io.IOException; + +class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler { + + final Manager m; + private final ObjectMapper jsonProcessor; + + JsonReceiveMessageHandler(Manager m) { + this.m = m; + this.jsonProcessor = new ObjectMapper(); + jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect + jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); + jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); + } + + @Override + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { + ObjectNode result = jsonProcessor.createObjectNode(); + if (exception != null) { + result.putPOJO("error", new JsonError(exception)); + } + if (envelope != null) { + result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content)); + } + try { + jsonProcessor.writeValue(System.out, result); + System.out.println(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 70a02738..604c51ee 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -16,13 +16,6 @@ */ package org.asamk.signal; -import com.fasterxml.jackson.annotation.JsonAutoDetect; -import com.fasterxml.jackson.annotation.PropertyAccessor; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.databind.node.ObjectNode; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; @@ -30,7 +23,6 @@ import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.asamk.signal.manager.BaseConfig; import org.asamk.signal.manager.Manager; -import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.util.DateUtils; @@ -43,14 +35,8 @@ import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.*; -import org.whispersystems.signalservice.api.messages.calls.*; -import org.whispersystems.signalservice.api.messages.multidevice.*; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; -import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; -import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.internal.push.LockedException; import org.whispersystems.signalservice.internal.util.Base64; @@ -68,6 +54,8 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import static org.asamk.signal.util.ErrorUtils.*; + public class Main { private static final String SIGNAL_BUSNAME = "org.asamk.Signal"; @@ -378,7 +366,7 @@ public class Main { attachments = new ArrayList<>(); } if (ns.getString("group") != null) { - byte[] groupId = decodeGroupId(ns.getString("group")); + byte[] groupId = Util.decodeGroupId(ns.getString("group")); ts.sendGroupMessage(messageText, attachments, groupId); } else { ts.sendMessage(messageText, attachments, ns.getList("recipient")); @@ -405,6 +393,9 @@ public class Main { } catch (DBusExecutionException e) { handleDBusExecutionException(e); return 1; + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; } } @@ -488,7 +479,7 @@ public class Main { } try { - m.sendQuitGroupMessage(decodeGroupId(ns.getString("group"))); + m.sendQuitGroupMessage(Util.decodeGroupId(ns.getString("group"))); } catch (IOException e) { handleIOException(e); return 3; @@ -504,6 +495,9 @@ public class Main { } catch (NotAGroupMemberException e) { handleNotAGroupMemberException(e); return 1; + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; } break; @@ -516,7 +510,7 @@ public class Main { try { byte[] groupId = null; if (ns.getString("group") != null) { - groupId = decodeGroupId(ns.getString("group")); + groupId = Util.decodeGroupId(ns.getString("group")); } if (groupId == null) { groupId = new byte[0]; @@ -553,6 +547,9 @@ public class Main { } catch (EncapsulatedExceptions e) { handleEncapsulatedExceptions(e); return 3; + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; } break; @@ -674,7 +671,7 @@ public class Main { } ignoreAttachments = ns.getBoolean("ignore_attachments"); try { - m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, ns.getBoolean("json") ? new JsonDbusReceiveMessageHandler(m, conn) : new DbusReceiveMessageHandler(m, conn)); + m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, ns.getBoolean("json") ? new JsonDbusReceiveMessageHandler(m, conn, Main.SIGNAL_OBJECTPATH) : new DbusReceiveMessageHandler(m, conn, Main.SIGNAL_OBJECTPATH)); } catch (IOException e) { System.err.println("Error while receiving messages: " + e.getMessage()); return 3; @@ -714,33 +711,6 @@ public class Main { } } - private static void handleGroupNotFoundException(GroupNotFoundException e) { - System.err.println("Failed to send to group: " + e.getMessage()); - System.err.println("Aborting sending."); - } - - private static void handleNotAGroupMemberException(NotAGroupMemberException e) { - System.err.println("Failed to send to group: " + e.getMessage()); - System.err.println("Update the group on another device to readd the user to this group."); - System.err.println("Aborting sending."); - } - - private static void handleDBusExecutionException(DBusExecutionException e) { - System.err.println("Cannot connect to dbus: " + e.getMessage()); - System.err.println("Aborting."); - } - - private static byte[] decodeGroupId(String groupId) { - try { - return Base64.decode(groupId); - } catch (IOException e) { - System.err.println("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage()); - System.err.println("Aborting sending."); - System.exit(1); - return null; - } - } - private static Namespace parseArgs(String[] args) { ArgumentParser parser = ArgumentParsers.newFor("signal-cli") .build() @@ -912,398 +882,4 @@ public class Main { return null; } } - - private static void handleAssertionError(AssertionError e) { - System.err.println("Failed to send/receive message (Assertion): " + e.getMessage()); - e.printStackTrace(); - System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); - } - - private static void handleEncapsulatedExceptions(EncapsulatedExceptions e) { - System.err.println("Failed to send (some) messages:"); - for (NetworkFailureException n : e.getNetworkExceptions()) { - System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage()); - } - for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) { - System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage()); - } - for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) { - System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage()); - } - } - - private static void handleIOException(IOException e) { - System.err.println("Failed to send message: " + e.getMessage()); - } - - private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { - - final Manager m; - - ReceiveMessageHandler(Manager m) { - this.m = m; - } - - @Override - public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { - SignalServiceAddress source = envelope.getSourceAddress(); - ContactInfo sourceContact = m.getContact(source.getNumber()); - System.out.println(String.format("Envelope from: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + source.getNumber(), envelope.getSourceDevice())); - if (source.getRelay().isPresent()) { - System.out.println("Relayed by: " + source.getRelay().get()); - } - System.out.println("Timestamp: " + DateUtils.formatTimestamp(envelope.getTimestamp())); - if (envelope.isUnidentifiedSender()) { - System.out.println("Sent by unidentified/sealed sender"); - } - - if (envelope.isReceipt()) { - System.out.println("Got receipt."); - } else if (envelope.isSignalMessage() | envelope.isPreKeySignalMessage()) { - if (exception != null) { - if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) { - org.whispersystems.libsignal.UntrustedIdentityException e = (org.whispersystems.libsignal.UntrustedIdentityException) exception; - System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message."); - System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getName() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getName() + "' to mark it as trusted"); - System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getName() + "' to trust it without verification"); - } else { - System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")"); - } - } - if (content == null) { - System.out.println("Failed to decrypt message."); - } else { - if (content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); - handleSignalServiceDataMessage(message); - } - if (content.getSyncMessage().isPresent()) { - System.out.println("Received a sync message"); - SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); - - if (syncMessage.getContacts().isPresent()) { - final ContactsMessage contactsMessage = syncMessage.getContacts().get(); - if (contactsMessage.isComplete()) { - System.out.println("Received complete sync contacts"); - } else { - System.out.println("Received sync contacts"); - } - printAttachment(contactsMessage.getContactsStream()); - } - if (syncMessage.getGroups().isPresent()) { - System.out.println("Received sync groups"); - printAttachment(syncMessage.getGroups().get()); - } - if (syncMessage.getRead().isPresent()) { - System.out.println("Received sync read messages list"); - for (ReadMessage rm : syncMessage.getRead().get()) { - ContactInfo fromContact = m.getContact(rm.getSender()); - System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp())); - } - } - if (syncMessage.getRequest().isPresent()) { - System.out.println("Received sync request"); - if (syncMessage.getRequest().get().isContactsRequest()) { - System.out.println(" - contacts request"); - } - if (syncMessage.getRequest().get().isGroupsRequest()) { - System.out.println(" - groups request"); - } - } - if (syncMessage.getSent().isPresent()) { - System.out.println("Received sync sent message"); - final SentTranscriptMessage sentTranscriptMessage = syncMessage.getSent().get(); - String to; - if (sentTranscriptMessage.getDestination().isPresent()) { - String dest = sentTranscriptMessage.getDestination().get(); - ContactInfo destContact = m.getContact(dest); - to = (destContact == null ? "" : "“" + destContact.name + "” ") + dest; - } else { - to = "Unknown"; - } - System.out.println("To: " + to + " , Message timestamp: " + DateUtils.formatTimestamp(sentTranscriptMessage.getTimestamp())); - if (sentTranscriptMessage.getExpirationStartTimestamp() > 0) { - System.out.println("Expiration started at: " + DateUtils.formatTimestamp(sentTranscriptMessage.getExpirationStartTimestamp())); - } - SignalServiceDataMessage message = sentTranscriptMessage.getMessage(); - handleSignalServiceDataMessage(message); - } - if (syncMessage.getBlockedList().isPresent()) { - System.out.println("Received sync message with block list"); - System.out.println("Blocked numbers:"); - final BlockedListMessage blockedList = syncMessage.getBlockedList().get(); - for (String number : blockedList.getNumbers()) { - System.out.println(" - " + number); - } - } - if (syncMessage.getVerified().isPresent()) { - System.out.println("Received sync message with verified identities:"); - final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); - System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified()); - String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey())); - System.out.println(" " + safetyNumber); - } - if (syncMessage.getConfiguration().isPresent()) { - System.out.println("Received sync message with configuration:"); - final ConfigurationMessage configurationMessage = syncMessage.getConfiguration().get(); - if (configurationMessage.getReadReceipts().isPresent()) { - System.out.println(" - Read receipts: " + (configurationMessage.getReadReceipts().get() ? "enabled" : "disabled")); - } - } - } - if (content.getCallMessage().isPresent()) { - System.out.println("Received a call message"); - SignalServiceCallMessage callMessage = content.getCallMessage().get(); - if (callMessage.getAnswerMessage().isPresent()) { - AnswerMessage answerMessage = callMessage.getAnswerMessage().get(); - System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getDescription()); - } - if (callMessage.getBusyMessage().isPresent()) { - BusyMessage busyMessage = callMessage.getBusyMessage().get(); - System.out.println("Busy message: " + busyMessage.getId()); - } - if (callMessage.getHangupMessage().isPresent()) { - HangupMessage hangupMessage = callMessage.getHangupMessage().get(); - System.out.println("Hangup message: " + hangupMessage.getId()); - } - if (callMessage.getIceUpdateMessages().isPresent()) { - List iceUpdateMessages = callMessage.getIceUpdateMessages().get(); - for (IceUpdateMessage iceUpdateMessage : iceUpdateMessages) { - System.out.println("Ice update message: " + iceUpdateMessage.getId() + ", sdp: " + iceUpdateMessage.getSdp()); - } - } - if (callMessage.getOfferMessage().isPresent()) { - OfferMessage offerMessage = callMessage.getOfferMessage().get(); - System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getDescription()); - } - } - if (content.getReceiptMessage().isPresent()) { - System.out.println("Received a receipt message"); - SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get(); - System.out.println(" - When: " + DateUtils.formatTimestamp(receiptMessage.getWhen())); - if (receiptMessage.isDeliveryReceipt()) { - System.out.println(" - Is delivery receipt"); - } - if (receiptMessage.isReadReceipt()) { - System.out.println(" - Is read receipt"); - } - System.out.println(" - Timestamps:"); - for (long timestamp : receiptMessage.getTimestamps()) { - System.out.println(" " + DateUtils.formatTimestamp(timestamp)); - } - } - if (content.getTypingMessage().isPresent()) { - System.out.println("Received a typing message"); - SignalServiceTypingMessage typingMessage = content.getTypingMessage().get(); - System.out.println(" - Action: " + typingMessage.getAction()); - System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp())); - if (typingMessage.getGroupId().isPresent()) { - GroupInfo group = m.getGroup(typingMessage.getGroupId().get()); - if (group != null) { - System.out.println(" Name: " + group.name); - } else { - System.out.println(" Name: "); - } - } - } - } - } else { - System.out.println("Unknown message received."); - } - System.out.println(); - } - - private void handleSignalServiceDataMessage(SignalServiceDataMessage message) { - System.out.println("Message timestamp: " + DateUtils.formatTimestamp(message.getTimestamp())); - - if (message.getBody().isPresent()) { - System.out.println("Body: " + message.getBody().get()); - } - if (message.getGroupInfo().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupInfo().get(); - System.out.println("Group info:"); - System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); - if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) { - System.out.println(" Name: " + groupInfo.getName().get()); - } else { - GroupInfo group = m.getGroup(groupInfo.getGroupId()); - if (group != null) { - System.out.println(" Name: " + group.name); - } else { - System.out.println(" Name: "); - } - } - System.out.println(" Type: " + groupInfo.getType()); - if (groupInfo.getMembers().isPresent()) { - for (String member : groupInfo.getMembers().get()) { - System.out.println(" Member: " + member); - } - } - if (groupInfo.getAvatar().isPresent()) { - System.out.println(" Avatar:"); - printAttachment(groupInfo.getAvatar().get()); - } - } - if (message.isEndSession()) { - System.out.println("Is end session"); - } - if (message.isExpirationUpdate()) { - System.out.println("Is Expiration update: " + message.isExpirationUpdate()); - } - if (message.getExpiresInSeconds() > 0) { - System.out.println("Expires in: " + message.getExpiresInSeconds() + " seconds"); - } - if (message.getProfileKey().isPresent()) { - System.out.println("Profile key update, key length:" + message.getProfileKey().get().length); - } - - if (message.getQuote().isPresent()) { - SignalServiceDataMessage.Quote quote = message.getQuote().get(); - System.out.println("Quote: (" + quote.getId() + ")"); - System.out.println(" Author: " + quote.getAuthor().getNumber()); - System.out.println(" Text: " + quote.getText()); - if (quote.getAttachments().size() > 0) { - System.out.println(" Attachments: "); - for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : quote.getAttachments()) { - System.out.println(" Filename: " + attachment.getFileName()); - System.out.println(" Type: " + attachment.getContentType()); - System.out.println(" Thumbnail:"); - if (attachment.getThumbnail() != null) { - printAttachment(attachment.getThumbnail()); - } - } - } - } - - if (message.getAttachments().isPresent()) { - System.out.println("Attachments: "); - for (SignalServiceAttachment attachment : message.getAttachments().get()) { - printAttachment(attachment); - } - } - } - - private void printAttachment(SignalServiceAttachment attachment) { - System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); - if (attachment.isPointer()) { - final SignalServiceAttachmentPointer pointer = attachment.asPointer(); - System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length); - System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-")); - System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); - System.out.println(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no")); - System.out.println(" Dimensions: " + pointer.getWidth() + "x" + pointer.getHeight()); - File file = m.getAttachmentFile(pointer.getId()); - if (file.exists()) { - System.out.println(" Stored plaintext in: " + file); - } - } - } - } - - private static class DbusReceiveMessageHandler extends ReceiveMessageHandler { - - final DBusConnection conn; - - DbusReceiveMessageHandler(Manager m, DBusConnection conn) { - super(m); - this.conn = conn; - } - - @Override - public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { - super.handleMessage(envelope, content, exception); - - JsonDbusReceiveMessageHandler.sendReceivedMessageToDbus(envelope, content, conn, m); - } - } - - private static class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler { - - final Manager m; - final ObjectMapper jsonProcessor; - - JsonReceiveMessageHandler(Manager m) { - this.m = m; - this.jsonProcessor = new ObjectMapper(); - jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect - jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES); - jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); - } - - @Override - public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { - ObjectNode result = jsonProcessor.createObjectNode(); - if (exception != null) { - result.putPOJO("error", new JsonError(exception)); - } - if (envelope != null) { - result.putPOJO("envelope", new JsonMessageEnvelope(envelope, content)); - } - try { - jsonProcessor.writeValue(System.out, result); - System.out.println(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - private static class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { - - final DBusConnection conn; - - JsonDbusReceiveMessageHandler(Manager m, DBusConnection conn) { - super(m); - this.conn = conn; - } - - private static void sendReceivedMessageToDbus(SignalServiceEnvelope envelope, SignalServiceContent content, DBusConnection conn, Manager m) { - if (envelope.isReceipt()) { - try { - conn.sendSignal(new Signal.ReceiptReceived( - SIGNAL_OBJECTPATH, - envelope.getTimestamp(), - envelope.getSource() - )); - } catch (DBusException e) { - e.printStackTrace(); - } - } else if (content != null && content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); - - if (!message.isEndSession() && - !(message.getGroupInfo().isPresent() && - message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) { - List attachments = new ArrayList<>(); - if (message.getAttachments().isPresent()) { - for (SignalServiceAttachment attachment : message.getAttachments().get()) { - if (attachment.isPointer()) { - attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath()); - } - } - } - - try { - conn.sendSignal(new Signal.MessageReceived( - SIGNAL_OBJECTPATH, - message.getTimestamp(), - envelope.getSource(), - message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0], - message.getBody().isPresent() ? message.getBody().get() : "", - attachments)); - } catch (DBusException e) { - e.printStackTrace(); - } - } - } - } - - @Override - public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { - super.handleMessage(envelope, content, exception); - - sendReceivedMessageToDbus(envelope, content, conn, m); - } - } } diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java new file mode 100644 index 00000000..5c578536 --- /dev/null +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -0,0 +1,279 @@ +package org.asamk.signal; + +import org.asamk.signal.manager.Manager; +import org.asamk.signal.storage.contacts.ContactInfo; +import org.asamk.signal.storage.groups.GroupInfo; +import org.asamk.signal.util.DateUtils; +import org.asamk.signal.util.Util; +import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.calls.*; +import org.whispersystems.signalservice.api.messages.multidevice.*; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.internal.util.Base64; + +import java.io.File; +import java.util.List; + +class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { + + final Manager m; + + ReceiveMessageHandler(Manager m) { + this.m = m; + } + + @Override + public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { + SignalServiceAddress source = envelope.getSourceAddress(); + ContactInfo sourceContact = m.getContact(source.getNumber()); + System.out.println(String.format("Envelope from: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + source.getNumber(), envelope.getSourceDevice())); + if (source.getRelay().isPresent()) { + System.out.println("Relayed by: " + source.getRelay().get()); + } + System.out.println("Timestamp: " + DateUtils.formatTimestamp(envelope.getTimestamp())); + if (envelope.isUnidentifiedSender()) { + System.out.println("Sent by unidentified/sealed sender"); + } + + if (envelope.isReceipt()) { + System.out.println("Got receipt."); + } else if (envelope.isSignalMessage() | envelope.isPreKeySignalMessage()) { + if (exception != null) { + if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) { + org.whispersystems.libsignal.UntrustedIdentityException e = (org.whispersystems.libsignal.UntrustedIdentityException) exception; + System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message."); + System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getName() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getName() + "' to mark it as trusted"); + System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getName() + "' to trust it without verification"); + } else { + System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")"); + } + } + if (content == null) { + System.out.println("Failed to decrypt message."); + } else { + if (content.getDataMessage().isPresent()) { + SignalServiceDataMessage message = content.getDataMessage().get(); + handleSignalServiceDataMessage(message); + } + if (content.getSyncMessage().isPresent()) { + System.out.println("Received a sync message"); + SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); + + if (syncMessage.getContacts().isPresent()) { + final ContactsMessage contactsMessage = syncMessage.getContacts().get(); + if (contactsMessage.isComplete()) { + System.out.println("Received complete sync contacts"); + } else { + System.out.println("Received sync contacts"); + } + printAttachment(contactsMessage.getContactsStream()); + } + if (syncMessage.getGroups().isPresent()) { + System.out.println("Received sync groups"); + printAttachment(syncMessage.getGroups().get()); + } + if (syncMessage.getRead().isPresent()) { + System.out.println("Received sync read messages list"); + for (ReadMessage rm : syncMessage.getRead().get()) { + ContactInfo fromContact = m.getContact(rm.getSender()); + System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender() + " Message timestamp: " + DateUtils.formatTimestamp(rm.getTimestamp())); + } + } + if (syncMessage.getRequest().isPresent()) { + System.out.println("Received sync request"); + if (syncMessage.getRequest().get().isContactsRequest()) { + System.out.println(" - contacts request"); + } + if (syncMessage.getRequest().get().isGroupsRequest()) { + System.out.println(" - groups request"); + } + } + if (syncMessage.getSent().isPresent()) { + System.out.println("Received sync sent message"); + final SentTranscriptMessage sentTranscriptMessage = syncMessage.getSent().get(); + String to; + if (sentTranscriptMessage.getDestination().isPresent()) { + String dest = sentTranscriptMessage.getDestination().get(); + ContactInfo destContact = m.getContact(dest); + to = (destContact == null ? "" : "“" + destContact.name + "” ") + dest; + } else { + to = "Unknown"; + } + System.out.println("To: " + to + " , Message timestamp: " + DateUtils.formatTimestamp(sentTranscriptMessage.getTimestamp())); + if (sentTranscriptMessage.getExpirationStartTimestamp() > 0) { + System.out.println("Expiration started at: " + DateUtils.formatTimestamp(sentTranscriptMessage.getExpirationStartTimestamp())); + } + SignalServiceDataMessage message = sentTranscriptMessage.getMessage(); + handleSignalServiceDataMessage(message); + } + if (syncMessage.getBlockedList().isPresent()) { + System.out.println("Received sync message with block list"); + System.out.println("Blocked numbers:"); + final BlockedListMessage blockedList = syncMessage.getBlockedList().get(); + for (String number : blockedList.getNumbers()) { + System.out.println(" - " + number); + } + } + if (syncMessage.getVerified().isPresent()) { + System.out.println("Received sync message with verified identities:"); + final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); + System.out.println(" - " + verifiedMessage.getDestination() + ": " + verifiedMessage.getVerified()); + String safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey())); + System.out.println(" " + safetyNumber); + } + if (syncMessage.getConfiguration().isPresent()) { + System.out.println("Received sync message with configuration:"); + final ConfigurationMessage configurationMessage = syncMessage.getConfiguration().get(); + if (configurationMessage.getReadReceipts().isPresent()) { + System.out.println(" - Read receipts: " + (configurationMessage.getReadReceipts().get() ? "enabled" : "disabled")); + } + } + } + if (content.getCallMessage().isPresent()) { + System.out.println("Received a call message"); + SignalServiceCallMessage callMessage = content.getCallMessage().get(); + if (callMessage.getAnswerMessage().isPresent()) { + AnswerMessage answerMessage = callMessage.getAnswerMessage().get(); + System.out.println("Answer message: " + answerMessage.getId() + ": " + answerMessage.getDescription()); + } + if (callMessage.getBusyMessage().isPresent()) { + BusyMessage busyMessage = callMessage.getBusyMessage().get(); + System.out.println("Busy message: " + busyMessage.getId()); + } + if (callMessage.getHangupMessage().isPresent()) { + HangupMessage hangupMessage = callMessage.getHangupMessage().get(); + System.out.println("Hangup message: " + hangupMessage.getId()); + } + if (callMessage.getIceUpdateMessages().isPresent()) { + List iceUpdateMessages = callMessage.getIceUpdateMessages().get(); + for (IceUpdateMessage iceUpdateMessage : iceUpdateMessages) { + System.out.println("Ice update message: " + iceUpdateMessage.getId() + ", sdp: " + iceUpdateMessage.getSdp()); + } + } + if (callMessage.getOfferMessage().isPresent()) { + OfferMessage offerMessage = callMessage.getOfferMessage().get(); + System.out.println("Offer message: " + offerMessage.getId() + ": " + offerMessage.getDescription()); + } + } + if (content.getReceiptMessage().isPresent()) { + System.out.println("Received a receipt message"); + SignalServiceReceiptMessage receiptMessage = content.getReceiptMessage().get(); + System.out.println(" - When: " + DateUtils.formatTimestamp(receiptMessage.getWhen())); + if (receiptMessage.isDeliveryReceipt()) { + System.out.println(" - Is delivery receipt"); + } + if (receiptMessage.isReadReceipt()) { + System.out.println(" - Is read receipt"); + } + System.out.println(" - Timestamps:"); + for (long timestamp : receiptMessage.getTimestamps()) { + System.out.println(" " + DateUtils.formatTimestamp(timestamp)); + } + } + if (content.getTypingMessage().isPresent()) { + System.out.println("Received a typing message"); + SignalServiceTypingMessage typingMessage = content.getTypingMessage().get(); + System.out.println(" - Action: " + typingMessage.getAction()); + System.out.println(" - Timestamp: " + DateUtils.formatTimestamp(typingMessage.getTimestamp())); + if (typingMessage.getGroupId().isPresent()) { + GroupInfo group = m.getGroup(typingMessage.getGroupId().get()); + if (group != null) { + System.out.println(" Name: " + group.name); + } else { + System.out.println(" Name: "); + } + } + } + } + } else { + System.out.println("Unknown message received."); + } + System.out.println(); + } + + private void handleSignalServiceDataMessage(SignalServiceDataMessage message) { + System.out.println("Message timestamp: " + DateUtils.formatTimestamp(message.getTimestamp())); + + if (message.getBody().isPresent()) { + System.out.println("Body: " + message.getBody().get()); + } + if (message.getGroupInfo().isPresent()) { + SignalServiceGroup groupInfo = message.getGroupInfo().get(); + System.out.println("Group info:"); + System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId())); + if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) { + System.out.println(" Name: " + groupInfo.getName().get()); + } else { + GroupInfo group = m.getGroup(groupInfo.getGroupId()); + if (group != null) { + System.out.println(" Name: " + group.name); + } else { + System.out.println(" Name: "); + } + } + System.out.println(" Type: " + groupInfo.getType()); + if (groupInfo.getMembers().isPresent()) { + for (String member : groupInfo.getMembers().get()) { + System.out.println(" Member: " + member); + } + } + if (groupInfo.getAvatar().isPresent()) { + System.out.println(" Avatar:"); + printAttachment(groupInfo.getAvatar().get()); + } + } + if (message.isEndSession()) { + System.out.println("Is end session"); + } + if (message.isExpirationUpdate()) { + System.out.println("Is Expiration update: " + message.isExpirationUpdate()); + } + if (message.getExpiresInSeconds() > 0) { + System.out.println("Expires in: " + message.getExpiresInSeconds() + " seconds"); + } + if (message.getProfileKey().isPresent()) { + System.out.println("Profile key update, key length:" + message.getProfileKey().get().length); + } + + if (message.getQuote().isPresent()) { + SignalServiceDataMessage.Quote quote = message.getQuote().get(); + System.out.println("Quote: (" + quote.getId() + ")"); + System.out.println(" Author: " + quote.getAuthor().getNumber()); + System.out.println(" Text: " + quote.getText()); + if (quote.getAttachments().size() > 0) { + System.out.println(" Attachments: "); + for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : quote.getAttachments()) { + System.out.println(" Filename: " + attachment.getFileName()); + System.out.println(" Type: " + attachment.getContentType()); + System.out.println(" Thumbnail:"); + if (attachment.getThumbnail() != null) { + printAttachment(attachment.getThumbnail()); + } + } + } + } + + if (message.getAttachments().isPresent()) { + System.out.println("Attachments: "); + for (SignalServiceAttachment attachment : message.getAttachments().get()) { + printAttachment(attachment); + } + } + } + + private void printAttachment(SignalServiceAttachment attachment) { + System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")"); + if (attachment.isPointer()) { + final SignalServiceAttachmentPointer pointer = attachment.asPointer(); + System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length); + System.out.println(" Filename: " + (pointer.getFileName().isPresent() ? pointer.getFileName().get() : "-")); + System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : "")); + System.out.println(" Voice note: " + (pointer.getVoiceNote() ? "yes" : "no")); + System.out.println(" Dimensions: " + pointer.getWidth() + "x" + pointer.getHeight()); + File file = m.getAttachmentFile(pointer.getId()); + if (file.exists()) { + System.out.println(" Stored plaintext in: " + file); + } + } + } +} diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java new file mode 100644 index 00000000..99fc409a --- /dev/null +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -0,0 +1,62 @@ +package org.asamk.signal.util; + +import org.asamk.signal.GroupIdFormatException; +import org.asamk.signal.GroupNotFoundException; +import org.asamk.signal.NotAGroupMemberException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; + +import java.io.IOException; + +public class ErrorUtils { + + private ErrorUtils() { + } + + public static void handleAssertionError(AssertionError e) { + System.err.println("Failed to send/receive message (Assertion): " + e.getMessage()); + e.printStackTrace(); + System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README"); + } + + public static void handleEncapsulatedExceptions(EncapsulatedExceptions e) { + System.err.println("Failed to send (some) messages:"); + for (NetworkFailureException n : e.getNetworkExceptions()) { + System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage()); + } + for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) { + System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage()); + } + for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) { + System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage()); + } + } + + public static void handleIOException(IOException e) { + System.err.println("Failed to send message: " + e.getMessage()); + } + + public static void handleGroupNotFoundException(GroupNotFoundException e) { + System.err.println("Failed to send to group: " + e.getMessage()); + System.err.println("Aborting sending."); + } + + public static void handleNotAGroupMemberException(NotAGroupMemberException e) { + System.err.println("Failed to send to group: " + e.getMessage()); + System.err.println("Update the group on another device to readd the user to this group."); + System.err.println("Aborting sending."); + } + + public static void handleDBusExecutionException(DBusExecutionException e) { + System.err.println("Cannot connect to dbus: " + e.getMessage()); + System.err.println("Aborting."); + } + + public static void handleGroupIdFormatException(GroupIdFormatException e) { + System.err.println(e.getMessage()); + System.err.println("Aborting sending."); + } +} diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 5a1dcdda..e7a68668 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -1,7 +1,10 @@ package org.asamk.signal.util; import com.fasterxml.jackson.databind.JsonNode; +import org.asamk.signal.GroupIdFormatException; +import org.whispersystems.signalservice.internal.util.Base64; +import java.io.IOException; import java.io.InvalidObjectException; public class Util { @@ -39,4 +42,12 @@ public class Util { return node; } + + public static byte[] decodeGroupId(String groupId) throws GroupIdFormatException { + try { + return Base64.decode(groupId); + } catch (IOException e) { + throw new GroupIdFormatException(groupId, e); + } + } } From 2ab70edc68eddbd8e20a0956ad0fec0982e45323 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 19 Nov 2018 23:00:32 +0100 Subject: [PATCH 0257/2005] Fix minor inspection issues --- .../org/asamk/signal/storage/groups/JsonGroupStore.java | 3 +-- .../signal/storage/protocol/JsonIdentityKeyStore.java | 8 ++++---- .../asamk/signal/storage/protocol/JsonPreKeyStore.java | 4 ++-- .../asamk/signal/storage/protocol/JsonSessionStore.java | 4 ++-- .../signal/storage/protocol/JsonSignedPreKeyStore.java | 4 ++-- src/main/java/org/asamk/signal/util/Hex.java | 4 ++-- 6 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index e8e97309..6a3cdedb 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -30,8 +30,7 @@ public class JsonGroupStore { } public GroupInfo getGroup(byte[] groupId) { - GroupInfo g = groups.get(Base64.encodeBytes(groupId)); - return g; + return groups.get(Base64.encodeBytes(groupId)); } public List getGroups() { diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index e086b929..a343ad4e 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -94,7 +94,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { @Override public IdentityKey getIdentity(SignalProtocolAddress address) { List identities = trustedKeys.get(address.getName()); - if (identities == null) { + if (identities == null || identities.size() == 0) { return null; } @@ -102,7 +102,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { Identity maxIdentity = null; for (Identity id : identities) { final long time = id.getDateAdded().getTime(); - if (maxDate <= time) { + if (maxIdentity == null || maxDate <= time) { maxDate = time; maxIdentity = id; } @@ -123,7 +123,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { public static class JsonIdentityKeyStoreDeserializer extends JsonDeserializer { @Override - public JsonIdentityKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + public JsonIdentityKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); try { @@ -157,7 +157,7 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { public static class JsonIdentityKeyStoreSerializer extends JsonSerializer { @Override - public void serialize(JsonIdentityKeyStore jsonIdentityKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { + public void serialize(JsonIdentityKeyStore jsonIdentityKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { json.writeStartObject(); json.writeNumberField("registrationId", jsonIdentityKeyStore.getLocalRegistrationId()); json.writeStringField("identityKey", Base64.encodeBytes(jsonIdentityKeyStore.getIdentityKeyPair().serialize())); diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java index 184c084b..3d4e21a3 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java @@ -56,7 +56,7 @@ class JsonPreKeyStore implements PreKeyStore { public static class JsonPreKeyStoreDeserializer extends JsonDeserializer { @Override - public JsonPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + public JsonPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); Map preKeyMap = new HashMap<>(); @@ -82,7 +82,7 @@ class JsonPreKeyStore implements PreKeyStore { public static class JsonPreKeyStoreSerializer extends JsonSerializer { @Override - public void serialize(JsonPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { + public void serialize(JsonPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { json.writeStartArray(); for (Map.Entry preKey : jsonPreKeyStore.store.entrySet()) { json.writeStartObject(); diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index bf6891c8..87007d35 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -78,7 +78,7 @@ class JsonSessionStore implements SessionStore { public static class JsonSessionStoreDeserializer extends JsonDeserializer { @Override - public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + public JsonSessionStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); Map sessionMap = new HashMap<>(); @@ -104,7 +104,7 @@ class JsonSessionStore implements SessionStore { public static class JsonPreKeyStoreSerializer extends JsonSerializer { @Override - public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { + public void serialize(JsonSessionStore jsonSessionStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { json.writeStartArray(); for (Map.Entry preKey : jsonSessionStore.sessions.entrySet()) { json.writeStartObject(); diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java index a8c400ce..defd7f93 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java @@ -73,7 +73,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { public static class JsonSignedPreKeyStoreDeserializer extends JsonDeserializer { @Override - public JsonSignedPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException { + public JsonSignedPreKeyStore deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); Map preKeyMap = new HashMap<>(); @@ -99,7 +99,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { public static class JsonSignedPreKeyStoreSerializer extends JsonSerializer { @Override - public void serialize(JsonSignedPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException, JsonProcessingException { + public void serialize(JsonSignedPreKeyStore jsonPreKeyStore, JsonGenerator json, SerializerProvider serializerProvider) throws IOException { json.writeStartArray(); for (Map.Entry signedPreKey : jsonPreKeyStore.store.entrySet()) { json.writeStartObject(); diff --git a/src/main/java/org/asamk/signal/util/Hex.java b/src/main/java/org/asamk/signal/util/Hex.java index 9f885791..95b2d26f 100644 --- a/src/main/java/org/asamk/signal/util/Hex.java +++ b/src/main/java/org/asamk/signal/util/Hex.java @@ -11,8 +11,8 @@ public class Hex { public static String toStringCondensed(byte[] bytes) { StringBuffer buf = new StringBuffer(); - for (int i = 0; i < bytes.length; i++) { - appendHexChar(buf, bytes[i]); + for (final byte aByte : bytes) { + appendHexChar(buf, aByte); } return buf.toString(); } From f60a10eb6e40921c32045c1e38843d6b87f3d274 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 20 Nov 2018 23:19:39 +0100 Subject: [PATCH 0258/2005] Split commands into separate classes --- src/main/java/org/asamk/Signal.java | 2 + .../java/org/asamk/signal/DbusConfig.java | 7 + .../signal/DbusReceiveMessageHandler.java | 4 +- .../signal/JsonDbusReceiveMessageHandler.java | 4 +- .../signal/JsonReceiveMessageHandler.java | 4 +- src/main/java/org/asamk/signal/Main.java | 783 ++---------------- .../asamk/signal/ReceiveMessageHandler.java | 4 +- .../signal/commands/AddDeviceCommand.java | 43 + .../org/asamk/signal/commands/Command.java | 8 + .../org/asamk/signal/commands/Commands.java | 38 + .../asamk/signal/commands/DaemonCommand.java | 76 ++ .../asamk/signal/commands/DbusCommand.java | 9 + .../signal/commands/ExtendedDbusCommand.java | 10 + .../asamk/signal/commands/LinkCommand.java | 50 ++ .../signal/commands/ListDevicesCommand.java | 38 + .../signal/commands/ListGroupsCommand.java | 46 + .../commands/ListIdentitiesCommand.java | 47 ++ .../asamk/signal/commands/LocalCommand.java | 9 + .../signal/commands/QuitGroupCommand.java | 55 ++ .../asamk/signal/commands/ReceiveCommand.java | 110 +++ .../signal/commands/RegisterCommand.java | 29 + .../signal/commands/RemoveDeviceCommand.java | 34 + .../signal/commands/RemovePinCommand.java | 30 + .../asamk/signal/commands/SendCommand.java | 124 +++ .../asamk/signal/commands/SetPinCommand.java | 33 + .../asamk/signal/commands/TrustCommand.java | 74 ++ .../signal/commands/UnregisterCommand.java | 30 + .../signal/commands/UpdateAccountCommand.java | 30 + .../signal/commands/UpdateGroupCommand.java | 88 ++ .../asamk/signal/commands/VerifyCommand.java | 44 + 30 files changed, 1121 insertions(+), 742 deletions(-) create mode 100644 src/main/java/org/asamk/signal/DbusConfig.java create mode 100644 src/main/java/org/asamk/signal/commands/AddDeviceCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/Command.java create mode 100644 src/main/java/org/asamk/signal/commands/Commands.java create mode 100644 src/main/java/org/asamk/signal/commands/DaemonCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/DbusCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/LinkCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/ListDevicesCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/ListGroupsCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/LocalCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/QuitGroupCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/ReceiveCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/RegisterCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/RemovePinCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/SendCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/SetPinCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/TrustCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/UnregisterCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/VerifyCommand.java diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 7c311813..6fb59303 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -32,6 +32,8 @@ public interface Signal extends DBusInterface { byte[] updateGroup(byte[] groupId, String name, List members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException; + boolean isRegistered(); + class MessageReceived extends DBusSignal { private long timestamp; diff --git a/src/main/java/org/asamk/signal/DbusConfig.java b/src/main/java/org/asamk/signal/DbusConfig.java new file mode 100644 index 00000000..c0d23175 --- /dev/null +++ b/src/main/java/org/asamk/signal/DbusConfig.java @@ -0,0 +1,7 @@ +package org.asamk.signal; + +public class DbusConfig { + + public static final String SIGNAL_BUSNAME = "org.asamk.Signal"; + public static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal"; +} diff --git a/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java index 2ea51e2e..cebabc18 100644 --- a/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/DbusReceiveMessageHandler.java @@ -5,12 +5,12 @@ import org.freedesktop.dbus.DBusConnection; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -class DbusReceiveMessageHandler extends ReceiveMessageHandler { +public class DbusReceiveMessageHandler extends ReceiveMessageHandler { private final DBusConnection conn; private final String objectPath; - DbusReceiveMessageHandler(Manager m, DBusConnection conn, final String objectPath) { + public DbusReceiveMessageHandler(Manager m, DBusConnection conn, final String objectPath) { super(m); this.conn = conn; this.objectPath = objectPath; diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 533a1aed..c0977c0f 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -9,13 +9,13 @@ import org.whispersystems.signalservice.api.messages.*; import java.util.ArrayList; import java.util.List; -class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { +public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { private final DBusConnection conn; private final String objectPath; - JsonDbusReceiveMessageHandler(Manager m, DBusConnection conn, final String objectPath) { + public JsonDbusReceiveMessageHandler(Manager m, DBusConnection conn, final String objectPath) { super(m); this.conn = conn; this.objectPath = objectPath; diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index 7ae5d45d..cbfe72bd 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -13,12 +13,12 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import java.io.IOException; -class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler { +public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler { final Manager m; private final ObjectMapper jsonProcessor; - JsonReceiveMessageHandler(Manager m) { + public JsonReceiveMessageHandler(Manager m) { this.m = m; this.jsonProcessor = new ObjectMapper(); jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // disable autodetect diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 604c51ee..a0820f24 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -21,46 +21,19 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; import org.apache.http.util.TextUtils; import org.asamk.Signal; +import org.asamk.signal.commands.*; import org.asamk.signal.manager.BaseConfig; import org.asamk.signal.manager.Manager; -import org.asamk.signal.storage.groups.GroupInfo; -import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; -import org.asamk.signal.util.DateUtils; -import org.asamk.signal.util.Hex; -import org.asamk.signal.util.IOUtils; -import org.asamk.signal.util.Util; import org.freedesktop.dbus.DBusConnection; -import org.freedesktop.dbus.DBusSigHandler; import org.freedesktop.dbus.exceptions.DBusException; -import org.freedesktop.dbus.exceptions.DBusExecutionException; -import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; -import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; -import org.whispersystems.signalservice.internal.push.LockedException; -import org.whispersystems.signalservice.internal.util.Base64; import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.Charset; import java.security.Security; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import static org.asamk.signal.util.ErrorUtils.*; public class Main { - private static final String SIGNAL_BUSNAME = "org.asamk.Signal"; - private static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal"; - public static void main(String[] args) { // Workaround for BKS truststore Security.insertProviderAt(new org.bouncycastle.jce.provider.BouncyCastleProvider(), 1); @@ -91,7 +64,7 @@ public class Main { } dBusConn = DBusConnection.getConnection(busType); ts = dBusConn.getRemoteObject( - SIGNAL_BUSNAME, SIGNAL_OBJECTPATH, + DbusConfig.SIGNAL_BUSNAME, DbusConfig.SIGNAL_OBJECTPATH, Signal.class); } catch (UnsatisfiedLinkError e) { System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); @@ -125,567 +98,30 @@ public class Main { } } - switch (ns.getString("command")) { - case "register": - if (dBusConn != null) { - System.err.println("register is not yet implemented via dbus"); - return 1; - } - try { - m.register(ns.getBoolean("voice")); - } catch (IOException e) { - System.err.println("Request verify error: " + e.getMessage()); - return 3; - } - break; - case "unregister": - if (dBusConn != null) { - System.err.println("unregister is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - try { - m.unregister(); - } catch (IOException e) { - System.err.println("Unregister error: " + e.getMessage()); - return 3; - } - break; - case "updateAccount": - if (dBusConn != null) { - System.err.println("updateAccount is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - try { - m.updateAccountAttributes(); - } catch (IOException e) { - System.err.println("UpdateAccount error: " + e.getMessage()); - return 3; - } - break; - case "setPin": - if (dBusConn != null) { - System.err.println("setPin is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - try { - String registrationLockPin = ns.getString("registrationLockPin"); - m.setRegistrationLockPin(Optional.of(registrationLockPin)); - } catch (IOException e) { - System.err.println("Set pin error: " + e.getMessage()); - return 3; - } - break; - case "removePin": - if (dBusConn != null) { - System.err.println("removePin is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - try { - m.setRegistrationLockPin(Optional.absent()); - } catch (IOException e) { - System.err.println("Remove pin error: " + e.getMessage()); - return 3; - } - break; - case "verify": - if (dBusConn != null) { - System.err.println("verify is not yet implemented via dbus"); - return 1; - } - if (!m.userHasKeys()) { - System.err.println("User has no keys, first call register."); - return 1; - } - if (m.isRegistered()) { - System.err.println("User registration is already verified"); - return 1; - } - try { - String verificationCode = ns.getString("verificationCode"); - String pin = ns.getString("pin"); - m.verifyAccount(verificationCode, pin); - } catch (LockedException e) { - System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: " + (e.getTimeRemaining() / 1000 / 60 / 60)); - System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN"); - return 3; - } catch (IOException e) { - System.err.println("Verify error: " + e.getMessage()); - return 3; - } - break; - case "link": - if (dBusConn != null) { - System.err.println("link is not yet implemented via dbus"); - return 1; - } + String commandKey = ns.getString("command"); + final Map commands = Commands.getCommands(); + if (commands.containsKey(commandKey)) { + Command command = commands.get(commandKey); - String deviceName = ns.getString("name"); - if (deviceName == null) { - deviceName = "cli"; - } - try { - System.out.println(m.getDeviceLinkUri()); - m.finishDeviceLink(deviceName); - System.out.println("Associated with: " + m.getUsername()); - } catch (TimeoutException e) { - System.err.println("Link request timed out, please try again."); - return 3; - } catch (IOException e) { - System.err.println("Link request error: " + e.getMessage()); - return 3; - } catch (AssertionError e) { - handleAssertionError(e); - return 1; - } catch (InvalidKeyException e) { - e.printStackTrace(); - return 2; - } catch (UserAlreadyExists e) { - System.err.println("The user " + e.getUsername() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again."); - return 1; - } - break; - case "addDevice": - if (dBusConn != null) { - System.err.println("link is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - try { - m.addDeviceLink(new URI(ns.getString("uri"))); - } catch (IOException e) { - e.printStackTrace(); - return 3; - } catch (InvalidKeyException | URISyntaxException e) { - e.printStackTrace(); - return 2; - } catch (AssertionError e) { - handleAssertionError(e); - return 1; - } - break; - case "listDevices": - if (dBusConn != null) { - System.err.println("listDevices is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - try { - List devices = m.getLinkedDevices(); - for (DeviceInfo d : devices) { - System.out.println("Device " + d.getId() + (d.getId() == m.getDeviceId() ? " (this device)" : "") + ":"); - System.out.println(" Name: " + d.getName()); - System.out.println(" Created: " + DateUtils.formatTimestamp(d.getCreated())); - System.out.println(" Last seen: " + DateUtils.formatTimestamp(d.getLastSeen())); - } - } catch (IOException e) { - e.printStackTrace(); - return 3; - } - break; - case "removeDevice": - if (dBusConn != null) { - System.err.println("removeDevice is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - try { - int deviceId = ns.getInt("deviceId"); - m.removeLinkedDevices(deviceId); - } catch (IOException e) { - e.printStackTrace(); - return 3; - } - break; - case "send": - if (dBusConn == null && !m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - - if (ns.getBoolean("endsession")) { - if (ns.getList("recipient") == null) { - System.err.println("No recipients given"); - System.err.println("Aborting sending."); - return 1; - } - try { - ts.sendEndSessionMessage(ns.getList("recipient")); - } catch (IOException e) { - handleIOException(e); - return 3; - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - return 3; - } catch (AssertionError e) { - handleAssertionError(e); - return 1; - } catch (DBusExecutionException e) { - handleDBusExecutionException(e); - return 1; - } + if (dBusConn != null) { + if (command instanceof ExtendedDbusCommand) { + return ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn); + } else if (command instanceof DbusCommand) { + return ((DbusCommand) command).handleCommand(ns, ts); } else { - String messageText = ns.getString("message"); - if (messageText == null) { - try { - messageText = IOUtils.readAll(System.in, Charset.defaultCharset()); - } catch (IOException e) { - System.err.println("Failed to read message from stdin: " + e.getMessage()); - System.err.println("Aborting sending."); - return 1; - } - } - - try { - List attachments = ns.getList("attachment"); - if (attachments == null) { - attachments = new ArrayList<>(); - } - if (ns.getString("group") != null) { - byte[] groupId = Util.decodeGroupId(ns.getString("group")); - ts.sendGroupMessage(messageText, attachments, groupId); - } else { - ts.sendMessage(messageText, attachments, ns.getList("recipient")); - } - } catch (IOException e) { - handleIOException(e); - return 3; - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - return 3; - } catch (AssertionError e) { - handleAssertionError(e); - return 1; - } catch (GroupNotFoundException e) { - handleGroupNotFoundException(e); - return 1; - } catch (NotAGroupMemberException e) { - handleNotAGroupMemberException(e); - return 1; - } catch (AttachmentInvalidException e) { - System.err.println("Failed to add attachment: " + e.getMessage()); - System.err.println("Aborting sending."); - return 1; - } catch (DBusExecutionException e) { - handleDBusExecutionException(e); - return 1; - } catch (GroupIdFormatException e) { - handleGroupIdFormatException(e); - return 1; - } - } - - break; - case "receive": - if (dBusConn != null) { - try { - dBusConn.addSigHandler(Signal.MessageReceived.class, new DBusSigHandler() { - @Override - public void handle(Signal.MessageReceived s) { - System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n", - s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()), s.getMessage())); - if (s.getGroupId().length > 0) { - System.out.println("Group info:"); - System.out.println(" Id: " + Base64.encodeBytes(s.getGroupId())); - } - if (s.getAttachments().size() > 0) { - System.out.println("Attachments: "); - for (String attachment : s.getAttachments()) { - System.out.println("- Stored plaintext in: " + attachment); - } - } - System.out.println(); - } - }); - dBusConn.addSigHandler(Signal.ReceiptReceived.class, new DBusSigHandler() { - @Override - public void handle(Signal.ReceiptReceived s) { - System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", - s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()))); - } - }); - } catch (UnsatisfiedLinkError e) { - System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); - return 1; - } catch (DBusException e) { - e.printStackTrace(); - return 1; - } - while (true) { - try { - Thread.sleep(10000); - } catch (InterruptedException e) { - return 0; - } - } - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); + System.err.println(commandKey + " is not yet implemented via dbus"); return 1; } - double timeout = 5; - if (ns.getDouble("timeout") != null) { - timeout = ns.getDouble("timeout"); - } - boolean returnOnTimeout = true; - if (timeout < 0) { - returnOnTimeout = false; - timeout = 3600; - } - boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); - try { - final Manager.ReceiveMessageHandler handler = ns.getBoolean("json") ? new JsonReceiveMessageHandler(m) : new ReceiveMessageHandler(m); - m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, ignoreAttachments, handler); - } catch (IOException e) { - System.err.println("Error while receiving messages: " + e.getMessage()); - return 3; - } catch (AssertionError e) { - handleAssertionError(e); - return 1; - } - break; - case "quitGroup": - if (dBusConn != null) { - System.err.println("quitGroup is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - - try { - m.sendQuitGroupMessage(Util.decodeGroupId(ns.getString("group"))); - } catch (IOException e) { - handleIOException(e); - return 3; - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - return 3; - } catch (AssertionError e) { - handleAssertionError(e); - return 1; - } catch (GroupNotFoundException e) { - handleGroupNotFoundException(e); - return 1; - } catch (NotAGroupMemberException e) { - handleNotAGroupMemberException(e); - return 1; - } catch (GroupIdFormatException e) { - handleGroupIdFormatException(e); - return 1; - } - - break; - case "updateGroup": - if (dBusConn == null && !m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - - try { - byte[] groupId = null; - if (ns.getString("group") != null) { - groupId = Util.decodeGroupId(ns.getString("group")); - } - if (groupId == null) { - groupId = new byte[0]; - } - String groupName = ns.getString("name"); - if (groupName == null) { - groupName = ""; - } - List groupMembers = ns.getList("member"); - if (groupMembers == null) { - groupMembers = new ArrayList<>(); - } - String groupAvatar = ns.getString("avatar"); - if (groupAvatar == null) { - groupAvatar = ""; - } - byte[] newGroupId = ts.updateGroup(groupId, groupName, groupMembers, groupAvatar); - if (groupId.length != newGroupId.length) { - System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …"); - } - } catch (IOException e) { - handleIOException(e); - return 3; - } catch (AttachmentInvalidException e) { - System.err.println("Failed to add avatar attachment for group\": " + e.getMessage()); - System.err.println("Aborting sending."); - return 1; - } catch (GroupNotFoundException e) { - handleGroupNotFoundException(e); - return 1; - } catch (NotAGroupMemberException e) { - handleNotAGroupMemberException(e); - return 1; - } catch (EncapsulatedExceptions e) { - handleEncapsulatedExceptions(e); - return 3; - } catch (GroupIdFormatException e) { - handleGroupIdFormatException(e); - return 1; - } - - break; - case "listGroups": - if (dBusConn != null) { - System.err.println("listGroups is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - - List groups = m.getGroups(); - boolean detailed = ns.getBoolean("detailed"); - - for (GroupInfo group : groups) { - printGroup(group, detailed); - } - break; - case "listIdentities": - if (dBusConn != null) { - System.err.println("listIdentities is not yet implemented via dbus"); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - if (ns.get("number") == null) { - for (Map.Entry> keys : m.getIdentities().entrySet()) { - for (JsonIdentityKeyStore.Identity id : keys.getValue()) { - printIdentityFingerprint(m, keys.getKey(), id); - } - } + } else { + if (command instanceof LocalCommand) { + return ((LocalCommand) command).handleCommand(ns, m); + } else if (command instanceof DbusCommand) { + return ((DbusCommand) command).handleCommand(ns, ts); } else { - String number = ns.getString("number"); - for (JsonIdentityKeyStore.Identity id : m.getIdentities(number)) { - printIdentityFingerprint(m, number, id); - } - } - break; - case "trust": - if (dBusConn != null) { - System.err.println("trust is not yet implemented via dbus"); + System.err.println(commandKey + " is only works via dbus"); return 1; } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - String number = ns.getString("number"); - if (ns.getBoolean("trust_all_known_keys")) { - boolean res = m.trustIdentityAllKeys(number); - if (!res) { - System.err.println("Failed to set the trust for this number, make sure the number is correct."); - return 1; - } - } else { - String fingerprint = ns.getString("verified_fingerprint"); - if (fingerprint != null) { - fingerprint = fingerprint.replaceAll(" ", ""); - if (fingerprint.length() == 66) { - byte[] fingerprintBytes; - try { - fingerprintBytes = Hex.toByteArray(fingerprint.toLowerCase(Locale.ROOT)); - } catch (Exception e) { - System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); - return 1; - } - boolean res = m.trustIdentityVerified(number, fingerprintBytes); - if (!res) { - System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); - return 1; - } - } else if (fingerprint.length() == 60) { - boolean res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint); - if (!res) { - System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); - return 1; - } - } else { - System.err.println("Fingerprint has invalid format, either specify the old hex fingerprint or the new safety number"); - return 1; - } - } else { - System.err.println("You need to specify the fingerprint you have verified with -v FINGERPRINT"); - return 1; - } - } - break; - case "daemon": - if (dBusConn != null) { - System.err.println("Stop it."); - return 1; - } - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - DBusConnection conn = null; - try { - try { - int busType; - if (ns.getBoolean("system")) { - busType = DBusConnection.SYSTEM; - } else { - busType = DBusConnection.SESSION; - } - conn = DBusConnection.getConnection(busType); - conn.exportObject(SIGNAL_OBJECTPATH, m); - conn.requestBusName(SIGNAL_BUSNAME); - } catch (UnsatisfiedLinkError e) { - System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); - return 1; - } catch (DBusException e) { - e.printStackTrace(); - return 2; - } - ignoreAttachments = ns.getBoolean("ignore_attachments"); - try { - m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, ns.getBoolean("json") ? new JsonDbusReceiveMessageHandler(m, conn, Main.SIGNAL_OBJECTPATH) : new DbusReceiveMessageHandler(m, conn, Main.SIGNAL_OBJECTPATH)); - } catch (IOException e) { - System.err.println("Error while receiving messages: " + e.getMessage()); - return 3; - } catch (AssertionError e) { - handleAssertionError(e); - return 1; - } - } finally { - if (conn != null) { - conn.disconnect(); - } - } - - break; + } } return 0; } finally { @@ -695,22 +131,6 @@ public class Main { } } - private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) { - String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey())); - System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, - theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits)); - } - - private static void printGroup(GroupInfo group, boolean detailed) { - if (detailed) { - System.out.println(String.format("Id: %s Name: %s Active: %s Members: %s", - Base64.encodeBytes(group.groupId), group.name, group.active, group.members)); - } else { - System.out.println(String.format("Id: %s Name: %s Active: %s", Base64.encodeBytes(group.groupId), - group.name, group.active)); - } - } - private static Namespace parseArgs(String[] args) { ArgumentParser parser = ArgumentParsers.newFor("signal-cli") .build() @@ -740,146 +160,41 @@ public class Main { .description("valid subcommands") .help("additional help"); - Subparser parserLink = subparsers.addParser("link"); - parserLink.addArgument("-n", "--name") - .help("Specify a name to describe this new device."); - - Subparser parserAddDevice = subparsers.addParser("addDevice"); - parserAddDevice.addArgument("--uri") - .required(true) - .help("Specify the uri contained in the QR code shown by the new device."); - - Subparser parserDevices = subparsers.addParser("listDevices"); - - Subparser parserRemoveDevice = subparsers.addParser("removeDevice"); - parserRemoveDevice.addArgument("-d", "--deviceId") - .type(int.class) - .required(true) - .help("Specify the device you want to remove. Use listDevices to see the deviceIds."); - - Subparser parserRegister = subparsers.addParser("register"); - parserRegister.addArgument("-v", "--voice") - .help("The verification should be done over voice, not sms.") - .action(Arguments.storeTrue()); - - Subparser parserUnregister = subparsers.addParser("unregister"); - parserUnregister.help("Unregister the current device from the signal server."); - - Subparser parserUpdateAccount = subparsers.addParser("updateAccount"); - parserUpdateAccount.help("Update the account attributes on the signal server."); - - Subparser parserSetPin = subparsers.addParser("setPin"); - parserSetPin.addArgument("registrationLockPin") - .help("The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity)"); - - Subparser parserRemovePin = subparsers.addParser("removePin"); - - Subparser parserVerify = subparsers.addParser("verify"); - parserVerify.addArgument("verificationCode") - .help("The verification code you received via sms or voice call."); - parserVerify.addArgument("-p", "--pin") - .help("The registration lock PIN, that was set by the user (Optional)"); - - Subparser parserSend = subparsers.addParser("send"); - parserSend.addArgument("-g", "--group") - .help("Specify the recipient group ID."); - parserSend.addArgument("recipient") - .help("Specify the recipients' phone number.") - .nargs("*"); - parserSend.addArgument("-m", "--message") - .help("Specify the message, if missing standard input is used."); - parserSend.addArgument("-a", "--attachment") - .nargs("*") - .help("Add file as attachment"); - parserSend.addArgument("-e", "--endsession") - .help("Clear session state and send end session message.") - .action(Arguments.storeTrue()); - - Subparser parserLeaveGroup = subparsers.addParser("quitGroup"); - parserLeaveGroup.addArgument("-g", "--group") - .required(true) - .help("Specify the recipient group ID."); - - Subparser parserUpdateGroup = subparsers.addParser("updateGroup"); - parserUpdateGroup.addArgument("-g", "--group") - .help("Specify the recipient group ID."); - parserUpdateGroup.addArgument("-n", "--name") - .help("Specify the new group name."); - parserUpdateGroup.addArgument("-a", "--avatar") - .help("Specify a new group avatar image file"); - parserUpdateGroup.addArgument("-m", "--member") - .nargs("*") - .help("Specify one or more members to add to the group"); - - Subparser parserListGroups = subparsers.addParser("listGroups"); - parserListGroups.addArgument("-d", "--detailed").action(Arguments.storeTrue()) - .help("List members of each group"); - parserListGroups.help("List group name and ids"); - - Subparser parserListIdentities = subparsers.addParser("listIdentities"); - parserListIdentities.addArgument("-n", "--number") - .help("Only show identity keys for the given phone number."); - - Subparser parserTrust = subparsers.addParser("trust"); - parserTrust.addArgument("number") - .help("Specify the phone number, for which to set the trust.") - .required(true); - MutuallyExclusiveGroup mutTrust = parserTrust.addMutuallyExclusiveGroup(); - mutTrust.addArgument("-a", "--trust-all-known-keys") - .help("Trust all known keys of this user, only use this for testing.") - .action(Arguments.storeTrue()); - mutTrust.addArgument("-v", "--verified-fingerprint") - .help("Specify the fingerprint of the key, only use this option if you have verified the fingerprint."); - - Subparser parserReceive = subparsers.addParser("receive"); - parserReceive.addArgument("-t", "--timeout") - .type(double.class) - .help("Number of seconds to wait for new messages (negative values disable timeout)"); - parserReceive.addArgument("--ignore-attachments") - .help("Don’t download attachments of received messages.") - .action(Arguments.storeTrue()); - parserReceive.addArgument("--json") - .help("Output received messages in json format, one json object per line.") - .action(Arguments.storeTrue()); - - Subparser parserDaemon = subparsers.addParser("daemon"); - parserDaemon.addArgument("--system") - .action(Arguments.storeTrue()) - .help("Use DBus system bus instead of user bus."); - parserDaemon.addArgument("--ignore-attachments") - .help("Don’t download attachments of received messages.") - .action(Arguments.storeTrue()); - parserDaemon.addArgument("--json") - .help("Output received messages in json format, one json object per line.") - .action(Arguments.storeTrue()); + final Map commands = Commands.getCommands(); + for (Map.Entry entry : commands.entrySet()) { + Subparser subparser = subparsers.addParser(entry.getKey()); + entry.getValue().attachToSubparser(subparser); + } + Namespace ns; try { - Namespace ns = parser.parseArgs(args); - if ("link".equals(ns.getString("command"))) { - if (ns.getString("username") != null) { - parser.printUsage(); - System.err.println("You cannot specify a username (phone number) when linking"); - System.exit(2); - } - } else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) { - if (ns.getString("username") == null) { - parser.printUsage(); - System.err.println("You need to specify a username (phone number)"); - System.exit(2); - } - if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) { - System.err.println("Invalid username (phone number), make sure you include the country code."); - System.exit(2); - } - } - if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) { - System.err.println("You cannot specify recipients by phone number and groups a the same time"); - System.exit(2); - } - return ns; + ns = parser.parseArgs(args); } catch (ArgumentParserException e) { parser.handleError(e); return null; } + + if ("link".equals(ns.getString("command"))) { + if (ns.getString("username") != null) { + parser.printUsage(); + System.err.println("You cannot specify a username (phone number) when linking"); + System.exit(2); + } + } else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) { + if (ns.getString("username") == null) { + parser.printUsage(); + System.err.println("You need to specify a username (phone number)"); + System.exit(2); + } + if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) { + System.err.println("Invalid username (phone number), make sure you include the country code."); + System.exit(2); + } + } + if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) { + System.err.println("You cannot specify recipients by phone number and groups a the same time"); + System.exit(2); + } + return ns; } } diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 5c578536..f8c93f52 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -14,11 +14,11 @@ import org.whispersystems.signalservice.internal.util.Base64; import java.io.File; import java.util.List; -class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { +public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final Manager m; - ReceiveMessageHandler(Manager m) { + public ReceiveMessageHandler(Manager m) { this.m = m; } diff --git a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java new file mode 100644 index 00000000..c4402696 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java @@ -0,0 +1,43 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; +import org.whispersystems.libsignal.InvalidKeyException; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; + +public class AddDeviceCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("--uri") + .required(true) + .help("Specify the uri contained in the QR code shown by the new device."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + m.addDeviceLink(new URI(ns.getString("uri"))); + return 0; + } catch (IOException e) { + e.printStackTrace(); + return 3; + } catch (InvalidKeyException | URISyntaxException e) { + e.printStackTrace(); + return 2; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/Command.java b/src/main/java/org/asamk/signal/commands/Command.java new file mode 100644 index 00000000..1e4abc19 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/Command.java @@ -0,0 +1,8 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Subparser; + +public interface Command { + + void attachToSubparser(Subparser subparser); +} diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java new file mode 100644 index 00000000..6f262fdf --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -0,0 +1,38 @@ +package org.asamk.signal.commands; + +import java.util.HashMap; +import java.util.Map; + +public class Commands { + + private static final Map commands = new HashMap<>(); + + static { + addCommand("addDevice", new AddDeviceCommand()); + addCommand("daemon", new DaemonCommand()); + addCommand("link", new LinkCommand()); + addCommand("listDevices", new ListDevicesCommand()); + addCommand("listGroups", new ListGroupsCommand()); + addCommand("listIdentities", new ListIdentitiesCommand()); + addCommand("quitGroup", new QuitGroupCommand()); + addCommand("receive", new ReceiveCommand()); + addCommand("register", new RegisterCommand()); + addCommand("removeDevice", new RemoveDeviceCommand()); + addCommand("removePin", new RemovePinCommand()); + addCommand("send", new SendCommand()); + addCommand("setPin", new SetPinCommand()); + addCommand("trust", new TrustCommand()); + addCommand("unregister", new UnregisterCommand()); + addCommand("updateAccount", new UpdateAccountCommand()); + addCommand("updateGroup", new UpdateGroupCommand()); + addCommand("verify", new VerifyCommand()); + } + + public static Map getCommands() { + return commands; + } + + private static void addCommand(String name, Command command) { + commands.put(name, command); + } +} diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java new file mode 100644 index 00000000..9b6c0d63 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -0,0 +1,76 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.DbusReceiveMessageHandler; +import org.asamk.signal.JsonDbusReceiveMessageHandler; +import org.asamk.signal.manager.Manager; +import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.exceptions.DBusException; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.asamk.signal.DbusConfig.SIGNAL_BUSNAME; +import static org.asamk.signal.DbusConfig.SIGNAL_OBJECTPATH; +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; + +public class DaemonCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("--system") + .action(Arguments.storeTrue()) + .help("Use DBus system bus instead of user bus."); + subparser.addArgument("--ignore-attachments") + .help("Don’t download attachments of received messages.") + .action(Arguments.storeTrue()); + subparser.addArgument("--json") + .help("Output received messages in json format, one json object per line.") + .action(Arguments.storeTrue()); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + DBusConnection conn = null; + try { + try { + int busType; + if (ns.getBoolean("system")) { + busType = DBusConnection.SYSTEM; + } else { + busType = DBusConnection.SESSION; + } + conn = DBusConnection.getConnection(busType); + conn.exportObject(SIGNAL_OBJECTPATH, m); + conn.requestBusName(SIGNAL_BUSNAME); + } catch (UnsatisfiedLinkError e) { + System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); + return 1; + } catch (DBusException e) { + e.printStackTrace(); + return 2; + } + boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); + try { + m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, ns.getBoolean("json") ? new JsonDbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH) : new DbusReceiveMessageHandler(m, conn, SIGNAL_OBJECTPATH)); + return 0; + } catch (IOException e) { + System.err.println("Error while receiving messages: " + e.getMessage()); + return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } + } finally { + if (conn != null) { + conn.disconnect(); + } + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/DbusCommand.java b/src/main/java/org/asamk/signal/commands/DbusCommand.java new file mode 100644 index 00000000..077600e4 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/DbusCommand.java @@ -0,0 +1,9 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import org.asamk.Signal; + +public interface DbusCommand extends Command { + + int handleCommand(Namespace ns, Signal signal); +} diff --git a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java new file mode 100644 index 00000000..df47994f --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java @@ -0,0 +1,10 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import org.asamk.Signal; +import org.freedesktop.dbus.DBusConnection; + +public interface ExtendedDbusCommand extends Command { + + int handleCommand(Namespace ns, Signal signal, DBusConnection dbusconnection); +} diff --git a/src/main/java/org/asamk/signal/commands/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java new file mode 100644 index 00000000..5c7ce403 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java @@ -0,0 +1,50 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.UserAlreadyExists; +import org.asamk.signal.manager.Manager; +import org.whispersystems.libsignal.InvalidKeyException; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; + +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; + +public class LinkCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("-n", "--name") + .help("Specify a name to describe this new device."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + String deviceName = ns.getString("name"); + if (deviceName == null) { + deviceName = "cli"; + } + try { + System.out.println(m.getDeviceLinkUri()); + m.finishDeviceLink(deviceName); + System.out.println("Associated with: " + m.getUsername()); + } catch (TimeoutException e) { + System.err.println("Link request timed out, please try again."); + return 3; + } catch (IOException e) { + System.err.println("Link request error: " + e.getMessage()); + return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } catch (InvalidKeyException e) { + e.printStackTrace(); + return 2; + } catch (UserAlreadyExists e) { + System.err.println("The user " + e.getUsername() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again."); + return 1; + } + return 0; + } +} diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java new file mode 100644 index 00000000..e30acd78 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java @@ -0,0 +1,38 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.DateUtils; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; + +import java.io.IOException; +import java.util.List; + +public class ListDevicesCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + List devices = m.getLinkedDevices(); + for (DeviceInfo d : devices) { + System.out.println("Device " + d.getId() + (d.getId() == m.getDeviceId() ? " (this device)" : "") + ":"); + System.out.println(" Name: " + d.getName()); + System.out.println(" Created: " + DateUtils.formatTimestamp(d.getCreated())); + System.out.println(" Last seen: " + DateUtils.formatTimestamp(d.getLastSeen())); + } + return 0; + } catch (IOException e) { + e.printStackTrace(); + return 3; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java new file mode 100644 index 00000000..29d136f5 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -0,0 +1,46 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.storage.groups.GroupInfo; +import org.whispersystems.signalservice.internal.util.Base64; + +import java.util.List; + +public class ListGroupsCommand implements LocalCommand { + + private static void printGroup(GroupInfo group, boolean detailed) { + if (detailed) { + System.out.println(String.format("Id: %s Name: %s Active: %s Members: %s", + Base64.encodeBytes(group.groupId), group.name, group.active, group.members)); + } else { + System.out.println(String.format("Id: %s Name: %s Active: %s", Base64.encodeBytes(group.groupId), + group.name, group.active)); + } + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("-d", "--detailed").action(Arguments.storeTrue()) + .help("List members of each group"); + subparser.help("List group name and ids"); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + List groups = m.getGroups(); + boolean detailed = ns.getBoolean("detailed"); + + for (GroupInfo group : groups) { + printGroup(group, detailed); + } + return 0; + } +} diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java new file mode 100644 index 00000000..b2f6f31c --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -0,0 +1,47 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; +import org.asamk.signal.util.Hex; +import org.asamk.signal.util.Util; + +import java.util.List; +import java.util.Map; + +public class ListIdentitiesCommand implements LocalCommand { + + private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) { + String digits = Util.formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.getIdentityKey())); + System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername, + theirId.getTrustLevel(), theirId.getDateAdded(), Hex.toStringCondensed(theirId.getFingerprint()), digits)); + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("-n", "--number") + .help("Only show identity keys for the given phone number."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + if (ns.get("number") == null) { + for (Map.Entry> keys : m.getIdentities().entrySet()) { + for (JsonIdentityKeyStore.Identity id : keys.getValue()) { + printIdentityFingerprint(m, keys.getKey(), id); + } + } + } else { + String number = ns.getString("number"); + for (JsonIdentityKeyStore.Identity id : m.getIdentities(number)) { + printIdentityFingerprint(m, number, id); + } + } + return 0; + } +} diff --git a/src/main/java/org/asamk/signal/commands/LocalCommand.java b/src/main/java/org/asamk/signal/commands/LocalCommand.java new file mode 100644 index 00000000..9f785802 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/LocalCommand.java @@ -0,0 +1,9 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import org.asamk.signal.manager.Manager; + +public interface LocalCommand extends Command { + + int handleCommand(Namespace ns, Manager m); +} diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java new file mode 100644 index 00000000..1bde1120 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -0,0 +1,55 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.GroupIdFormatException; +import org.asamk.signal.GroupNotFoundException; +import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.Util; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; + +import java.io.IOException; + +import static org.asamk.signal.util.ErrorUtils.*; + +public class QuitGroupCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("-g", "--group") + .required(true) + .help("Specify the recipient group ID."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + try { + m.sendQuitGroupMessage(Util.decodeGroupId(ns.getString("group"))); + return 0; + } catch (IOException e) { + handleIOException(e); + return 3; + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } catch (GroupNotFoundException e) { + handleGroupNotFoundException(e); + return 1; + } catch (NotAGroupMemberException e) { + handleNotAGroupMemberException(e); + return 1; + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java new file mode 100644 index 00000000..03f3d1b2 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -0,0 +1,110 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.Signal; +import org.asamk.signal.JsonReceiveMessageHandler; +import org.asamk.signal.ReceiveMessageHandler; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.DateUtils; +import org.freedesktop.dbus.DBusConnection; +import org.freedesktop.dbus.DBusSigHandler; +import org.freedesktop.dbus.exceptions.DBusException; +import org.whispersystems.signalservice.internal.util.Base64; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; + +public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("-t", "--timeout") + .type(double.class) + .help("Number of seconds to wait for new messages (negative values disable timeout)"); + subparser.addArgument("--ignore-attachments") + .help("Don’t download attachments of received messages.") + .action(Arguments.storeTrue()); + subparser.addArgument("--json") + .help("Output received messages in json format, one json object per line.") + .action(Arguments.storeTrue()); + } + + public int handleCommand(final Namespace ns, final Signal signal, DBusConnection dbusconnection) { + if (dbusconnection != null) { + try { + dbusconnection.addSigHandler(Signal.MessageReceived.class, new DBusSigHandler() { + @Override + public void handle(Signal.MessageReceived s) { + System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n", + s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()), s.getMessage())); + if (s.getGroupId().length > 0) { + System.out.println("Group info:"); + System.out.println(" Id: " + Base64.encodeBytes(s.getGroupId())); + } + if (s.getAttachments().size() > 0) { + System.out.println("Attachments: "); + for (String attachment : s.getAttachments()) { + System.out.println("- Stored plaintext in: " + attachment); + } + } + System.out.println(); + } + }); + dbusconnection.addSigHandler(Signal.ReceiptReceived.class, new DBusSigHandler() { + @Override + public void handle(Signal.ReceiptReceived s) { + System.out.print(String.format("Receipt from: %s\nTimestamp: %s\n", + s.getSender(), DateUtils.formatTimestamp(s.getTimestamp()))); + } + }); + } catch (UnsatisfiedLinkError e) { + System.err.println("Missing native library dependency for dbus service: " + e.getMessage()); + return 1; + } catch (DBusException e) { + e.printStackTrace(); + return 1; + } + while (true) { + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + return 0; + } + } + } + return 0; + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + double timeout = 5; + if (ns.getDouble("timeout") != null) { + timeout = ns.getDouble("timeout"); + } + boolean returnOnTimeout = true; + if (timeout < 0) { + returnOnTimeout = false; + timeout = 3600; + } + boolean ignoreAttachments = ns.getBoolean("ignore_attachments"); + try { + final Manager.ReceiveMessageHandler handler = ns.getBoolean("json") ? new JsonReceiveMessageHandler(m) : new ReceiveMessageHandler(m); + m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, ignoreAttachments, handler); + return 0; + } catch (IOException e) { + System.err.println("Error while receiving messages: " + e.getMessage()); + return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java new file mode 100644 index 00000000..546578b6 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -0,0 +1,29 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; + +import java.io.IOException; + +public class RegisterCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("-v", "--voice") + .help("The verification should be done over voice, not sms.") + .action(Arguments.storeTrue()); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + try { + m.register(ns.getBoolean("voice")); + return 0; + } catch (IOException e) { + System.err.println("Request verify error: " + e.getMessage()); + return 3; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java new file mode 100644 index 00000000..9f5787ae --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java @@ -0,0 +1,34 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; + +import java.io.IOException; + +public class RemoveDeviceCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("-d", "--deviceId") + .type(int.class) + .required(true) + .help("Specify the device you want to remove. Use listDevices to see the deviceIds."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + int deviceId = ns.getInt("deviceId"); + m.removeLinkedDevices(deviceId); + return 0; + } catch (IOException e) { + e.printStackTrace(); + return 3; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java new file mode 100644 index 00000000..491c26bd --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java @@ -0,0 +1,30 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; +import org.whispersystems.libsignal.util.guava.Optional; + +import java.io.IOException; + +public class RemovePinCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + m.setRegistrationLockPin(Optional.absent()); + return 0; + } catch (IOException e) { + System.err.println("Remove pin error: " + e.getMessage()); + return 3; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java new file mode 100644 index 00000000..308e564c --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -0,0 +1,124 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.Signal; +import org.asamk.signal.AttachmentInvalidException; +import org.asamk.signal.GroupIdFormatException; +import org.asamk.signal.GroupNotFoundException; +import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.util.IOUtils; +import org.asamk.signal.util.Util; +import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import static org.asamk.signal.util.ErrorUtils.*; + +public class SendCommand implements DbusCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("-g", "--group") + .help("Specify the recipient group ID."); + subparser.addArgument("recipient") + .help("Specify the recipients' phone number.") + .nargs("*"); + subparser.addArgument("-m", "--message") + .help("Specify the message, if missing standard input is used."); + subparser.addArgument("-a", "--attachment") + .nargs("*") + .help("Add file as attachment"); + subparser.addArgument("-e", "--endsession") + .help("Clear session state and send end session message.") + .action(Arguments.storeTrue()); + } + + @Override + public int handleCommand(final Namespace ns, final Signal signal) { + if (!signal.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + if (ns.getList("recipient") == null || ns.getList("recipient").size() == 0) { + System.err.println("No recipients given"); + System.err.println("Aborting sending."); + return 1; + } + + if (ns.getBoolean("endsession")) { + try { + signal.sendEndSessionMessage(ns.getList("recipient")); + return 0; + } catch (IOException e) { + handleIOException(e); + return 3; + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } catch (DBusExecutionException e) { + handleDBusExecutionException(e); + return 1; + } + } + + String messageText = ns.getString("message"); + if (messageText == null) { + try { + messageText = IOUtils.readAll(System.in, Charset.defaultCharset()); + } catch (IOException e) { + System.err.println("Failed to read message from stdin: " + e.getMessage()); + System.err.println("Aborting sending."); + return 1; + } + } + + try { + List attachments = ns.getList("attachment"); + if (attachments == null) { + attachments = new ArrayList<>(); + } + if (ns.getString("group") != null) { + byte[] groupId = Util.decodeGroupId(ns.getString("group")); + signal.sendGroupMessage(messageText, attachments, groupId); + } else { + signal.sendMessage(messageText, attachments, ns.getList("recipient")); + } + return 0; + } catch (IOException e) { + handleIOException(e); + return 3; + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + return 3; + } catch (AssertionError e) { + handleAssertionError(e); + return 1; + } catch (GroupNotFoundException e) { + handleGroupNotFoundException(e); + return 1; + } catch (NotAGroupMemberException e) { + handleNotAGroupMemberException(e); + return 1; + } catch (AttachmentInvalidException e) { + System.err.println("Failed to add attachment: " + e.getMessage()); + System.err.println("Aborting sending."); + return 1; + } catch (DBusExecutionException e) { + handleDBusExecutionException(e); + return 1; + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/SetPinCommand.java b/src/main/java/org/asamk/signal/commands/SetPinCommand.java new file mode 100644 index 00000000..de4e28ec --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SetPinCommand.java @@ -0,0 +1,33 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; +import org.whispersystems.libsignal.util.guava.Optional; + +import java.io.IOException; + +public class SetPinCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("registrationLockPin") + .help("The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity)"); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + String registrationLockPin = ns.getString("registrationLockPin"); + m.setRegistrationLockPin(Optional.of(registrationLockPin)); + return 0; + } catch (IOException e) { + System.err.println("Set pin error: " + e.getMessage()); + return 3; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java new file mode 100644 index 00000000..13fb63d4 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -0,0 +1,74 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.Hex; + +import java.util.Locale; + +public class TrustCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("number") + .help("Specify the phone number, for which to set the trust.") + .required(true); + MutuallyExclusiveGroup mutTrust = subparser.addMutuallyExclusiveGroup(); + mutTrust.addArgument("-a", "--trust-all-known-keys") + .help("Trust all known keys of this user, only use this for testing.") + .action(Arguments.storeTrue()); + mutTrust.addArgument("-v", "--verified-fingerprint") + .help("Specify the fingerprint of the key, only use this option if you have verified the fingerprint."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + String number = ns.getString("number"); + if (ns.getBoolean("trust_all_known_keys")) { + boolean res = m.trustIdentityAllKeys(number); + if (!res) { + System.err.println("Failed to set the trust for this number, make sure the number is correct."); + return 1; + } + } else { + String fingerprint = ns.getString("verified_fingerprint"); + if (fingerprint != null) { + fingerprint = fingerprint.replaceAll(" ", ""); + if (fingerprint.length() == 66) { + byte[] fingerprintBytes; + try { + fingerprintBytes = Hex.toByteArray(fingerprint.toLowerCase(Locale.ROOT)); + } catch (Exception e) { + System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); + return 1; + } + boolean res = m.trustIdentityVerified(number, fingerprintBytes); + if (!res) { + System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); + return 1; + } + } else if (fingerprint.length() == 60) { + boolean res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint); + if (!res) { + System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); + return 1; + } + } else { + System.err.println("Fingerprint has invalid format, either specify the old hex fingerprint or the new safety number"); + return 1; + } + } else { + System.err.println("You need to specify the fingerprint you have verified with -v FINGERPRINT"); + return 1; + } + } + return 0; + } +} diff --git a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java new file mode 100644 index 00000000..3830abe6 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java @@ -0,0 +1,30 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; + +import java.io.IOException; + +public class UnregisterCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Unregister the current device from the signal server."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + m.unregister(); + return 0; + } catch (IOException e) { + System.err.println("Unregister error: " + e.getMessage()); + return 3; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java new file mode 100644 index 00000000..31964721 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java @@ -0,0 +1,30 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; + +import java.io.IOException; + +public class UpdateAccountCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Update the account attributes on the signal server."); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + try { + m.updateAccountAttributes(); + return 0; + } catch (IOException e) { + System.err.println("UpdateAccount error: " + e.getMessage()); + return 3; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java new file mode 100644 index 00000000..8f601a68 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -0,0 +1,88 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.Signal; +import org.asamk.signal.AttachmentInvalidException; +import org.asamk.signal.GroupIdFormatException; +import org.asamk.signal.GroupNotFoundException; +import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.util.Util; +import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions; +import org.whispersystems.signalservice.internal.util.Base64; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.asamk.signal.util.ErrorUtils.*; + +public class UpdateGroupCommand implements DbusCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("-g", "--group") + .help("Specify the recipient group ID."); + subparser.addArgument("-n", "--name") + .help("Specify the new group name."); + subparser.addArgument("-a", "--avatar") + .help("Specify a new group avatar image file"); + subparser.addArgument("-m", "--member") + .nargs("*") + .help("Specify one or more members to add to the group"); + } + + @Override + public int handleCommand(final Namespace ns, final Signal signal) { + if (!signal.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + try { + byte[] groupId = null; + if (ns.getString("group") != null) { + groupId = Util.decodeGroupId(ns.getString("group")); + } + if (groupId == null) { + groupId = new byte[0]; + } + String groupName = ns.getString("name"); + if (groupName == null) { + groupName = ""; + } + List groupMembers = ns.getList("member"); + if (groupMembers == null) { + groupMembers = new ArrayList<>(); + } + String groupAvatar = ns.getString("avatar"); + if (groupAvatar == null) { + groupAvatar = ""; + } + byte[] newGroupId = signal.updateGroup(groupId, groupName, groupMembers, groupAvatar); + if (groupId.length != newGroupId.length) { + System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …"); + } + return 0; + } catch (IOException e) { + handleIOException(e); + return 3; + } catch (AttachmentInvalidException e) { + System.err.println("Failed to add avatar attachment for group\": " + e.getMessage()); + System.err.println("Aborting sending."); + return 1; + } catch (GroupNotFoundException e) { + handleGroupNotFoundException(e); + return 1; + } catch (NotAGroupMemberException e) { + handleNotAGroupMemberException(e); + return 1; + } catch (EncapsulatedExceptions e) { + handleEncapsulatedExceptions(e); + return 3; + } catch (GroupIdFormatException e) { + handleGroupIdFormatException(e); + return 1; + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/VerifyCommand.java b/src/main/java/org/asamk/signal/commands/VerifyCommand.java new file mode 100644 index 00000000..aca0d700 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java @@ -0,0 +1,44 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.internal.push.LockedException; + +import java.io.IOException; + +public class VerifyCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("verificationCode") + .help("The verification code you received via sms or voice call."); + subparser.addArgument("-p", "--pin") + .help("The registration lock PIN, that was set by the user (Optional)"); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.userHasKeys()) { + System.err.println("User has no keys, first call register."); + return 1; + } + if (m.isRegistered()) { + System.err.println("User registration is already verified"); + return 1; + } + try { + String verificationCode = ns.getString("verificationCode"); + String pin = ns.getString("pin"); + m.verifyAccount(verificationCode, pin); + return 0; + } catch (LockedException e) { + System.err.println("Verification failed! This number is locked with a pin. Hours remaining until reset: " + (e.getTimeRemaining() / 1000 / 60 / 60)); + System.err.println("Use '--pin PIN_CODE' to specify the registration lock PIN"); + return 3; + } catch (IOException e) { + System.err.println("Verify error: " + e.getMessage()); + return 3; + } + } +} From 7e897fa6d0aaa87646b51efa3ee1a5ecfaa3865e Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 20 Nov 2018 23:27:36 +0100 Subject: [PATCH 0259/2005] Fix inspections --- .../java/org/asamk/signal/GroupNotFoundException.java | 4 ---- .../java/org/asamk/signal/NotAGroupMemberException.java | 4 ---- src/main/java/org/asamk/signal/UserAlreadyExists.java | 4 ++-- src/main/java/org/asamk/signal/manager/Manager.java | 4 +--- src/main/java/org/asamk/signal/manager/Utils.java | 4 ++-- .../asamk/signal/storage/contacts/JsonContactsStore.java | 4 ++-- .../org/asamk/signal/storage/groups/JsonGroupStore.java | 4 ++-- .../signal/storage/protocol/JsonIdentityKeyStore.java | 5 ++--- .../asamk/signal/storage/protocol/JsonPreKeyStore.java | 3 +-- .../asamk/signal/storage/protocol/JsonSessionStore.java | 3 +-- .../signal/storage/protocol/JsonSignalProtocolStore.java | 8 ++++---- .../signal/storage/protocol/JsonSignedPreKeyStore.java | 3 +-- .../org/asamk/signal/storage/threads/JsonThreadStore.java | 4 ++-- 13 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/asamk/signal/GroupNotFoundException.java b/src/main/java/org/asamk/signal/GroupNotFoundException.java index 9f8c681a..5fee6233 100644 --- a/src/main/java/org/asamk/signal/GroupNotFoundException.java +++ b/src/main/java/org/asamk/signal/GroupNotFoundException.java @@ -5,10 +5,6 @@ import org.whispersystems.signalservice.internal.util.Base64; public class GroupNotFoundException extends DBusExecutionException { - public GroupNotFoundException(String message) { - super(message); - } - public GroupNotFoundException(byte[] groupId) { super("Group not found: " + Base64.encodeBytes(groupId)); } diff --git a/src/main/java/org/asamk/signal/NotAGroupMemberException.java b/src/main/java/org/asamk/signal/NotAGroupMemberException.java index 42e021ec..84e2f90f 100644 --- a/src/main/java/org/asamk/signal/NotAGroupMemberException.java +++ b/src/main/java/org/asamk/signal/NotAGroupMemberException.java @@ -5,10 +5,6 @@ import org.whispersystems.signalservice.internal.util.Base64; public class NotAGroupMemberException extends DBusExecutionException { - public NotAGroupMemberException(String message) { - super(message); - } - public NotAGroupMemberException(byte[] groupId, String groupName) { super("User is not a member in group: " + groupName + " (" + Base64.encodeBytes(groupId) + ")"); } diff --git a/src/main/java/org/asamk/signal/UserAlreadyExists.java b/src/main/java/org/asamk/signal/UserAlreadyExists.java index 047b5fc7..28836f28 100644 --- a/src/main/java/org/asamk/signal/UserAlreadyExists.java +++ b/src/main/java/org/asamk/signal/UserAlreadyExists.java @@ -2,8 +2,8 @@ package org.asamk.signal; public class UserAlreadyExists extends Exception { - private String username; - private String fileName; + private final String username; + private final String fileName; public UserAlreadyExists(String username, String fileName) { this.username = username; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 1b703e42..f8870a72 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -73,16 +73,14 @@ public class Manager implements Signal { private final String dataPath; private final String attachmentsPath; private final String avatarsPath; + private final SleepTimer timer = new UptimeSleepTimer(); private SignalAccount account; - private String username; private SignalServiceAccountManager accountManager; private SignalServiceMessagePipe messagePipe = null; private SignalServiceMessagePipe unidentifiedMessagePipe = null; - private SleepTimer timer = new UptimeSleepTimer(); - public Manager(String username, String settingsPath) { this.username = username; this.settingsPath = settingsPath; diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index f47dc1ce..dbcd7e42 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -223,8 +223,8 @@ class Utils { static class DeviceLinkInfo { - String deviceIdentifier; - ECPublicKey deviceKey; + final String deviceIdentifier; + final ECPublicKey deviceKey; DeviceLinkInfo(final String deviceIdentifier, final ECPublicKey deviceKey) { this.deviceIdentifier = deviceIdentifier; diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java index 45e35e99..8d22550b 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java @@ -40,7 +40,7 @@ public class JsonContactsStore { contacts.clear(); } - public static class MapToListSerializer extends JsonSerializer> { + private static class MapToListSerializer extends JsonSerializer> { @Override public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { @@ -48,7 +48,7 @@ public class JsonContactsStore { } } - public static class ContactsDeserializer extends JsonDeserializer> { + private static class ContactsDeserializer extends JsonDeserializer> { @Override public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index 6a3cdedb..4c5677a6 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -37,7 +37,7 @@ public class JsonGroupStore { return new ArrayList<>(groups.values()); } - public static class MapToListSerializer extends JsonSerializer> { + private static class MapToListSerializer extends JsonSerializer> { @Override public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { @@ -45,7 +45,7 @@ public class JsonGroupStore { } } - public static class GroupsDeserializer extends JsonDeserializer> { + private static class GroupsDeserializer extends JsonDeserializer> { @Override public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index a343ad4e..d7049e4a 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -2,7 +2,6 @@ package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; import org.asamk.signal.TrustLevel; import org.whispersystems.libsignal.IdentityKey; @@ -189,13 +188,13 @@ public class JsonIdentityKeyStore implements IdentityKeyStore { this.added = new Date(); } - public Identity(IdentityKey identityKey, TrustLevel trustLevel, Date added) { + Identity(IdentityKey identityKey, TrustLevel trustLevel, Date added) { this.identityKey = identityKey; this.trustLevel = trustLevel; this.added = added; } - public boolean isTrusted() { + boolean isTrusted() { return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java index 3d4e21a3..3065bfde 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java @@ -2,7 +2,6 @@ package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.PreKeyRecord; @@ -21,7 +20,7 @@ class JsonPreKeyStore implements PreKeyStore { } - public void addPreKeys(Map preKeys) { + private void addPreKeys(Map preKeys) { store.putAll(preKeys); } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index 87007d35..7f5c6b06 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -2,7 +2,6 @@ package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.SessionRecord; @@ -20,7 +19,7 @@ class JsonSessionStore implements SessionStore { } - public void addSessions(Map sessions) { + private void addSessions(Map sessions) { this.sessions.putAll(sessions); } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 12522432..6fcb052b 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -21,22 +21,22 @@ public class JsonSignalProtocolStore implements SignalProtocolStore { @JsonProperty("preKeys") @JsonDeserialize(using = JsonPreKeyStore.JsonPreKeyStoreDeserializer.class) @JsonSerialize(using = JsonPreKeyStore.JsonPreKeyStoreSerializer.class) - protected JsonPreKeyStore preKeyStore; + private JsonPreKeyStore preKeyStore; @JsonProperty("sessionStore") @JsonDeserialize(using = JsonSessionStore.JsonSessionStoreDeserializer.class) @JsonSerialize(using = JsonSessionStore.JsonPreKeyStoreSerializer.class) - protected JsonSessionStore sessionStore; + private JsonSessionStore sessionStore; @JsonProperty("signedPreKeyStore") @JsonDeserialize(using = JsonSignedPreKeyStore.JsonSignedPreKeyStoreDeserializer.class) @JsonSerialize(using = JsonSignedPreKeyStore.JsonSignedPreKeyStoreSerializer.class) - protected JsonSignedPreKeyStore signedPreKeyStore; + private JsonSignedPreKeyStore signedPreKeyStore; @JsonProperty("identityKeyStore") @JsonDeserialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreDeserializer.class) @JsonSerialize(using = JsonIdentityKeyStore.JsonIdentityKeyStoreSerializer.class) - protected JsonIdentityKeyStore identityKeyStore; + private JsonIdentityKeyStore identityKeyStore; public JsonSignalProtocolStore() { } diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java index defd7f93..c8f4c3cc 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java @@ -2,7 +2,6 @@ package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.*; import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.SignedPreKeyRecord; @@ -23,7 +22,7 @@ class JsonSignedPreKeyStore implements SignedPreKeyStore { } - public void addSignedPreKeys(Map preKeys) { + private void addSignedPreKeys(Map preKeys) { store.putAll(preKeys); } diff --git a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java b/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java index a9ce6fb6..9ee613b8 100644 --- a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java +++ b/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java @@ -34,7 +34,7 @@ public class JsonThreadStore { return new ArrayList<>(threads.values()); } - public static class MapToListSerializer extends JsonSerializer> { + private static class MapToListSerializer extends JsonSerializer> { @Override public void serialize(final Map value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException { @@ -42,7 +42,7 @@ public class JsonThreadStore { } } - public static class ThreadsDeserializer extends JsonDeserializer> { + private static class ThreadsDeserializer extends JsonDeserializer> { @Override public Map deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { From cf972e5b6c6ae51aeb8a3d4cad95d9da75b42968 Mon Sep 17 00:00:00 2001 From: Vincent Olivier Date: Tue, 20 Nov 2018 17:47:10 -0500 Subject: [PATCH 0260/2005] Manager : removeLinkedDevices updates isMultiDevice and saves the account Manager : addDevice, getLinkedDevices save the account SignalAccount : save/load isMultiDevice SignalAccount : save profileKey SignalAccount : registrationLockPin doesn't automagically becomes the "null" string, and stays null if null --- src/main/java/org/asamk/signal/manager/Manager.java | 5 +++++ src/main/java/org/asamk/signal/storage/SignalAccount.java | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index f8870a72..cb49d463 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -248,11 +248,15 @@ public class Manager implements Signal { public List getLinkedDevices() throws IOException { List devices = accountManager.getDevices(); account.setMultiDevice(devices.size() > 1); + account.save(); return devices; } public void removeLinkedDevices(int deviceId) throws IOException { accountManager.removeDevice(deviceId); + List devices = accountManager.getDevices(); + account.setMultiDevice(devices.size() > 1); + account.save(); } public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { @@ -267,6 +271,7 @@ public class Manager implements Signal { accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey()), verificationCode); account.setMultiDevice(true); + account.save(); } private List generatePreKeys() { diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index cdc3efa5..4378d052 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -135,10 +135,11 @@ public class SignalAccount { if (node != null) { deviceId = node.asInt(); } + if (rootNode.has("isMultiDevice")) isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean(); username = Util.getNotNullNode(rootNode, "username").asText(); password = Util.getNotNullNode(rootNode, "password").asText(); JsonNode pinNode = rootNode.get("registrationLockPin"); - registrationLockPin = pinNode == null ? null : pinNode.asText(); + registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText(); if (rootNode.has("signalingKey")) { signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText(); } @@ -189,11 +190,13 @@ public class SignalAccount { ObjectNode rootNode = jsonProcessor.createObjectNode(); rootNode.put("username", username) .put("deviceId", deviceId) + .put("isMultiDevice", isMultiDevice) .put("password", password) .put("registrationLockPin", registrationLockPin) .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) + .put("profileKey", Base64.encodeBytes(profileKey)) .put("registered", registered) .putPOJO("axolotlStore", signalProtocolStore) .putPOJO("groupStore", groupStore) From 5f2190713afdcc57469384f11bfb0926f1a64b7f Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 21 Nov 2018 00:07:05 +0100 Subject: [PATCH 0261/2005] Use custom SecureRandom instance - Use NativePRNG algorithm instead of using SHA1PRNG if available - Register a custom security provider to use the same SecureRandom everywhere --- src/main/java/org/asamk/signal/Main.java | 7 ++- .../org/asamk/signal/manager/KeyUtils.java | 14 +----- .../org/asamk/signal/util/RandomUtils.java | 37 ++++++++++++++++ .../asamk/signal/util/SecurityProvider.java | 44 +++++++++++++++++++ 4 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/asamk/signal/util/RandomUtils.java create mode 100644 src/main/java/org/asamk/signal/util/SecurityProvider.java diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index a0820f24..df22e63b 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -24,6 +24,8 @@ import org.asamk.Signal; import org.asamk.signal.commands.*; import org.asamk.signal.manager.BaseConfig; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.SecurityProvider; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -35,8 +37,9 @@ import java.util.Map; public class Main { public static void main(String[] args) { - // Workaround for BKS truststore - Security.insertProviderAt(new org.bouncycastle.jce.provider.BouncyCastleProvider(), 1); + // Register our own security provider + Security.insertProviderAt(new SecurityProvider(), 1); + Security.addProvider(new BouncyCastleProvider()); Namespace ns = parseArgs(args); if (ns == null) { diff --git a/src/main/java/org/asamk/signal/manager/KeyUtils.java b/src/main/java/org/asamk/signal/manager/KeyUtils.java index 225cf682..617893fc 100644 --- a/src/main/java/org/asamk/signal/manager/KeyUtils.java +++ b/src/main/java/org/asamk/signal/manager/KeyUtils.java @@ -1,10 +1,8 @@ package org.asamk.signal.manager; +import org.asamk.signal.util.RandomUtils; import org.whispersystems.signalservice.internal.util.Base64; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - class KeyUtils { private KeyUtils() { @@ -33,15 +31,7 @@ class KeyUtils { private static byte[] getSecretBytes(int size) { byte[] secret = new byte[size]; - getSecureRandom().nextBytes(secret); + RandomUtils.getSecureRandom().nextBytes(secret); return secret; } - - private static SecureRandom getSecureRandom() { - try { - return SecureRandom.getInstance("SHA1PRNG"); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } - } } diff --git a/src/main/java/org/asamk/signal/util/RandomUtils.java b/src/main/java/org/asamk/signal/util/RandomUtils.java new file mode 100644 index 00000000..d0463b47 --- /dev/null +++ b/src/main/java/org/asamk/signal/util/RandomUtils.java @@ -0,0 +1,37 @@ +package org.asamk.signal.util; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class RandomUtils { + + private static final ThreadLocal LOCAL_RANDOM = new ThreadLocal() { + @Override + protected SecureRandom initialValue() { + SecureRandom rand = getSecureRandomUnseeded(); + + // Let the SecureRandom seed it self initially + rand.nextBoolean(); + + return rand; + } + }; + + private static SecureRandom getSecureRandomUnseeded() { + try { + return SecureRandom.getInstance("NativePRNG"); + } catch (NoSuchAlgorithmException e) { + // Fallback to SHA1PRNG if NativePRNG is not available (e.g. on windows) + try { + return SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e1) { + // Fallback to default + return new SecureRandom(); + } + } + } + + public static SecureRandom getSecureRandom() { + return LOCAL_RANDOM.get(); + } +} diff --git a/src/main/java/org/asamk/signal/util/SecurityProvider.java b/src/main/java/org/asamk/signal/util/SecurityProvider.java new file mode 100644 index 00000000..9177a781 --- /dev/null +++ b/src/main/java/org/asamk/signal/util/SecurityProvider.java @@ -0,0 +1,44 @@ +package org.asamk.signal.util; + +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; + +public class SecurityProvider extends Provider { + + private static final String PROVIDER_NAME = "SSP"; + + private static final String info = "Security Provider v1.0"; + + public SecurityProvider() { + super(PROVIDER_NAME, 1.0, info); + put("SecureRandom.DEFAULT", DefaultRandom.class.getName()); + + // Workaround for BKS truststore + put("KeyStore.BKS", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Std"); + put("KeyStore.BKS-V1", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Version1"); + put("KeyStore.BouncyCastle", "org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$BouncyCastleStore"); + put("KeyFactory.X.509", "org.bouncycastle.jcajce.provider.asymmetric.x509.KeyFactory"); + put("CertificateFactory.X.509", "org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory"); + } + + public static class DefaultRandom extends SecureRandomSpi { + + private static final SecureRandom random = RandomUtils.getSecureRandom(); + + public DefaultRandom() { + } + + protected void engineSetSeed(byte[] bytes) { + random.setSeed(bytes); + } + + protected void engineNextBytes(byte[] bytes) { + random.nextBytes(bytes); + } + + protected byte[] engineGenerateSeed(int numBytes) { + return random.generateSeed(numBytes); + } + } +} From e809792467f50ed482fa4a2fd6e5e1577d306e6f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 25 Nov 2018 22:07:48 +0100 Subject: [PATCH 0262/2005] Save account after creating profile key --- src/main/java/org/asamk/signal/manager/Manager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index cb49d463..f18cb219 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -161,6 +161,7 @@ public class Manager implements Signal { if (account.getProfileKey() == null) { // Old config file, creating new profile key account.setProfileKey(KeyUtils.createProfileKey()); + account.save(); } } From 65390ef1d8d3b0d3153197d1843a2def2200855c Mon Sep 17 00:00:00 2001 From: "Lars K.W. Gohlke" Date: Thu, 29 Nov 2018 23:26:51 +0100 Subject: [PATCH 0263/2005] makes checkLibVersions gradle 5 compatible --- build.gradle | 52 +++++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/build.gradle b/build.gradle index 66b2510a..c0c9097d 100644 --- a/build.gradle +++ b/build.gradle @@ -47,33 +47,35 @@ run { // Find any 3rd party libraries which have released new versions // to the central Maven repo since we last upgraded. // http://daniel.gredler.net/2011/08/08/gradle-keeping-libraries-up-to-date/ -task checkLibVersions << { - def checked = [:] - allprojects { - configurations.each { configuration -> - configuration.allDependencies.each { dependency -> - def version = dependency.version - if (!version.contains('SNAPSHOT') && !checked[dependency]) { - def group = dependency.group - def path = group.replace('.', '/') - def name = dependency.name - def url = "http://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" - try { - def metadata = new XmlSlurper().parseText(url.toURL().text) - def versions = metadata.versioning.versions.version.collect { it.text() } - versions.removeAll { it.toLowerCase().contains('alpha') } - versions.removeAll { it.toLowerCase().contains('beta') } - versions.removeAll { it.toLowerCase().contains('rc') } - def newest = versions.max() - if (version != newest) { - println "$group:$name $version -> $newest" +task checkLibVersions { + doLast { + def checked = [:] + allprojects { + configurations.each { configuration -> + configuration.allDependencies.each { dependency -> + def version = dependency.version + if (!version.contains('SNAPSHOT') && !checked[dependency]) { + def group = dependency.group + def path = group.replace('.', '/') + def name = dependency.name + def url = "http://repo1.maven.org/maven2/$path/$name/maven-metadata.xml" + try { + def metadata = new XmlSlurper().parseText(url.toURL().text) + def versions = metadata.versioning.versions.version.collect { it.text() } + versions.removeAll { it.toLowerCase().contains('alpha') } + versions.removeAll { it.toLowerCase().contains('beta') } + versions.removeAll { it.toLowerCase().contains('rc') } + def newest = versions.max() + if (version != newest) { + println "$group:$name $version -> $newest" + } + } catch (FileNotFoundException e) { + logger.debug "Unable to download $url: $e.message" + } catch (org.xml.sax.SAXParseException e) { + logger.debug "Unable to parse $url: $e.message" } - } catch (FileNotFoundException e) { - logger.debug "Unable to download $url: $e.message" - } catch (org.xml.sax.SAXParseException e) { - logger.debug "Unable to parse $url: $e.message" + checked[dependency] = true } - checked[dependency] = true } } } From ffbc356218a54fec6b80efe9dcb8eb09cba98d67 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 1 Dec 2018 14:31:41 +0100 Subject: [PATCH 0264/2005] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55741 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 2 +- gradlew.bat | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..457aad0d98108420a977756b7145c93c8910b076 100644 GIT binary patch delta 46476 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

d1ZzV`PJaf;LURr$w8USfDLW+{$cK=5Uq0( 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�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 V;3?$)Jo1M@amx&F&%^&5`+o%ZZcYFI diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e0b3fb8d..75b8c7c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index cccdd3d5..af6708ff 100755 --- a/gradlew +++ b/gradlew @@ -28,7 +28,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/gradlew.bat b/gradlew.bat index e95643d6..0f8d5937 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome From 337f84ed2162d3aaf7efa5e70c9ad9a092f98348 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 8 Dec 2018 18:26:54 +0100 Subject: [PATCH 0265/2005] Change default data path to $XDG_DATA_HOME/signal-cli Closes #152 and #125 --- README.md | 6 ++- man/signal-cli.1.adoc | 8 ++-- src/main/java/org/asamk/signal/Main.java | 43 ++++++++++++++----- .../java/org/asamk/signal/util/IOUtils.java | 9 ++++ 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e703a4ef..b4bed3b8 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,11 @@ For more information read the [man page](https://github.com/AsamK/signal-cli/blo The password and cryptographic keys are created when registering and stored in the current users home directory: - $HOME/.config/signal/data/ +`$XDG_DATA_HOME/signal-cli/data/` (`$HOME/.local/share/signal-cli/data/`) -For legacy users, the old config directory is used as a fallback: +For legacy users, the old config directories are used as a fallback: + + $HOME/.config/signal/data/ $HOME/.config/textsecure/data/ diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 67e55bf5..d8e0cb90 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -35,7 +35,7 @@ Options *--config* CONFIG:: Set the path, where to store the config. Make sure you have full read/write access to the given directory. - (Default: $HOME/.config/signal) + (Default: `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`)) *-u* USERNAME, *--username* USERNAME:: Specify your phone number, that will be your identifier. @@ -259,9 +259,11 @@ Files The password and cryptographic keys are created when registering and stored in the current users home directory, the directory can be changed with *--config*: - $HOME/.config/signal/ +`$XDG_DATA_HOME/signal-cli/` (`$HOME/.local/share/signal-cli/`) -For legacy users, the old config directory is used as a fallback: +For legacy users, the old config directories are used as a fallback: + + $HOME/.config/signal/ $HOME/.config/textsecure/ diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index df22e63b..e598d8e5 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -24,6 +24,7 @@ import org.asamk.Signal; import org.asamk.signal.commands.*; import org.asamk.signal.manager.BaseConfig; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.SecurityProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.freedesktop.dbus.DBusConnection; @@ -80,18 +81,12 @@ public class Main { return 3; } } else { - String settingsPath = ns.getString("config"); - if (TextUtils.isEmpty(settingsPath)) { - settingsPath = System.getProperty("user.home") + "/.config/signal"; - if (!new File(settingsPath).exists()) { - String legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure"; - if (new File(legacySettingsPath).exists()) { - settingsPath = legacySettingsPath; - } - } + String dataPath = ns.getString("config"); + if (TextUtils.isEmpty(dataPath)) { + dataPath = getDefaultDataPath(); } - m = new Manager(username, settingsPath); + m = new Manager(username, dataPath); ts = m; try { m.init(); @@ -134,6 +129,32 @@ public class Main { } } + /** + * Uses $XDG_DATA_HOME/signal-cli if it exists, or if none of the legacy directories exist: + * - $HOME/.config/signal + * - $HOME/.config/textsecure + * + * @return the data directory to be used by signal-cli. + */ + private static String getDefaultDataPath() { + String dataPath = IOUtils.getDataHomeDir() + "/signal-cli"; + if (new File(dataPath).exists()) { + return dataPath; + } + + String legacySettingsPath = System.getProperty("user.home") + "/.config/signal"; + if (new File(legacySettingsPath).exists()) { + return legacySettingsPath; + } + + legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure"; + if (new File(legacySettingsPath).exists()) { + return legacySettingsPath; + } + + return dataPath; + } + private static Namespace parseArgs(String[] args) { ArgumentParser parser = ArgumentParsers.newFor("signal-cli") .build() @@ -145,7 +166,7 @@ public class Main { .help("Show package version.") .action(Arguments.version()); parser.addArgument("--config") - .help("Set the path, where to store the config (Default: $HOME/.config/signal)."); + .help("Set the path, where to store the config (Default: $XDG_DATA_HOME/signal-cli , $HOME/.local/share/signal-cli)."); MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup(); mut.addArgument("-u", "--username") diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index 9b8c3b5b..e1464b1c 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -57,4 +57,13 @@ public class IOUtils { Files.createFile(file); } } + + public static String getDataHomeDir() { + String dataHome = System.getenv("XDG_DATA_HOME"); + if (dataHome != null) { + return dataHome; + } + + return System.getProperty("user.home") + "/.local/share"; + } } From ea8f7e75286d03f989d4fe56e56c35e171073546 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 9 Dec 2018 18:22:28 +0100 Subject: [PATCH 0266/2005] Bump version 0.6.1 - Added getGroupIds dbus command - Use "NativePRNG" pseudo random number generator, if available - Switch default data path: $XDG_DATA_HOME/signal-cli ($HOME/.local/share/signal-cli) Existing data paths will continue to work (used as fallback) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c0c9097d..0f0231d7 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.6.0' +version = '0.6.1' compileJava.options.encoding = 'UTF-8' From fd550d6088d2561e7dad5a8b123c142e1a4bd6f7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 12 Dec 2018 22:21:49 +0100 Subject: [PATCH 0267/2005] Update libsignal-service-java --- build.gradle | 2 +- src/main/java/org/asamk/signal/manager/Manager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 0f0231d7..73823ba2 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.12.2_unofficial_2' + compile 'com.github.turasa:signal-service-java:2.12.3_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.60' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index f18cb219..8bfdff08 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -189,7 +189,7 @@ public class Manager implements Signal { accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, account.getUsername(), account.getPassword(), BaseConfig.USER_AGENT, timer); if (voiceVerification) - accountManager.requestVoiceVerificationCode(); + accountManager.requestVoiceVerificationCode(Locale.getDefault()); else accountManager.requestSmsVerificationCode(); From 5d843d82effaa355c2e2d69cf979aacc89a96570 Mon Sep 17 00:00:00 2001 From: Tai Groot Date: Thu, 13 Dec 2018 21:50:26 -0800 Subject: [PATCH 0268/2005] Minor spelling/grammar fix --- src/main/java/org/asamk/signal/Main.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index e598d8e5..2d15c096 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -216,7 +216,7 @@ public class Main { } } if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) { - System.err.println("You cannot specify recipients by phone number and groups a the same time"); + System.err.println("You cannot specify recipients by phone number and groups at the same time"); System.exit(2); } return ns; From 51c130b40651b1d01caee7175235f2632f5aa73e Mon Sep 17 00:00:00 2001 From: Herohtar Date: Fri, 14 Dec 2018 11:15:12 -0600 Subject: [PATCH 0269/2005] Don't abort on empty recipient unless there was also no group specified. (#176) * Don't abort on empty recipient unless there was also no group specified. * Fixed potential error if user tries to send `endsession` to a group * Display error if trying to send `endsession` to a group * No need for this check since we're handling that condition above --- src/main/java/org/asamk/signal/commands/SendCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 308e564c..176c2d92 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -46,7 +46,7 @@ public class SendCommand implements DbusCommand { return 1; } - if (ns.getList("recipient") == null || ns.getList("recipient").size() == 0) { + if ((ns.getList("recipient") == null || ns.getList("recipient").size() == 0) && (ns.getBoolean("endsession") || ns.getString("group") == null)) { System.err.println("No recipients given"); System.err.println("Aborting sending."); return 1; From f3878c54a627d278e503de1f4497a86f76519bc8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 16 Dec 2018 21:14:00 +0100 Subject: [PATCH 0270/2005] Update signal-service-java --- build.gradle | 2 +- src/main/java/org/asamk/signal/manager/Manager.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 73823ba2..c96f6339 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.12.3_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.12.4_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.60' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 8bfdff08..44ec6445 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -188,10 +188,11 @@ public class Manager implements Signal { account.setPassword(KeyUtils.createPassword()); accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, account.getUsername(), account.getPassword(), BaseConfig.USER_AGENT, timer); - if (voiceVerification) + if (voiceVerification) { accountManager.requestVoiceVerificationCode(Locale.getDefault()); - else - accountManager.requestSmsVerificationCode(); + } else { + accountManager.requestSmsVerificationCode(false); + } account.setRegistered(false); account.save(); From a055f282c69472b1aab2538c5b9ccbbd4a4b3f5d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 16 Dec 2018 21:14:05 +0100 Subject: [PATCH 0271/2005] Bump version 0.6.2 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index c96f6339..f034e1bd 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.6.1' +version = '0.6.2' compileJava.options.encoding = 'UTF-8' From 58895aaf03742cb5c2067ec54ab3ab2bfaaa27b4 Mon Sep 17 00:00:00 2001 From: Parker Higgins Date: Wed, 9 Jan 2019 14:17:45 -0500 Subject: [PATCH 0272/2005] Expose filename of attachments to json message handler (#185) --- src/main/java/org/asamk/signal/JsonAttachment.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/asamk/signal/JsonAttachment.java b/src/main/java/org/asamk/signal/JsonAttachment.java index 29e8592e..785fa9e2 100644 --- a/src/main/java/org/asamk/signal/JsonAttachment.java +++ b/src/main/java/org/asamk/signal/JsonAttachment.java @@ -6,14 +6,19 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPoin class JsonAttachment { String contentType; + String filename; long id; int size; JsonAttachment(SignalServiceAttachment attachment) { this.contentType = attachment.getContentType(); + final SignalServiceAttachmentPointer pointer = attachment.asPointer(); if (attachment.isPointer()) { this.id = pointer.getId(); + if (pointer.getFileName().isPresent()) { + this.filename = pointer.getFileName().get(); + } if (pointer.getSize().isPresent()) { this.size = pointer.getSize().get(); } From c90d5db608fd3c15c53506cd474a225cb7041bd7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 13 Feb 2019 21:05:27 +0100 Subject: [PATCH 0273/2005] Update libsignal-service-java --- build.gradle | 2 +- src/main/java/org/asamk/signal/Main.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index f034e1bd..13fd298e 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.12.4_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.12.7_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.60' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 2d15c096..e5335b28 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -210,7 +210,7 @@ public class Main { System.err.println("You need to specify a username (phone number)"); System.exit(2); } - if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) { + if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"), null)) { System.err.println("Invalid username (phone number), make sure you include the country code."); System.exit(2); } From 6d5cfa32e2d4b7e2038872ab1e526c4d6d608215 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 13 Feb 2019 21:19:31 +0100 Subject: [PATCH 0274/2005] Fix NPE when receiving contacts sync message Fixes #191 --- src/main/java/org/asamk/signal/manager/Manager.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 44ec6445..e358fcb9 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -1149,6 +1149,10 @@ public class Manager implements Signal { } if (c.getExpirationTimer().isPresent()) { ThreadInfo thread = account.getThreadStore().getThread(c.getNumber()); + if (thread == null) { + thread = new ThreadInfo(); + thread.id = c.getNumber(); + } thread.messageExpirationTime = c.getExpirationTimer().get(); account.getThreadStore().updateThread(thread); } From 6f7350d031585e47172a52ffd2892b5aceb956b6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 Mar 2019 18:32:31 +0100 Subject: [PATCH 0275/2005] Update dependencies --- build.gradle | 4 ++-- src/main/java/org/asamk/signal/Main.java | 5 +++-- src/main/java/org/asamk/signal/manager/Manager.java | 4 ++-- src/main/java/org/asamk/signal/manager/Utils.java | 5 +++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 13fd298e..97f0d0c2 100644 --- a/build.gradle +++ b/build.gradle @@ -20,8 +20,8 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.12.7_unofficial_1' - compile 'org.bouncycastle:bcprov-jdk15on:1.60' + compile 'com.github.turasa:signal-service-java:2.13.0_unofficial_2' + compile 'org.bouncycastle:bcprov-jdk15on:1.61' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index e5335b28..e1343fbf 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -19,7 +19,6 @@ package org.asamk.signal; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.*; -import org.apache.http.util.TextUtils; import org.asamk.Signal; import org.asamk.signal.commands.*; import org.asamk.signal.manager.BaseConfig; @@ -35,6 +34,8 @@ import java.io.File; import java.security.Security; import java.util.Map; +import static org.whispersystems.signalservice.internal.util.Util.isEmpty; + public class Main { public static void main(String[] args) { @@ -82,7 +83,7 @@ public class Main { } } else { String dataPath = ns.getString("config"); - if (TextUtils.isEmpty(dataPath)) { + if (isEmpty(dataPath)) { dataPath = getDefaultDataPath(); } diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index e358fcb9..94605632 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -189,9 +189,9 @@ public class Manager implements Signal { accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, account.getUsername(), account.getPassword(), BaseConfig.USER_AGENT, timer); if (voiceVerification) { - accountManager.requestVoiceVerificationCode(Locale.getDefault()); + accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent()); } else { - accountManager.requestSmsVerificationCode(false); + accountManager.requestSmsVerificationCode(false, Optional.absent()); } account.setRegistered(false); diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index dbcd7e42..4012674f 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -1,6 +1,5 @@ package org.asamk.signal.manager; -import org.apache.http.util.TextUtils; import org.asamk.signal.AttachmentInvalidException; import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.whispersystems.libsignal.IdentityKey; @@ -25,6 +24,8 @@ import java.net.URLEncoder; import java.nio.file.Files; import java.util.*; +import static org.whispersystems.signalservice.internal.util.Util.isEmpty; + class Utils { static List getSignalServiceAttachments(List attachments) throws AttachmentInvalidException { @@ -100,7 +101,7 @@ class Utils { String deviceIdentifier = query.get("uuid"); String publicKeyEncoded = query.get("pub_key"); - if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) { + if (isEmpty(deviceIdentifier) || isEmpty(publicKeyEncoded)) { throw new RuntimeException("Invalid device link uri"); } From 24714454dd43fe2df8981d37c98bd5c4c1a69a45 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 23 Mar 2019 22:08:25 +0100 Subject: [PATCH 0276/2005] Send self messages only as sync messages To align with the way Note to Self messages are implemented on Android --- .../java/org/asamk/signal/manager/Manager.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 94605632..d75ac76b 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -773,6 +773,24 @@ public class Manager implements Signal { account.getSignalProtocolStore().saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); return Collections.emptyList(); } + } else if (recipientsTS.size() == 1 && recipientsTS.contains(new SignalServiceAddress(username))) { + SignalServiceAddress recipient = new SignalServiceAddress(username); + final Optional unidentifiedAccess = getAccessFor(recipient); + SentTranscriptMessage transcript = new SentTranscriptMessage(recipient.getNumber(), + message.getTimestamp(), + message, + message.getExpiresInSeconds(), + Collections.singletonMap(recipient.getNumber(), unidentifiedAccess.isPresent())); + SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); + + List results = new ArrayList<>(recipientsTS.size()); + try { + messageSender.sendMessage(syncMessage, unidentifiedAccess); + } catch (UntrustedIdentityException e) { + account.getSignalProtocolStore().saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED); + results.add(SendMessageResult.identityFailure(recipient, e.getIdentityKey())); + } + return results; } else { // Send to all individually, so sync messages are sent correctly List results = new ArrayList<>(recipientsTS.size()); From 35181251bfe672a21f84a105b5f9dc8080b83cd9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 19 Jun 2019 21:47:18 +0200 Subject: [PATCH 0277/2005] Update libsignal-service-java dependency --- build.gradle | 2 +- src/main/java/org/asamk/signal/manager/Manager.java | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 97f0d0c2..acadd909 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.13.0_unofficial_2' + compile 'com.github.turasa:signal-service-java:2.13.4_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.61' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index d75ac76b..a719dcf7 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -56,6 +56,7 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; +import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.util.Base64; import java.io.*; @@ -762,7 +763,8 @@ public class Manager implements Signal { message = messageBuilder.build(); if (message.getGroupInfo().isPresent()) { try { - List result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), message); + final boolean isRecipientUpdate = true; + List result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), isRecipientUpdate, message); for (SendMessageResult r : result) { if (r.getIdentityFailure() != null) { account.getSignalProtocolStore().saveIdentity(r.getAddress().getNumber(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED); @@ -780,7 +782,8 @@ public class Manager implements Signal { message.getTimestamp(), message, message.getExpiresInSeconds(), - Collections.singletonMap(recipient.getNumber(), unidentifiedAccess.isPresent())); + Collections.singletonMap(recipient.getNumber(), unidentifiedAccess.isPresent()), + false); SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); List results = new ArrayList<>(recipientsTS.size()); @@ -822,7 +825,7 @@ public class Manager implements Signal { } } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException { + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException, UnsupportedDataMessageException { SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), account.getSignalProtocolStore(), Utils.getCertificateValidator()); try { return cipher.decrypt(envelope); From 93ae4641fa1898d5ecf4d589f189bd2590af00b7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 13 Jul 2019 14:22:20 +0200 Subject: [PATCH 0278/2005] Update libsignal-service-java --- build.gradle | 2 +- src/main/java/org/asamk/signal/manager/Manager.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index acadd909..bfac34b0 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.13.4_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.13.5_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.61' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index a719dcf7..492842ab 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -190,9 +190,9 @@ public class Manager implements Signal { accountManager = new SignalServiceAccountManager(BaseConfig.serviceConfiguration, account.getUsername(), account.getPassword(), BaseConfig.USER_AGENT, timer); if (voiceVerification) { - accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent()); + accountManager.requestVoiceVerificationCode(Locale.getDefault(), Optional.absent(), Optional.absent()); } else { - accountManager.requestSmsVerificationCode(false, Optional.absent()); + accountManager.requestSmsVerificationCode(false, Optional.absent(), Optional.absent()); } account.setRegistered(false); From 8574eb3f95a21405de64af359ca9f357a4fbe3f1 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 13 Jul 2019 14:24:14 +0200 Subject: [PATCH 0279/2005] Update dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bfac34b0..73302563 100644 --- a/build.gradle +++ b/build.gradle @@ -21,7 +21,7 @@ repositories { dependencies { compile 'com.github.turasa:signal-service-java:2.13.5_unofficial_1' - compile 'org.bouncycastle:bcprov-jdk15on:1.61' + compile 'org.bouncycastle:bcprov-jdk15on:1.62' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' } From 78474453d7de5fa50c32c460e18877485bc26f78 Mon Sep 17 00:00:00 2001 From: Juergen Kurzmann Date: Fri, 19 Jul 2019 11:26:19 +0200 Subject: [PATCH 0280/2005] Throw error on failed authorization - to exit signal-cli in case the number was registered elsewhere --- src/main/java/org/asamk/signal/manager/Manager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 492842ab..575dde67 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -137,6 +137,7 @@ public class Manager implements Signal { } } catch (AuthorizationFailedException e) { System.err.println("Authorization failed, was the number registered elsewhere?"); + throw e; } } From 8c295a3f905114815ed97d4cb9eb98e41f03c39d Mon Sep 17 00:00:00 2001 From: Juergen Kurzmann Date: Sat, 3 Aug 2019 21:49:02 +0200 Subject: [PATCH 0281/2005] Update SignalAccount storage on unregister - save registered false state in the SignalAccount storage on unregister action --- src/main/java/org/asamk/signal/manager/Manager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 575dde67..8672684a 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -209,6 +209,9 @@ public class Manager implements Signal { // If this is the master device, other users can't send messages to this number anymore. // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. accountManager.setGcmId(Optional.absent()); + + account.setRegistered(false); + account.save(); } public String getDeviceLinkUri() throws TimeoutException, IOException { From e36a54e7ccb7e103027c71c6128854f06537d787 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 5 Sep 2019 10:15:52 +0200 Subject: [PATCH 0282/2005] Synchronize fileChannel access Potention fix for #89 --- .../org/asamk/signal/storage/SignalAccount.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 4378d052..6af9d460 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -129,7 +129,11 @@ public class SignalAccount { } private void load() throws IOException { - JsonNode rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel)); + JsonNode rootNode; + synchronized (fileChannel) { + fileChannel.position(0); + rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel)); + } JsonNode node = rootNode.get("deviceId"); if (node != null) { @@ -204,10 +208,12 @@ public class SignalAccount { .putPOJO("threadStore", threadStore) ; try { - fileChannel.position(0); - jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode); - fileChannel.truncate(fileChannel.position()); - fileChannel.force(false); + synchronized (fileChannel) { + fileChannel.position(0); + jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode); + fileChannel.truncate(fileChannel.position()); + fileChannel.force(false); + } } catch (Exception e) { System.err.println(String.format("Error saving file: %s", e.getMessage())); } From 83122737dc250e9a7bee10f40f9fe79b40fc0bfa Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 5 Sep 2019 10:24:09 +0200 Subject: [PATCH 0283/2005] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 55741 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 22 +++++++++++++++++++--- gradlew.bat | 18 +++++++++++++++++- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 457aad0d98108420a977756b7145c93c8910b076..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch delta 19567 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 PFlPAY0appj7IgMMQM_1| diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 75b8c7c8..59b5f892 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index af6708ff..83f2acfd 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -109,8 +125,8 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` diff --git a/gradlew.bat b/gradlew.bat index 0f8d5937..24467a14 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome From bd69660ac64dfdb0a0f067729cc9041979df21da Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 5 Sep 2019 11:18:21 +0200 Subject: [PATCH 0284/2005] Send expirationTime with all group updates Fixes #140 --- .../java/org/asamk/signal/manager/Manager.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 8672684a..0428755e 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -515,8 +515,15 @@ public class Manager implements Signal { } } - return SignalServiceDataMessage.newBuilder() + SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()); + + ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(g.groupId)); + if (thread != null) { + messageBuilder.withExpiration(thread.messageExpirationTime); + } + + return messageBuilder; } private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions { @@ -530,6 +537,11 @@ public class Manager implements Signal { SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()); + ThreadInfo thread = account.getThreadStore().getThread(Base64.encodeBytes(groupId)); + if (thread != null) { + messageBuilder.withExpiration(thread.messageExpirationTime); + } + // Send group info request message to the recipient who sent us a message with this groupId final List membersSend = new ArrayList<>(); membersSend.add(recipient); From 6e9a3dd6494d60b6bb4f8f1457ea07a39c5bb7ad Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 5 Sep 2019 11:21:33 +0200 Subject: [PATCH 0285/2005] Update dependencies --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 73302563..e6446f27 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.13.5_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.13.7_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.62' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' From 129f48e109e71e164a67c5f2b5f71a7a3742cfe2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 5 Sep 2019 19:43:24 +0200 Subject: [PATCH 0286/2005] Fix sending sync messages for group messages Fixes #210 --- src/main/java/org/asamk/signal/manager/Manager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 0428755e..ee999194 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -779,7 +779,7 @@ public class Manager implements Signal { message = messageBuilder.build(); if (message.getGroupInfo().isPresent()) { try { - final boolean isRecipientUpdate = true; + final boolean isRecipientUpdate = false; List result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), isRecipientUpdate, message); for (SendMessageResult r : result) { if (r.getIdentityFailure() != null) { From 1184a87f2dbd4d3486785b8c665897b4c218da86 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 5 Sep 2019 20:04:42 +0200 Subject: [PATCH 0287/2005] Bump version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index e6446f27..2fbcc2df 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ targetCompatibility = JavaVersion.VERSION_1_7 mainClassName = 'org.asamk.signal.Main' -version = '0.6.2' +version = '0.6.3' compileJava.options.encoding = 'UTF-8' From 1df862234d60de56a5950aa6423de578003fdca6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 11 Sep 2019 10:28:10 +0200 Subject: [PATCH 0288/2005] Switch to github actions --- .github/workflows/ci.yml | 20 ++++++++++++++++++++ .travis.yml | 8 -------- 2 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..6c1f00b0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: signal-cli CI + +on: [push, pull_request] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + java: [ '1.8', '12.0.2' ] + + steps: + - uses: actions/checkout@v1 + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + - name: Build with Gradle + run: ./gradlew build diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 367c1ef7..00000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: java -before_cache: - - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ -cache: - directories: - - $HOME/.gradle/caches/ - - $HOME/.gradle/wrapper/ From 9aa13e92fed26cb2cc5c90045becac9e1502b36c Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 11 Sep 2019 11:30:13 +0200 Subject: [PATCH 0289/2005] Require java 1.8 - VERSION_1_7 is deprecated in java 12 - the used gradle version already requires java 1.8 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 2fbcc2df..ecd2a889 100644 --- a/build.gradle +++ b/build.gradle @@ -2,8 +2,8 @@ apply plugin: 'java' apply plugin: 'application' apply plugin: 'eclipse' -sourceCompatibility = JavaVersion.VERSION_1_7 -targetCompatibility = JavaVersion.VERSION_1_7 +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 mainClassName = 'org.asamk.signal.Main' From e490604d4320352531dc53570962de8f48341c26 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 29 Sep 2019 11:21:15 +0200 Subject: [PATCH 0290/2005] Output attachment id as json string to prevent rounding due to conversion to double Fixes #226 --- src/main/java/org/asamk/signal/JsonAttachment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonAttachment.java b/src/main/java/org/asamk/signal/JsonAttachment.java index 785fa9e2..58165639 100644 --- a/src/main/java/org/asamk/signal/JsonAttachment.java +++ b/src/main/java/org/asamk/signal/JsonAttachment.java @@ -7,7 +7,7 @@ class JsonAttachment { String contentType; String filename; - long id; + String id; int size; JsonAttachment(SignalServiceAttachment attachment) { @@ -15,7 +15,7 @@ class JsonAttachment { final SignalServiceAttachmentPointer pointer = attachment.asPointer(); if (attachment.isPointer()) { - this.id = pointer.getId(); + this.id = String.valueOf(pointer.getId()); if (pointer.getFileName().isPresent()) { this.filename = pointer.getFileName().get(); } From 625034b2d2e43346cf77a02b4c987dbd87c1adce Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 29 Sep 2019 11:21:41 +0200 Subject: [PATCH 0291/2005] Update dependencies --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index ecd2a889..56d67bfd 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { } dependencies { - compile 'com.github.turasa:signal-service-java:2.13.7_unofficial_1' + compile 'com.github.turasa:signal-service-java:2.13.8_unofficial_1' compile 'org.bouncycastle:bcprov-jdk15on:1.62' compile 'net.sourceforge.argparse4j:argparse4j:0.8.1' compile 'org.freedesktop.dbus:dbus-java:2.7.0' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 59b5f892..7c4388a9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 7e267f1ebb17dd8e6b9ea007c2d65305fea0fb05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fern=C3=A1ndez=20Vald=C3=A9s?= Date: Mon, 23 Sep 2019 12:31:55 -0400 Subject: [PATCH 0292/2005] Added ReceiptMessage to JSON output --- .../org/asamk/signal/JsonMessageEnvelope.java | 4 +++ .../org/asamk/signal/JsonReceiptMessage.java | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/main/java/org/asamk/signal/JsonReceiptMessage.java diff --git a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/JsonMessageEnvelope.java index 9971b011..e7003130 100644 --- a/src/main/java/org/asamk/signal/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/JsonMessageEnvelope.java @@ -14,6 +14,7 @@ class JsonMessageEnvelope { JsonDataMessage dataMessage; JsonSyncMessage syncMessage; JsonCallMessage callMessage; + JsonReceiptMessage receiptMessage; public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content) { SignalServiceAddress source = envelope.getSourceAddress(); @@ -32,6 +33,9 @@ class JsonMessageEnvelope { if (content.getCallMessage().isPresent()) { this.callMessage = new JsonCallMessage(content.getCallMessage().get()); } + if (content.getReceiptMessage().isPresent()) { + this.receiptMessage = new JsonReceiptMessage(content.getReceiptMessage().get()); + } } } } diff --git a/src/main/java/org/asamk/signal/JsonReceiptMessage.java b/src/main/java/org/asamk/signal/JsonReceiptMessage.java new file mode 100644 index 00000000..fd875af5 --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonReceiptMessage.java @@ -0,0 +1,25 @@ +package org.asamk.signal; + +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; + +import java.util.List; + +class JsonReceiptMessage { + + long when; + boolean isDelivery; + boolean isRead; + List timestamps; + + JsonReceiptMessage(SignalServiceReceiptMessage receiptMessage) { + + this.when = receiptMessage.getWhen(); + if (receiptMessage.isDeliveryReceipt()) { + this.isDelivery = true; + } + if (receiptMessage.isReadReceipt()) { + this.isRead = true; + } + this.timestamps = receiptMessage.getTimestamps(); + } +} From 62696fbc67aca074fc75ba3476bfb725a6fdb48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Fern=C3=A1ndez=20Vald=C3=A9s?= Date: Sun, 29 Sep 2019 09:03:12 -0400 Subject: [PATCH 0293/2005] Added JsonSyncDataMessage class with destination field. --- .../org/asamk/signal/JsonSyncDataMessage.java | 18 ++++++++++++++++++ .../java/org/asamk/signal/JsonSyncMessage.java | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/asamk/signal/JsonSyncDataMessage.java diff --git a/src/main/java/org/asamk/signal/JsonSyncDataMessage.java b/src/main/java/org/asamk/signal/JsonSyncDataMessage.java new file mode 100644 index 00000000..06c9b32f --- /dev/null +++ b/src/main/java/org/asamk/signal/JsonSyncDataMessage.java @@ -0,0 +1,18 @@ +package org.asamk.signal; + +import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; + +import java.util.ArrayList; +import java.util.List; + +class JsonSyncDataMessage extends JsonDataMessage { + + String destination; + + JsonSyncDataMessage(SentTranscriptMessage transcriptMessage) { + super(transcriptMessage.getMessage()); + if (transcriptMessage.getDestination().isPresent()) { + this.destination = transcriptMessage.getDestination().get(); + } + } +} diff --git a/src/main/java/org/asamk/signal/JsonSyncMessage.java b/src/main/java/org/asamk/signal/JsonSyncMessage.java index febf64a4..6b597c87 100644 --- a/src/main/java/org/asamk/signal/JsonSyncMessage.java +++ b/src/main/java/org/asamk/signal/JsonSyncMessage.java @@ -7,13 +7,13 @@ import java.util.List; class JsonSyncMessage { - JsonDataMessage sentMessage; + JsonSyncDataMessage sentMessage; List blockedNumbers; List readMessages; JsonSyncMessage(SignalServiceSyncMessage syncMessage) { if (syncMessage.getSent().isPresent()) { - this.sentMessage = new JsonDataMessage(syncMessage.getSent().get().getMessage()); + this.sentMessage = new JsonSyncDataMessage(syncMessage.getSent().get()); } if (syncMessage.getBlockedList().isPresent()) { this.blockedNumbers = syncMessage.getBlockedList().get().getNumbers(); From c53bb132eb9759ee6541d037d2eeadc848c2c89b Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 16 Oct 2019 19:15:14 +0200 Subject: [PATCH 0294/2005] Include profile key in outgoing messages --- src/main/java/org/asamk/signal/manager/Manager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index ee999194..202a7d44 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -564,6 +564,7 @@ public class Manager implements Signal { if (attachments != null) { messageBuilder.withAttachments(Utils.getSignalServiceAttachments(attachments)); } + messageBuilder.withProfileKey(account.getProfileKey()); sendMessageLegacy(messageBuilder, recipients); } From abb6ebc910d7d46ec3644d49d6bd456fcccfa1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Bobbio?= Date: Tue, 24 Sep 2019 19:22:14 +0200 Subject: [PATCH 0295/2005] Add commands to update profile name and avatar Two new commands are added `setProfileName` and `setProfileAvatar` which allow to update the name and avatar visible by other users for the current profiles. Closes #227 --- man/signal-cli.1.adoc | 13 ++++++ .../org/asamk/signal/commands/Commands.java | 2 + .../commands/SetProfileAvatarCommand.java | 40 +++++++++++++++++++ .../commands/SetProfileNameCommand.java | 38 ++++++++++++++++++ .../org/asamk/signal/manager/Manager.java | 9 +++++ .../java/org/asamk/signal/manager/Utils.java | 11 +++++ 6 files changed, 113 insertions(+) create mode 100644 src/main/java/org/asamk/signal/commands/SetProfileAvatarCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/SetProfileNameCommand.java diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index d8e0cb90..e72c3d02 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -208,6 +208,19 @@ number:: Specify the safety number or fingerprint of the key, only use this option if you have verified the fingerprint. +setProfileName +-------------- +Update the name visible by message recipients for the current users. + +name:: + New name visible by message recipients. + +setProfileAvatar +---------------- +Update the avatar visible by message recipients for the current users. + +avatar:: + Path to the new avatar visible by message recipients. daemon ~~~~~~ diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 6f262fdf..aef34d86 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -20,6 +20,8 @@ public class Commands { addCommand("removeDevice", new RemoveDeviceCommand()); addCommand("removePin", new RemovePinCommand()); addCommand("send", new SendCommand()); + addCommand("setProfileAvatar", new SetProfileAvatarCommand()); + addCommand("setProfileName", new SetProfileNameCommand()); addCommand("setPin", new SetPinCommand()); addCommand("trust", new TrustCommand()); addCommand("unregister", new UnregisterCommand()); diff --git a/src/main/java/org/asamk/signal/commands/SetProfileAvatarCommand.java b/src/main/java/org/asamk/signal/commands/SetProfileAvatarCommand.java new file mode 100644 index 00000000..fae66e36 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SetProfileAvatarCommand.java @@ -0,0 +1,40 @@ +package org.asamk.signal.commands; + +import java.io.IOException; +import java.io.File; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; + +public class SetProfileAvatarCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("avatar") + .help("Path to new profile avatar"); + subparser.help("Set the avatar for this profile"); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + String avatarPath = ns.getString("avatar"); + File avatarFile = new File(avatarPath); + + try { + m.setProfileAvatar(avatarFile); + } catch (IOException e) { + System.err.println("UpdateAccount error: " + e.getMessage()); + return 3; + } + + return 0; + } + +} diff --git a/src/main/java/org/asamk/signal/commands/SetProfileNameCommand.java b/src/main/java/org/asamk/signal/commands/SetProfileNameCommand.java new file mode 100644 index 00000000..761261b9 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SetProfileNameCommand.java @@ -0,0 +1,38 @@ +package org.asamk.signal.commands; + +import java.io.IOException; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; + +public class SetProfileNameCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.addArgument("name") + .help("New profile name"); + subparser.help("Set a new name for this profile"); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + String name = ns.getString("name"); + + try { + m.setProfileName(name); + } catch (IOException e) { + System.err.println("UpdateAccount error: " + e.getMessage()); + return 3; + } + + return 0; + } + +} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 202a7d44..5a26ff56 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -54,6 +54,7 @@ import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureExcept import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.SleepTimer; +import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; @@ -204,6 +205,14 @@ public class Manager implements Signal { accountManager.setAccountAttributes(account.getSignalingKey(), account.getSignalProtocolStore().getLocalRegistrationId(), true, account.getRegistrationLockPin(), getSelfUnidentifiedAccessKey(), false); } + public void setProfileName(String name) throws IOException { + accountManager.setProfileName(account.getProfileKey(), name); + } + + public void setProfileAvatar(File avatar) throws IOException { + accountManager.setProfileAvatar(account.getProfileKey(), Utils.createStreamDetailsFromFile(avatar)); + } + public void unregister() throws IOException { // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // If this is the master device, other users can't send messages to this number anymore. diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index 4012674f..2636a301 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -15,6 +15,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.util.Base64; import java.io.*; @@ -56,6 +57,16 @@ class Utils { return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, null); } + static StreamDetails createStreamDetailsFromFile(File file) throws IOException { + InputStream stream = new FileInputStream(file); + final long size = file.length(); + String mime = Files.probeContentType(file.toPath()); + if (mime == null) { + mime = "application/octet-stream"; + } + return new StreamDetails(stream, mime, size); + } + static CertificateValidator getCertificateValidator() { try { ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BaseConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0); From 958d10fcd1c92e92dc6f1a0493150c9cbfda190d Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 16 Oct 2019 18:28:55 +0200 Subject: [PATCH 0296/2005] Merge profile commands to a single UpdateProfileCommand --- man/signal-cli.1.adoc | 17 ++--- .../org/asamk/signal/commands/Commands.java | 3 +- .../commands/SetProfileAvatarCommand.java | 40 ----------- .../commands/SetProfileNameCommand.java | 38 ---------- .../signal/commands/UpdateProfileCommand.java | 72 +++++++++++++++++++ .../org/asamk/signal/manager/Manager.java | 8 ++- 6 files changed, 89 insertions(+), 89 deletions(-) delete mode 100644 src/main/java/org/asamk/signal/commands/SetProfileAvatarCommand.java delete mode 100644 src/main/java/org/asamk/signal/commands/SetProfileNameCommand.java create mode 100644 src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index e72c3d02..c338bb63 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -208,20 +208,21 @@ number:: Specify the safety number or fingerprint of the key, only use this option if you have verified the fingerprint. -setProfileName +updateProfile -------------- -Update the name visible by message recipients for the current users. +Update the name and/or avatar image visible by message recipients for the current users. +The profile is stored encrypted on the Signal servers. The decryption key is sent +with every outgoing messages (excluding group messages). -name:: +*--name*:: New name visible by message recipients. -setProfileAvatar ----------------- -Update the avatar visible by message recipients for the current users. - -avatar:: +*--avatar*:: Path to the new avatar visible by message recipients. +*--remove-avatar*:: + Remove the avatar visible by message recipients. + daemon ~~~~~~ signal-cli can run in daemon mode and provides an experimental dbus interface. For diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index aef34d86..75efaef2 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -20,13 +20,12 @@ public class Commands { addCommand("removeDevice", new RemoveDeviceCommand()); addCommand("removePin", new RemovePinCommand()); addCommand("send", new SendCommand()); - addCommand("setProfileAvatar", new SetProfileAvatarCommand()); - addCommand("setProfileName", new SetProfileNameCommand()); addCommand("setPin", new SetPinCommand()); addCommand("trust", new TrustCommand()); addCommand("unregister", new UnregisterCommand()); addCommand("updateAccount", new UpdateAccountCommand()); addCommand("updateGroup", new UpdateGroupCommand()); + addCommand("updateProfile", new UpdateProfileCommand()); addCommand("verify", new VerifyCommand()); } diff --git a/src/main/java/org/asamk/signal/commands/SetProfileAvatarCommand.java b/src/main/java/org/asamk/signal/commands/SetProfileAvatarCommand.java deleted file mode 100644 index fae66e36..00000000 --- a/src/main/java/org/asamk/signal/commands/SetProfileAvatarCommand.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.asamk.signal.commands; - -import java.io.IOException; -import java.io.File; - -import net.sourceforge.argparse4j.impl.Arguments; -import net.sourceforge.argparse4j.inf.Namespace; -import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.Manager; - -public class SetProfileAvatarCommand implements LocalCommand { - - @Override - public void attachToSubparser(final Subparser subparser) { - subparser.addArgument("avatar") - .help("Path to new profile avatar"); - subparser.help("Set the avatar for this profile"); - } - - @Override - public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - - String avatarPath = ns.getString("avatar"); - File avatarFile = new File(avatarPath); - - try { - m.setProfileAvatar(avatarFile); - } catch (IOException e) { - System.err.println("UpdateAccount error: " + e.getMessage()); - return 3; - } - - return 0; - } - -} diff --git a/src/main/java/org/asamk/signal/commands/SetProfileNameCommand.java b/src/main/java/org/asamk/signal/commands/SetProfileNameCommand.java deleted file mode 100644 index 761261b9..00000000 --- a/src/main/java/org/asamk/signal/commands/SetProfileNameCommand.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.asamk.signal.commands; - -import java.io.IOException; - -import net.sourceforge.argparse4j.impl.Arguments; -import net.sourceforge.argparse4j.inf.Namespace; -import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.manager.Manager; - -public class SetProfileNameCommand implements LocalCommand { - - @Override - public void attachToSubparser(final Subparser subparser) { - subparser.addArgument("name") - .help("New profile name"); - subparser.help("Set a new name for this profile"); - } - - @Override - public int handleCommand(final Namespace ns, final Manager m) { - if (!m.isRegistered()) { - System.err.println("User is not registered."); - return 1; - } - - String name = ns.getString("name"); - - try { - m.setProfileName(name); - } catch (IOException e) { - System.err.println("UpdateAccount error: " + e.getMessage()); - return 3; - } - - return 0; - } - -} diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java new file mode 100644 index 00000000..b4040d48 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -0,0 +1,72 @@ +package org.asamk.signal.commands; + +import java.io.IOException; +import java.io.File; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.manager.Manager; + +public class UpdateProfileCommand implements LocalCommand { + + @Override + public void attachToSubparser(final Subparser subparser) { + final MutuallyExclusiveGroup avatarOptions = subparser.addMutuallyExclusiveGroup(); + avatarOptions.addArgument("--avatar") + .help("Path to new profile avatar"); + avatarOptions.addArgument("--remove-avatar") + .action(Arguments.storeTrue()); + + subparser.addArgument("--name") + .help("New profile name"); + + subparser.help("Set a name and/or avatar image for the user profile"); + } + + @Override + public int handleCommand(final Namespace ns, final Manager m) { + if (!m.isRegistered()) { + System.err.println("User is not registered."); + return 1; + } + + String name = ns.getString("name"); + + if (name != null) { + try { + m.setProfileName(name); + } catch (IOException e) { + System.err.println("UpdateAccount error: " + e.getMessage()); + return 3; + } + } + + String avatarPath = ns.getString("avatar"); + + if (avatarPath != null) { + File avatarFile = new File(avatarPath); + + try { + m.setProfileAvatar(avatarFile); + } catch (IOException e) { + System.err.println("UpdateAccount error: " + e.getMessage()); + return 3; + } + } + + boolean removeAvatar = ns.getBoolean("remove_avatar"); + + if (removeAvatar) { + try { + m.removeProfileAvatar(); + } catch (IOException e) { + System.err.println("UpdateAccount error: " + e.getMessage()); + return 3; + } + } + + return 0; + } +} diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index 5a26ff56..c0c4a18f 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -210,7 +210,13 @@ public class Manager implements Signal { } public void setProfileAvatar(File avatar) throws IOException { - accountManager.setProfileAvatar(account.getProfileKey(), Utils.createStreamDetailsFromFile(avatar)); + final StreamDetails streamDetails = Utils.createStreamDetailsFromFile(avatar); + accountManager.setProfileAvatar(account.getProfileKey(), streamDetails); + streamDetails.getStream().close(); + } + + public void removeProfileAvatar() throws IOException { + accountManager.setProfileAvatar(account.getProfileKey(), null); } public void unregister() throws IOException { From b2efef4d8c3238b413fe9fe758bd6a5c428498a2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 16 Oct 2019 18:58:20 +0200 Subject: [PATCH 0297/2005] Reformat imports --- .../org/asamk/signal/JsonCallMessage.java | 7 +- .../signal/JsonDbusReceiveMessageHandler.java | 6 +- .../signal/JsonReceiveMessageHandler.java | 1 + .../org/asamk/signal/JsonSyncDataMessage.java | 3 - src/main/java/org/asamk/signal/Main.java | 14 +++- .../asamk/signal/ReceiveMessageHandler.java | 24 ++++++- .../signal/commands/AddDeviceCommand.java | 1 + .../asamk/signal/commands/DaemonCommand.java | 1 + .../asamk/signal/commands/DbusCommand.java | 1 + .../signal/commands/ExtendedDbusCommand.java | 1 + .../asamk/signal/commands/LinkCommand.java | 1 + .../signal/commands/ListDevicesCommand.java | 1 + .../signal/commands/ListGroupsCommand.java | 1 + .../commands/ListIdentitiesCommand.java | 1 + .../asamk/signal/commands/LocalCommand.java | 1 + .../signal/commands/QuitGroupCommand.java | 8 ++- .../asamk/signal/commands/ReceiveCommand.java | 1 + .../signal/commands/RegisterCommand.java | 1 + .../signal/commands/RemoveDeviceCommand.java | 1 + .../signal/commands/RemovePinCommand.java | 1 + .../asamk/signal/commands/SendCommand.java | 9 ++- .../asamk/signal/commands/SetPinCommand.java | 1 + .../asamk/signal/commands/TrustCommand.java | 1 + .../signal/commands/UnregisterCommand.java | 1 + .../signal/commands/UpdateAccountCommand.java | 1 + .../signal/commands/UpdateGroupCommand.java | 7 +- .../signal/commands/UpdateProfileCommand.java | 7 +- .../asamk/signal/commands/VerifyCommand.java | 1 + .../org/asamk/signal/manager/Manager.java | 67 +++++++++++++++++-- .../java/org/asamk/signal/manager/Utils.java | 19 +++++- .../asamk/signal/storage/SignalAccount.java | 5 +- .../storage/contacts/JsonContactsStore.java | 7 +- .../signal/storage/groups/JsonGroupStore.java | 8 ++- .../protocol/JsonIdentityKeyStore.java | 13 +++- .../storage/protocol/JsonPreKeyStore.java | 7 +- .../storage/protocol/JsonSessionStore.java | 13 +++- .../protocol/JsonSignalProtocolStore.java | 1 + .../protocol/JsonSignedPreKeyStore.java | 7 +- .../storage/threads/JsonThreadStore.java | 7 +- .../java/org/asamk/signal/util/IOUtils.java | 4 +- src/main/java/org/asamk/signal/util/Util.java | 1 + 41 files changed, 227 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonCallMessage.java b/src/main/java/org/asamk/signal/JsonCallMessage.java index b10e6f7b..2c8518f9 100644 --- a/src/main/java/org/asamk/signal/JsonCallMessage.java +++ b/src/main/java/org/asamk/signal/JsonCallMessage.java @@ -1,6 +1,11 @@ package org.asamk.signal; -import org.whispersystems.signalservice.api.messages.calls.*; +import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; +import org.whispersystems.signalservice.api.messages.calls.BusyMessage; +import org.whispersystems.signalservice.api.messages.calls.HangupMessage; +import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; +import org.whispersystems.signalservice.api.messages.calls.OfferMessage; +import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; import java.util.List; diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index c0977c0f..6a57e50b 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -4,7 +4,11 @@ import org.asamk.Signal; import org.asamk.signal.manager.Manager; import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; -import org.whispersystems.signalservice.api.messages.*; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index cbfe72bd..1aea2327 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; + import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; diff --git a/src/main/java/org/asamk/signal/JsonSyncDataMessage.java b/src/main/java/org/asamk/signal/JsonSyncDataMessage.java index 06c9b32f..aaf04348 100644 --- a/src/main/java/org/asamk/signal/JsonSyncDataMessage.java +++ b/src/main/java/org/asamk/signal/JsonSyncDataMessage.java @@ -2,9 +2,6 @@ package org.asamk.signal; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; -import java.util.ArrayList; -import java.util.List; - class JsonSyncDataMessage extends JsonDataMessage { String destination; diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index e1343fbf..f0010a44 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -18,9 +18,19 @@ package org.asamk.signal; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.impl.Arguments; -import net.sourceforge.argparse4j.inf.*; +import net.sourceforge.argparse4j.inf.ArgumentParser; +import net.sourceforge.argparse4j.inf.ArgumentParserException; +import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import net.sourceforge.argparse4j.inf.Subparsers; + import org.asamk.Signal; -import org.asamk.signal.commands.*; +import org.asamk.signal.commands.Command; +import org.asamk.signal.commands.Commands; +import org.asamk.signal.commands.DbusCommand; +import org.asamk.signal.commands.ExtendedDbusCommand; +import org.asamk.signal.commands.LocalCommand; import org.asamk.signal.manager.BaseConfig; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.IOUtils; diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index f8c93f52..eeac7cb5 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -5,9 +5,27 @@ import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.Util; -import org.whispersystems.signalservice.api.messages.*; -import org.whispersystems.signalservice.api.messages.calls.*; -import org.whispersystems.signalservice.api.messages.multidevice.*; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; +import org.whispersystems.signalservice.api.messages.calls.AnswerMessage; +import org.whispersystems.signalservice.api.messages.calls.BusyMessage; +import org.whispersystems.signalservice.api.messages.calls.HangupMessage; +import org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage; +import org.whispersystems.signalservice.api.messages.calls.OfferMessage; +import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMessage; +import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; +import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage; +import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; +import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.internal.util.Base64; diff --git a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java index c4402696..dab886d7 100644 --- a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import org.whispersystems.libsignal.InvalidKeyException; diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 9b6c0d63..85fee723 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -3,6 +3,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.DbusReceiveMessageHandler; import org.asamk.signal.JsonDbusReceiveMessageHandler; import org.asamk.signal.manager.Manager; diff --git a/src/main/java/org/asamk/signal/commands/DbusCommand.java b/src/main/java/org/asamk/signal/commands/DbusCommand.java index 077600e4..4dee75b2 100644 --- a/src/main/java/org/asamk/signal/commands/DbusCommand.java +++ b/src/main/java/org/asamk/signal/commands/DbusCommand.java @@ -1,6 +1,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; + import org.asamk.Signal; public interface DbusCommand extends Command { diff --git a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java index df47994f..b7f70dee 100644 --- a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java +++ b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java @@ -1,6 +1,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; + import org.asamk.Signal; import org.freedesktop.dbus.DBusConnection; diff --git a/src/main/java/org/asamk/signal/commands/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java index 5c7ce403..2a2d4c4b 100644 --- a/src/main/java/org/asamk/signal/commands/LinkCommand.java +++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.UserAlreadyExists; import org.asamk.signal.manager.Manager; import org.whispersystems.libsignal.InvalidKeyException; diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java index e30acd78..d1590d7c 100644 --- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import org.asamk.signal.util.DateUtils; import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 29d136f5..2f8bf411 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -3,6 +3,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.groups.GroupInfo; import org.whispersystems.signalservice.internal.util.Base64; diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index b2f6f31c..14250eea 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.util.Hex; diff --git a/src/main/java/org/asamk/signal/commands/LocalCommand.java b/src/main/java/org/asamk/signal/commands/LocalCommand.java index 9f785802..7bcb1a4b 100644 --- a/src/main/java/org/asamk/signal/commands/LocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/LocalCommand.java @@ -1,6 +1,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; + import org.asamk.signal.manager.Manager; public interface LocalCommand extends Command { diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 1bde1120..6e53cb2a 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.GroupIdFormatException; import org.asamk.signal.GroupNotFoundException; import org.asamk.signal.NotAGroupMemberException; @@ -11,7 +12,12 @@ import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptio import java.io.IOException; -import static org.asamk.signal.util.ErrorUtils.*; +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; +import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; +import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; +import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; +import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class QuitGroupCommand implements LocalCommand { diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 03f3d1b2..9025aa55 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -3,6 +3,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.Signal; import org.asamk.signal.JsonReceiveMessageHandler; import org.asamk.signal.ReceiveMessageHandler; diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index 546578b6..2e2b7c4f 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -3,6 +3,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java index 9f5787ae..1e2343e7 100644 --- a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java index 491c26bd..39eb1f1b 100644 --- a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import org.whispersystems.libsignal.util.guava.Optional; diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 176c2d92..7320d76d 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -3,6 +3,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.Signal; import org.asamk.signal.AttachmentInvalidException; import org.asamk.signal.GroupIdFormatException; @@ -18,7 +19,13 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; -import static org.asamk.signal.util.ErrorUtils.*; +import static org.asamk.signal.util.ErrorUtils.handleAssertionError; +import static org.asamk.signal.util.ErrorUtils.handleDBusExecutionException; +import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; +import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; +import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; +import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class SendCommand implements DbusCommand { diff --git a/src/main/java/org/asamk/signal/commands/SetPinCommand.java b/src/main/java/org/asamk/signal/commands/SetPinCommand.java index de4e28ec..9351dad0 100644 --- a/src/main/java/org/asamk/signal/commands/SetPinCommand.java +++ b/src/main/java/org/asamk/signal/commands/SetPinCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import org.whispersystems.libsignal.util.guava.Optional; diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 13fb63d4..f2744545 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -4,6 +4,7 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import org.asamk.signal.util.Hex; diff --git a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java index 3830abe6..7a7616bd 100644 --- a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java index 31964721..79459fe6 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 8f601a68..5778b333 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.Signal; import org.asamk.signal.AttachmentInvalidException; import org.asamk.signal.GroupIdFormatException; @@ -15,7 +16,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import static org.asamk.signal.util.ErrorUtils.*; +import static org.asamk.signal.util.ErrorUtils.handleEncapsulatedExceptions; +import static org.asamk.signal.util.ErrorUtils.handleGroupIdFormatException; +import static org.asamk.signal.util.ErrorUtils.handleGroupNotFoundException; +import static org.asamk.signal.util.ErrorUtils.handleIOException; +import static org.asamk.signal.util.ErrorUtils.handleNotAGroupMemberException; public class UpdateGroupCommand implements DbusCommand { diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index b4040d48..a7b02937 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -1,14 +1,15 @@ package org.asamk.signal.commands; -import java.io.IOException; -import java.io.File; - import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; +import java.io.File; +import java.io.IOException; + public class UpdateProfileCommand implements LocalCommand { @Override diff --git a/src/main/java/org/asamk/signal/commands/VerifyCommand.java b/src/main/java/org/asamk/signal/commands/VerifyCommand.java index aca0d700..9d99c0d2 100644 --- a/src/main/java/org/asamk/signal/commands/VerifyCommand.java +++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; + import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.internal.push.LockedException; diff --git a/src/main/java/org/asamk/signal/manager/Manager.java b/src/main/java/org/asamk/signal/manager/Manager.java index c0c4a18f..5aaf14e3 100644 --- a/src/main/java/org/asamk/signal/manager/Manager.java +++ b/src/main/java/org/asamk/signal/manager/Manager.java @@ -17,7 +17,11 @@ package org.asamk.signal.manager; import org.asamk.Signal; -import org.asamk.signal.*; +import org.asamk.signal.AttachmentInvalidException; +import org.asamk.signal.GroupNotFoundException; +import org.asamk.signal.NotAGroupMemberException; +import org.asamk.signal.TrustLevel; +import org.asamk.signal.UserAlreadyExists; import org.asamk.signal.storage.SignalAccount; import org.asamk.signal.storage.contacts.ContactInfo; import org.asamk.signal.storage.groups.GroupInfo; @@ -26,8 +30,22 @@ import org.asamk.signal.storage.protocol.JsonIdentityKeyStore; import org.asamk.signal.storage.threads.ThreadInfo; import org.asamk.signal.util.IOUtils; import org.asamk.signal.util.Util; -import org.signal.libsignal.metadata.*; -import org.whispersystems.libsignal.*; +import org.signal.libsignal.metadata.InvalidMetadataMessageException; +import org.signal.libsignal.metadata.InvalidMetadataVersionException; +import org.signal.libsignal.metadata.ProtocolDuplicateMessageException; +import org.signal.libsignal.metadata.ProtocolInvalidKeyException; +import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException; +import org.signal.libsignal.metadata.ProtocolInvalidMessageException; +import org.signal.libsignal.metadata.ProtocolInvalidVersionException; +import org.signal.libsignal.metadata.ProtocolLegacyMessageException; +import org.signal.libsignal.metadata.ProtocolNoSessionException; +import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; +import org.signal.libsignal.metadata.SelfSendException; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.InvalidVersionException; import org.whispersystems.libsignal.ecc.Curve; import org.whispersystems.libsignal.ecc.ECKeyPair; import org.whispersystems.libsignal.ecc.ECPublicKey; @@ -44,8 +62,26 @@ import org.whispersystems.signalservice.api.crypto.SignalServiceCipher; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.messages.*; -import org.whispersystems.signalservice.api.messages.multidevice.*; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; +import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.push.ContactTokenDetails; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException; @@ -60,12 +96,29 @@ import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.util.Base64; -import java.io.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.URI; import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; diff --git a/src/main/java/org/asamk/signal/manager/Utils.java b/src/main/java/org/asamk/signal/manager/Utils.java index 2636a301..4864dc16 100644 --- a/src/main/java/org/asamk/signal/manager/Utils.java +++ b/src/main/java/org/asamk/signal/manager/Utils.java @@ -18,12 +18,27 @@ import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.StreamDetails; import org.whispersystems.signalservice.internal.util.Base64; -import java.io.*; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.file.Files; -import java.util.*; +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 static org.whispersystems.signalservice.internal.util.Util.isEmpty; diff --git a/src/main/java/org/asamk/signal/storage/SignalAccount.java b/src/main/java/org/asamk/signal/storage/SignalAccount.java index 6af9d460..1c6f320e 100644 --- a/src/main/java/org/asamk/signal/storage/SignalAccount.java +++ b/src/main/java/org/asamk/signal/storage/SignalAccount.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.node.ObjectNode; + import org.asamk.signal.storage.contacts.JsonContactsStore; import org.asamk.signal.storage.groups.JsonGroupStore; import org.asamk.signal.storage.protocol.JsonSignalProtocolStore; @@ -139,7 +140,9 @@ public class SignalAccount { if (node != null) { deviceId = node.asInt(); } - if (rootNode.has("isMultiDevice")) isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean(); + if (rootNode.has("isMultiDevice")) { + isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean(); + } username = Util.getNotNullNode(rootNode, "username").asText(); password = Util.getNotNullNode(rootNode, "password").asText(); JsonNode pinNode = rootNode.get("registrationLockPin"); diff --git a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java index 8d22550b..c10dfbb7 100644 --- a/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java +++ b/src/main/java/org/asamk/signal/storage/contacts/JsonContactsStore.java @@ -3,7 +3,12 @@ package org.asamk.signal.storage.contacts; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; diff --git a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java index 4c5677a6..2eee4eda 100644 --- a/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java +++ b/src/main/java/org/asamk/signal/storage/groups/JsonGroupStore.java @@ -3,9 +3,15 @@ package org.asamk.signal.storage.groups; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; + import org.whispersystems.signalservice.internal.util.Base64; import java.io.IOException; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java index d7049e4a..ce9da228 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonIdentityKeyStore.java @@ -2,7 +2,12 @@ package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + import org.asamk.signal.TrustLevel; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; @@ -12,7 +17,11 @@ import org.whispersystems.libsignal.state.IdentityKeyStore; import org.whispersystems.signalservice.internal.util.Base64; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class JsonIdentityKeyStore implements IdentityKeyStore { diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java index 3065bfde..e1ad4ddf 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonPreKeyStore.java @@ -2,7 +2,12 @@ package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.PreKeyStore; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java index 7f5c6b06..910fe44a 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSessionStore.java @@ -2,14 +2,23 @@ package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SessionStore; import org.whispersystems.signalservice.internal.util.Base64; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; class JsonSessionStore implements SessionStore { diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java index 6fcb052b..65ee4a6e 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignalProtocolStore.java @@ -3,6 +3,7 @@ package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; + import org.asamk.signal.TrustLevel; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; diff --git a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java index c8f4c3cc..eb191eb8 100644 --- a/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java +++ b/src/main/java/org/asamk/signal/storage/protocol/JsonSignedPreKeyStore.java @@ -2,7 +2,12 @@ package org.asamk.signal.storage.protocol; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + import org.whispersystems.libsignal.InvalidKeyIdException; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; diff --git a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java b/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java index 9ee613b8..a4a89ccd 100644 --- a/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java +++ b/src/main/java/org/asamk/signal/storage/threads/JsonThreadStore.java @@ -3,7 +3,12 @@ package org.asamk.signal.storage.threads; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; diff --git a/src/main/java/org/asamk/signal/util/IOUtils.java b/src/main/java/org/asamk/signal/util/IOUtils.java index e1464b1c..434669de 100644 --- a/src/main/java/org/asamk/signal/util/IOUtils.java +++ b/src/main/java/org/asamk/signal/util/IOUtils.java @@ -12,7 +12,9 @@ import java.nio.file.attribute.PosixFilePermissions; import java.util.EnumSet; import java.util.Set; -import static java.nio.file.attribute.PosixFilePermission.*; +import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE; +import static java.nio.file.attribute.PosixFilePermission.OWNER_READ; +import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE; public class IOUtils { diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index e7a68668..f0d39601 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -1,6 +1,7 @@ package org.asamk.signal.util; import com.fasterxml.jackson.databind.JsonNode; + import org.asamk.signal.GroupIdFormatException; import org.whispersystems.signalservice.internal.util.Base64; From 0722ec23612689b61bd4d673bde220d4d58c38e2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 16 Oct 2019 19:06:00 +0200 Subject: [PATCH 0298/2005] Update dependencies --- .idea/codeStyles/Project.xml | 6 ++++++ build.gradle | 4 ++-- src/main/java/org/asamk/signal/manager/Utils.java | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 2bf5e0cf..f26eeb33 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -15,5 +15,11 @@

+ * The monitor is also responsible for sending heartbeats/keep-alive messages to prevent + * timeouts. + */ +public final class SignalWebSocketHealthMonitor implements HealthMonitor { + + private final static Logger logger = LoggerFactory.getLogger(SignalWebSocketHealthMonitor.class); + + private static final long KEEP_ALIVE_SEND_CADENCE = TimeUnit.SECONDS.toMillis(WebSocketConnection.KEEPALIVE_TIMEOUT_SECONDS); + private static final long MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE = KEEP_ALIVE_SEND_CADENCE * 3; + + private SignalWebSocket signalWebSocket; + private final SleepTimer sleepTimer; + + private volatile KeepAliveSender keepAliveSender; + + private final HealthState identified = new HealthState(); + private final HealthState unidentified = new HealthState(); + + public SignalWebSocketHealthMonitor(SleepTimer sleepTimer) { + this.sleepTimer = sleepTimer; + } + + public void monitor(SignalWebSocket signalWebSocket) { + Preconditions.checkNotNull(signalWebSocket); + Preconditions.checkArgument(this.signalWebSocket == null, "monitor can only be called once"); + + this.signalWebSocket = signalWebSocket; + + //noinspection ResultOfMethodCallIgnored + signalWebSocket.getWebSocketState() + .subscribeOn(Schedulers.computation()) + .observeOn(Schedulers.computation()) + .distinctUntilChanged() + .subscribe(s -> onStateChange(s, identified)); + + //noinspection ResultOfMethodCallIgnored + signalWebSocket.getUnidentifiedWebSocketState() + .subscribeOn(Schedulers.computation()) + .observeOn(Schedulers.computation()) + .distinctUntilChanged() + .subscribe(s -> onStateChange(s, unidentified)); + } + + private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) { + switch (connectionState) { + case CONNECTED: + logger.debug("WebSocket is now connected"); + break; + case AUTHENTICATION_FAILED: + logger.debug("WebSocket authentication failed"); + break; + case FAILED: + logger.debug("WebSocket connection failed"); + break; + } + + healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED; + + if (keepAliveSender == null && isKeepAliveNecessary()) { + keepAliveSender = new KeepAliveSender(); + keepAliveSender.start(); + } else if (keepAliveSender != null && !isKeepAliveNecessary()) { + keepAliveSender.shutdown(); + keepAliveSender = null; + } + } + + @Override + public void onKeepAliveResponse(long sentTimestamp, boolean isIdentifiedWebSocket) { + if (isIdentifiedWebSocket) { + identified.lastKeepAliveReceived = System.currentTimeMillis(); + } else { + unidentified.lastKeepAliveReceived = System.currentTimeMillis(); + } + } + + @Override + public void onMessageError(int status, boolean isIdentifiedWebSocket) { + if (status == 409) { + HealthState healthState = (isIdentifiedWebSocket ? identified : unidentified); + if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) { + logger.warn("Received too many mismatch device errors, forcing new websockets."); + signalWebSocket.forceNewWebSockets(); + } + } + } + + private boolean isKeepAliveNecessary() { + return identified.needsKeepAlive || unidentified.needsKeepAlive; + } + + private static class HealthState { + + private final HttpErrorTracker mismatchErrorTracker = new HttpErrorTracker(5, TimeUnit.MINUTES.toMillis(1)); + + private volatile boolean needsKeepAlive; + private volatile long lastKeepAliveReceived; + } + + /** + * Sends periodic heartbeats/keep-alives over both WebSockets to prevent connection timeouts. If + * either WebSocket fails 3 times to get a return heartbeat both are forced to be recreated. + */ + private class KeepAliveSender extends Thread { + + private volatile boolean shouldKeepRunning = true; + + public void run() { + identified.lastKeepAliveReceived = System.currentTimeMillis(); + unidentified.lastKeepAliveReceived = System.currentTimeMillis(); + + while (shouldKeepRunning && isKeepAliveNecessary()) { + try { + sleepTimer.sleep(KEEP_ALIVE_SEND_CADENCE); + + if (shouldKeepRunning && isKeepAliveNecessary()) { + long keepAliveRequiredSinceTime = System.currentTimeMillis() + - MAX_TIME_SINCE_SUCCESSFUL_KEEP_ALIVE; + + if (identified.lastKeepAliveReceived < keepAliveRequiredSinceTime + || unidentified.lastKeepAliveReceived < keepAliveRequiredSinceTime) { + logger.warn("Missed keep alives, identified last: " + + identified.lastKeepAliveReceived + + " unidentified last: " + + unidentified.lastKeepAliveReceived + + " needed by: " + + keepAliveRequiredSinceTime); + signalWebSocket.forceNewWebSockets(); + } else { + signalWebSocket.sendKeepAlive(); + } + } + } catch (Throwable e) { + logger.warn("Error occured in KeepAliveSender, ignoring ...", e); + } + } + } + + public void shutdown() { + shouldKeepRunning = false; + } + } + + private final static class HttpErrorTracker { + + private final long[] timestamps; + private final long errorTimeRange; + + public HttpErrorTracker(int samples, long errorTimeRange) { + this.timestamps = new long[samples]; + this.errorTimeRange = errorTimeRange; + } + + public synchronized boolean addSample(long now) { + long errorsMustBeAfter = now - errorTimeRange; + int count = 1; + int minIndex = 0; + + for (int i = 0; i < timestamps.length; i++) { + if (timestamps[i] < errorsMustBeAfter) { + timestamps[i] = 0; + } else if (timestamps[i] != 0) { + count++; + } + + if (timestamps[i] < timestamps[minIndex]) { + minIndex = i; + } + } + + timestamps[minIndex] = now; + + if (count >= timestamps.length) { + Arrays.fill(timestamps, 0); + return true; + } + return false; + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index 9e0a4375..3567bd7f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -34,7 +34,12 @@ public class ServiceConfig { } catch (Throwable ignored) { zkGroupAvailable = false; } - capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, false); + capabilities = new AccountAttributes.Capabilities(false, + zkGroupAvailable, + false, + zkGroupAvailable, + false, + false); try { TrustStore contactTrustStore = new IasTrustStore(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 34ae4bfa..206994f5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -207,7 +207,7 @@ public class GroupV2Helper { var change = name != null ? groupOperations.createModifyGroupTitle(name) : GroupChange.Actions.newBuilder(); if (description != null) { - change.setModifyDescription(groupOperations.createModifyGroupDescription(description)); + change.setModifyDescription(groupOperations.createModifyGroupDescriptionAction(description)); } if (avatarFile != null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/MessagePipeProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/MessagePipeProvider.java deleted file mode 100644 index 7739928c..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/helper/MessagePipeProvider.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.asamk.signal.manager.helper; - -import org.whispersystems.signalservice.api.SignalServiceMessagePipe; - -public interface MessagePipeProvider { - - SignalServiceMessagePipe getMessagePipe(boolean unidentified); -} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 2676135b..c3c74b0b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -9,14 +9,12 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; -import org.whispersystems.signalservice.internal.util.concurrent.CascadingFuture; -import org.whispersystems.signalservice.internal.util.concurrent.ListenableFuture; +import org.whispersystems.signalservice.api.services.ProfileService; +import org.whispersystems.signalservice.internal.ServiceResponse; import java.io.IOException; -import java.util.Arrays; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; + +import io.reactivex.rxjava3.core.Single; public final class ProfileHelper { @@ -24,7 +22,7 @@ public final class ProfileHelper { private final UnidentifiedAccessProvider unidentifiedAccessProvider; - private final MessagePipeProvider messagePipeProvider; + private final ProfileServiceProvider profileServiceProvider; private final MessageReceiverProvider messageReceiverProvider; @@ -33,13 +31,13 @@ public final class ProfileHelper { public ProfileHelper( final ProfileKeyProvider profileKeyProvider, final UnidentifiedAccessProvider unidentifiedAccessProvider, - final MessagePipeProvider messagePipeProvider, + final ProfileServiceProvider profileServiceProvider, final MessageReceiverProvider messageReceiverProvider, final SignalServiceAddressResolver addressResolver ) { this.profileKeyProvider = profileKeyProvider; this.unidentifiedAccessProvider = unidentifiedAccessProvider; - this.messagePipeProvider = messagePipeProvider; + this.profileServiceProvider = profileServiceProvider; this.messageReceiverProvider = messageReceiverProvider; this.addressResolver = addressResolver; } @@ -48,8 +46,8 @@ public final class ProfileHelper { RecipientId recipientId, SignalServiceProfile.RequestType requestType ) throws IOException { try { - return retrieveProfile(recipientId, requestType).get(10, TimeUnit.SECONDS); - } catch (ExecutionException e) { + return retrieveProfile(recipientId, requestType).blockingGet(); + } catch (RuntimeException e) { if (e.getCause() instanceof PushNetworkException) { throw (PushNetworkException) e.getCause(); } else if (e.getCause() instanceof NotFoundException) { @@ -57,79 +55,55 @@ public final class ProfileHelper { } else { throw new IOException(e); } - } catch (InterruptedException | TimeoutException e) { - throw new PushNetworkException(e); } } - public ListenableFuture retrieveProfile( + public SignalServiceProfile retrieveProfileSync(String username) throws IOException { + return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent()); + } + + public Single retrieveProfile( RecipientId recipientId, SignalServiceProfile.RequestType requestType - ) { + ) throws IOException { var unidentifiedAccess = getUnidentifiedAccess(recipientId); var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId)); final var address = addressResolver.resolveSignalServiceAddress(recipientId); - if (unidentifiedAccess.isPresent()) { - return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address, - profileKey, - unidentifiedAccess, - requestType), - () -> getSocketRetrievalFuture(address, profileKey, unidentifiedAccess, requestType), - () -> getPipeRetrievalFuture(address, profileKey, Optional.absent(), requestType), - () -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)), - e -> !(e instanceof NotFoundException)); - } else { - return new CascadingFuture<>(Arrays.asList(() -> getPipeRetrievalFuture(address, - profileKey, - Optional.absent(), - requestType), () -> getSocketRetrievalFuture(address, profileKey, Optional.absent(), requestType)), - e -> !(e instanceof NotFoundException)); - } + return retrieveProfile(address, profileKey, unidentifiedAccess, requestType); } - private ListenableFuture getPipeRetrievalFuture( + private Single retrieveProfile( SignalServiceAddress address, Optional profileKey, Optional unidentifiedAccess, SignalServiceProfile.RequestType requestType ) throws IOException { - var unidentifiedPipe = messagePipeProvider.getMessagePipe(true); - var pipe = unidentifiedPipe != null && unidentifiedAccess.isPresent() - ? unidentifiedPipe - : messagePipeProvider.getMessagePipe(false); - if (pipe != null) { - try { - return pipe.getProfile(address, profileKey, unidentifiedAccess, requestType); - } catch (NoClassDefFoundError e) { - // Native zkgroup lib not available for ProfileKey - if (!address.getNumber().isPresent()) { - throw new NotFoundException("Can't request profile without number"); - } - var addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber()); - return pipe.getProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType); - } - } + var profileService = profileServiceProvider.getProfileService(); - throw new IOException("No pipe available!"); - } - - private ListenableFuture getSocketRetrievalFuture( - SignalServiceAddress address, - Optional profileKey, - Optional unidentifiedAccess, - SignalServiceProfile.RequestType requestType - ) throws NotFoundException { - var receiver = messageReceiverProvider.getMessageReceiver(); + Single> responseSingle; try { - return receiver.retrieveProfile(address, profileKey, unidentifiedAccess, requestType); + responseSingle = profileService.getProfile(address, profileKey, unidentifiedAccess, requestType); } catch (NoClassDefFoundError e) { // Native zkgroup lib not available for ProfileKey if (!address.getNumber().isPresent()) { throw new NotFoundException("Can't request profile without number"); } var addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber()); - return receiver.retrieveProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType); + responseSingle = profileService.getProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType); } + + return responseSingle.map(pair -> { + var processor = new ProfileService.ProfileResponseProcessor(pair); + if (processor.hasResult()) { + return processor.getResult(); + } else if (processor.notFound()) { + throw new NotFoundException("Profile not found"); + } else { + throw pair.getExecutionError() + .or(pair.getApplicationError()) + .or(new IOException("Unknown error while retrieving profile")); + } + }); } private Optional getUnidentifiedAccess(RecipientId recipientId) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileServiceProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileServiceProvider.java new file mode 100644 index 00000000..4fffb15c --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileServiceProvider.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.helper; + +import org.whispersystems.signalservice.api.services.ProfileService; + +public interface ProfileServiceProvider { + + ProfileService getProfileService(); +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index a19459c0..9b61fbb7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -168,7 +168,11 @@ public class SignalAccount implements Closeable { recipientStore::resolveRecipient, identityKey, registrationId); - signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore); + signalProtocolStore = new SignalProtocolStore(preKeyStore, + signedPreKeyStore, + sessionStore, + identityKeyStore, + this::isMultiDevice); messageCache = new MessageCache(getMessageCachePath(dataPath, username)); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java index 5e504ec6..84923423 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java @@ -12,7 +12,7 @@ import org.whispersystems.libsignal.state.PreKeyStore; import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; -import org.whispersystems.signalservice.api.SignalServiceProtocolStore; +import org.whispersystems.signalservice.api.SignalServiceDataStore; import org.whispersystems.signalservice.api.SignalServiceSessionStore; import org.whispersystems.signalservice.api.push.DistributionId; @@ -20,24 +20,28 @@ import java.util.Collection; import java.util.List; import java.util.Set; import java.util.UUID; +import java.util.function.Supplier; -public class SignalProtocolStore implements SignalServiceProtocolStore { +public class SignalProtocolStore implements SignalServiceDataStore { private final PreKeyStore preKeyStore; private final SignedPreKeyStore signedPreKeyStore; private final SignalServiceSessionStore sessionStore; private final IdentityKeyStore identityKeyStore; + private final Supplier isMultiDevice; public SignalProtocolStore( final PreKeyStore preKeyStore, final SignedPreKeyStore signedPreKeyStore, final SignalServiceSessionStore sessionStore, - final IdentityKeyStore identityKeyStore + final IdentityKeyStore identityKeyStore, + final Supplier isMultiDevice ) { this.preKeyStore = preKeyStore; this.signedPreKeyStore = signedPreKeyStore; this.sessionStore = sessionStore; this.identityKeyStore = identityKeyStore; + this.isMultiDevice = isMultiDevice; } @Override @@ -177,9 +181,12 @@ public class SignalProtocolStore implements SignalServiceProtocolStore { } @Override - public void clearSenderKeySharedWith( - final DistributionId distributionId, final Collection addresses - ) { + public void clearSenderKeySharedWith(final Collection addresses) { // TODO } + + @Override + public boolean isMultiDevice() { + return isMultiDevice.get(); + } } From 28f735741a26c571da59323f0faceee95b055e3b Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 15 Aug 2021 21:20:26 +0200 Subject: [PATCH 0736/2005] Fix JsonRpcLocalCommand interface --- .../java/org/asamk/signal/commands/JsonRpcLocalCommand.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java index 3d2cd035..abe4e74d 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java @@ -13,9 +13,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -public interface JsonRpcLocalCommand extends JsonRpcCommand> { - - void handleCommand(Namespace ns, Manager m) throws CommandException; +public interface JsonRpcLocalCommand extends JsonRpcCommand>, LocalCommand { default TypeReference> getRequestType() { return new TypeReference<>() { From dbfa8bb66b194db87a78f59ae2e4e8041bef1b9a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 15 Aug 2021 21:29:26 +0200 Subject: [PATCH 0737/2005] Update graalvm-config --- graalvm-config-dir/jni-config.json | 12 +- .../predefined-classes-config.json | 8 + graalvm-config-dir/reflect-config.json | 932 +++++++++--------- 3 files changed, 489 insertions(+), 463 deletions(-) create mode 100644 graalvm-config-dir/predefined-classes-config.json diff --git a/graalvm-config-dir/jni-config.json b/graalvm-config-dir/jni-config.json index bb19e040..d18c13e2 100644 --- a/graalvm-config-dir/jni-config.json +++ b/graalvm-config-dir/jni-config.json @@ -1,7 +1,10 @@ [ { "name":"java.lang.ClassLoader", - "methods":[{"name":"getPlatformClassLoader","parameterTypes":[] }] + "methods":[ + {"name":"getPlatformClassLoader","parameterTypes":[] }, + {"name":"loadClass","parameterTypes":["java.lang.String"] } + ] }, { "name":"java.lang.IllegalStateException", @@ -14,6 +17,9 @@ "name":"java.lang.UnsatisfiedLinkError", "methods":[{"name":"","parameterTypes":["java.lang.String"] }] }, +{ + "name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader" +}, { "name":"org.asamk.signal.manager.storage.protocol.SignalProtocolStore", "methods":[ @@ -29,6 +35,10 @@ {"name":"storeSession","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.state.SessionRecord"] } ] }, +{ + "name":"org.graalvm.nativebridge.jni.JNIExceptionWrapperEntryPoints", + "methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }] +}, { "name":"org.whispersystems.libsignal.DuplicateMessageException", "methods":[{"name":"","parameterTypes":["java.lang.String"] }] diff --git a/graalvm-config-dir/predefined-classes-config.json b/graalvm-config-dir/predefined-classes-config.json new file mode 100644 index 00000000..0e79b2c5 --- /dev/null +++ b/graalvm-config-dir/predefined-classes-config.json @@ -0,0 +1,8 @@ +[ + { + "type":"agent-extracted", + "classes":[ + ] + } +] + diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 29ae087d..854013d1 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -144,7 +144,7 @@ { "name":"java.nio.Buffer", "allDeclaredMethods":true, - "fields":[{"name":"address", "allowUnsafeAccess":true}] + "fields":[{"name":"address"}] }, { "name":"java.nio.ByteBuffer", @@ -486,6 +486,13 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true, + "fields":[{"name":"contacts", "allowWrite":true}] +}, { "name":"org.asamk.signal.manager.storage.groups.GroupInfo", "allDeclaredFields":true, @@ -538,13 +545,6 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, -{ - "name":"org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore", - "allDeclaredFields":true, - "allDeclaredMethods":true, - "allDeclaredConstructors":true, - "fields":[{"name":"contacts", "allowWrite":true}] -}, { "name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore", "allDeclaredFields":true, @@ -1048,285 +1048,289 @@ { "name":"org.signal.storageservice.protos.groups.AccessControl", "fields":[ - {"name":"addFromInviteLink_", "allowUnsafeAccess":true}, - {"name":"attributes_", "allowUnsafeAccess":true}, - {"name":"members_", "allowUnsafeAccess":true} + {"name":"addFromInviteLink_"}, + {"name":"attributes_"}, + {"name":"members_"} ] }, { "name":"org.signal.storageservice.protos.groups.AvatarUploadAttributes", "fields":[ - {"name":"acl_", "allowUnsafeAccess":true}, - {"name":"algorithm_", "allowUnsafeAccess":true}, - {"name":"credential_", "allowUnsafeAccess":true}, - {"name":"date_", "allowUnsafeAccess":true}, - {"name":"key_", "allowUnsafeAccess":true}, - {"name":"policy_", "allowUnsafeAccess":true}, - {"name":"signature_", "allowUnsafeAccess":true} + {"name":"acl_"}, + {"name":"algorithm_"}, + {"name":"credential_"}, + {"name":"date_"}, + {"name":"key_"}, + {"name":"policy_"}, + {"name":"signature_"} ] }, { "name":"org.signal.storageservice.protos.groups.Group", "fields":[ - {"name":"accessControl_", "allowUnsafeAccess":true}, - {"name":"avatar_", "allowUnsafeAccess":true}, - {"name":"description_", "allowUnsafeAccess":true}, - {"name":"disappearingMessagesTimer_", "allowUnsafeAccess":true}, - {"name":"inviteLinkPassword_", "allowUnsafeAccess":true}, - {"name":"members_", "allowUnsafeAccess":true}, - {"name":"pendingMembers_", "allowUnsafeAccess":true}, - {"name":"publicKey_", "allowUnsafeAccess":true}, - {"name":"requestingMembers_", "allowUnsafeAccess":true}, - {"name":"revision_", "allowUnsafeAccess":true}, - {"name":"title_", "allowUnsafeAccess":true} + {"name":"accessControl_"}, + {"name":"announcementsOnly_"}, + {"name":"avatar_"}, + {"name":"description_"}, + {"name":"disappearingMessagesTimer_"}, + {"name":"inviteLinkPassword_"}, + {"name":"members_"}, + {"name":"pendingMembers_"}, + {"name":"publicKey_"}, + {"name":"requestingMembers_"}, + {"name":"revision_"}, + {"name":"title_"} ] }, { "name":"org.signal.storageservice.protos.groups.GroupAttributeBlob", "fields":[ - {"name":"contentCase_", "allowUnsafeAccess":true}, - {"name":"content_", "allowUnsafeAccess":true} + {"name":"contentCase_"}, + {"name":"content_"} ] }, { "name":"org.signal.storageservice.protos.groups.GroupChange", "fields":[ - {"name":"actions_", "allowUnsafeAccess":true}, - {"name":"changeEpoch_", "allowUnsafeAccess":true}, - {"name":"serverSignature_", "allowUnsafeAccess":true} + {"name":"actions_"}, + {"name":"changeEpoch_"}, + {"name":"serverSignature_"} ] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions", "fields":[ - {"name":"addMembers_", "allowUnsafeAccess":true}, - {"name":"addPendingMembers_", "allowUnsafeAccess":true}, - {"name":"addRequestingMembers_", "allowUnsafeAccess":true}, - {"name":"deleteMembers_", "allowUnsafeAccess":true}, - {"name":"deletePendingMembers_", "allowUnsafeAccess":true}, - {"name":"deleteRequestingMembers_", "allowUnsafeAccess":true}, - {"name":"modifyAddFromInviteLinkAccess_", "allowUnsafeAccess":true}, - {"name":"modifyAttributesAccess_", "allowUnsafeAccess":true}, - {"name":"modifyAvatar_", "allowUnsafeAccess":true}, - {"name":"modifyDescription_", "allowUnsafeAccess":true}, - {"name":"modifyDisappearingMessagesTimer_", "allowUnsafeAccess":true}, - {"name":"modifyInviteLinkPassword_", "allowUnsafeAccess":true}, - {"name":"modifyMemberAccess_", "allowUnsafeAccess":true}, - {"name":"modifyMemberProfileKeys_", "allowUnsafeAccess":true}, - {"name":"modifyMemberRoles_", "allowUnsafeAccess":true}, - {"name":"modifyTitle_", "allowUnsafeAccess":true}, - {"name":"promotePendingMembers_", "allowUnsafeAccess":true}, - {"name":"promoteRequestingMembers_", "allowUnsafeAccess":true}, - {"name":"revision_", "allowUnsafeAccess":true}, - {"name":"sourceUuid_", "allowUnsafeAccess":true} + {"name":"addMembers_"}, + {"name":"addPendingMembers_"}, + {"name":"addRequestingMembers_"}, + {"name":"deleteMembers_"}, + {"name":"deletePendingMembers_"}, + {"name":"deleteRequestingMembers_"}, + {"name":"modifyAddFromInviteLinkAccess_"}, + {"name":"modifyAnnouncementsOnly_"}, + {"name":"modifyAttributesAccess_"}, + {"name":"modifyAvatar_"}, + {"name":"modifyDescription_"}, + {"name":"modifyDisappearingMessagesTimer_"}, + {"name":"modifyInviteLinkPassword_"}, + {"name":"modifyMemberAccess_"}, + {"name":"modifyMemberProfileKeys_"}, + {"name":"modifyMemberRoles_"}, + {"name":"modifyTitle_"}, + {"name":"promotePendingMembers_"}, + {"name":"promoteRequestingMembers_"}, + {"name":"revision_"}, + {"name":"sourceUuid_"} ] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$AddMemberAction", "fields":[ - {"name":"added_", "allowUnsafeAccess":true}, - {"name":"joinFromInviteLink_", "allowUnsafeAccess":true} + {"name":"added_"}, + {"name":"joinFromInviteLink_"} ] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$AddPendingMemberAction", - "fields":[{"name":"added_", "allowUnsafeAccess":true}] + "fields":[{"name":"added_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$AddRequestingMemberAction", - "fields":[{"name":"added_", "allowUnsafeAccess":true}] + "fields":[{"name":"added_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$DeleteMemberAction", - "fields":[{"name":"deletedUserId_", "allowUnsafeAccess":true}] + "fields":[{"name":"deletedUserId_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$DeletePendingMemberAction", - "fields":[{"name":"deletedUserId_", "allowUnsafeAccess":true}] + "fields":[{"name":"deletedUserId_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$DeleteRequestingMemberAction", - "fields":[{"name":"deletedUserId_", "allowUnsafeAccess":true}] + "fields":[{"name":"deletedUserId_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyAddFromInviteLinkAccessControlAction", - "fields":[{"name":"addFromInviteLinkAccess_", "allowUnsafeAccess":true}] + "fields":[{"name":"addFromInviteLinkAccess_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyAttributesAccessControlAction", - "fields":[{"name":"attributesAccess_", "allowUnsafeAccess":true}] + "fields":[{"name":"attributesAccess_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyDescriptionAction", - "fields":[{"name":"description_", "allowUnsafeAccess":true}] + "fields":[{"name":"description_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyDisappearingMessagesTimerAction", - "fields":[{"name":"timer_", "allowUnsafeAccess":true}] + "fields":[{"name":"timer_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyInviteLinkPasswordAction", - "fields":[{"name":"inviteLinkPassword_", "allowUnsafeAccess":true}] + "fields":[{"name":"inviteLinkPassword_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMemberProfileKeyAction", - "fields":[{"name":"presentation_", "allowUnsafeAccess":true}] + "fields":[{"name":"presentation_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMemberRoleAction", "fields":[ - {"name":"role_", "allowUnsafeAccess":true}, - {"name":"userId_", "allowUnsafeAccess":true} + {"name":"role_"}, + {"name":"userId_"} ] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMembersAccessControlAction", - "fields":[{"name":"membersAccess_", "allowUnsafeAccess":true}] + "fields":[{"name":"membersAccess_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyTitleAction", - "fields":[{"name":"title_", "allowUnsafeAccess":true}] + "fields":[{"name":"title_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$PromotePendingMemberAction", - "fields":[{"name":"presentation_", "allowUnsafeAccess":true}] + "fields":[{"name":"presentation_"}] }, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$PromoteRequestingMemberAction", "fields":[ - {"name":"role_", "allowUnsafeAccess":true}, - {"name":"userId_", "allowUnsafeAccess":true} + {"name":"role_"}, + {"name":"userId_"} ] }, { "name":"org.signal.storageservice.protos.groups.GroupInviteLink", "fields":[ - {"name":"contentsCase_", "allowUnsafeAccess":true}, - {"name":"contents_", "allowUnsafeAccess":true} + {"name":"contentsCase_"}, + {"name":"contents_"} ] }, { "name":"org.signal.storageservice.protos.groups.GroupInviteLink$GroupInviteLinkContentsV1", "fields":[ - {"name":"groupMasterKey_", "allowUnsafeAccess":true}, - {"name":"inviteLinkPassword_", "allowUnsafeAccess":true} + {"name":"groupMasterKey_"}, + {"name":"inviteLinkPassword_"} ] }, { "name":"org.signal.storageservice.protos.groups.Member", "fields":[ - {"name":"joinedAtRevision_", "allowUnsafeAccess":true}, - {"name":"presentation_", "allowUnsafeAccess":true}, - {"name":"profileKey_", "allowUnsafeAccess":true}, - {"name":"role_", "allowUnsafeAccess":true}, - {"name":"userId_", "allowUnsafeAccess":true} + {"name":"joinedAtRevision_"}, + {"name":"presentation_"}, + {"name":"profileKey_"}, + {"name":"role_"}, + {"name":"userId_"} ] }, { "name":"org.signal.storageservice.protos.groups.PendingMember", "fields":[ - {"name":"addedByUserId_", "allowUnsafeAccess":true}, - {"name":"member_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true} + {"name":"addedByUserId_"}, + {"name":"member_"}, + {"name":"timestamp_"} ] }, { "name":"org.signal.storageservice.protos.groups.RequestingMember", "fields":[ - {"name":"presentation_", "allowUnsafeAccess":true}, - {"name":"profileKey_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true}, - {"name":"userId_", "allowUnsafeAccess":true} + {"name":"presentation_"}, + {"name":"profileKey_"}, + {"name":"timestamp_"}, + {"name":"userId_"} ] }, { "name":"org.signal.storageservice.protos.groups.local.DecryptedGroup", "fields":[ - {"name":"accessControl_", "allowUnsafeAccess":true}, - {"name":"avatar_", "allowUnsafeAccess":true}, - {"name":"description_", "allowUnsafeAccess":true}, - {"name":"disappearingMessagesTimer_", "allowUnsafeAccess":true}, - {"name":"inviteLinkPassword_", "allowUnsafeAccess":true}, - {"name":"members_", "allowUnsafeAccess":true}, - {"name":"pendingMembers_", "allowUnsafeAccess":true}, - {"name":"requestingMembers_", "allowUnsafeAccess":true}, - {"name":"revision_", "allowUnsafeAccess":true}, - {"name":"title_", "allowUnsafeAccess":true} + {"name":"accessControl_"}, + {"name":"avatar_"}, + {"name":"description_"}, + {"name":"disappearingMessagesTimer_"}, + {"name":"inviteLinkPassword_"}, + {"name":"isAnnouncementGroup_"}, + {"name":"members_"}, + {"name":"pendingMembers_"}, + {"name":"requestingMembers_"}, + {"name":"revision_"}, + {"name":"title_"} ] }, { "name":"org.signal.storageservice.protos.groups.local.DecryptedGroupChange", "fields":[ - {"name":"deleteMembers_", "allowUnsafeAccess":true}, - {"name":"deletePendingMembers_", "allowUnsafeAccess":true}, - {"name":"deleteRequestingMembers_", "allowUnsafeAccess":true}, - {"name":"editor_", "allowUnsafeAccess":true}, - {"name":"modifiedProfileKeys_", "allowUnsafeAccess":true}, - {"name":"modifyMemberRoles_", "allowUnsafeAccess":true}, - {"name":"newAttributeAccess_", "allowUnsafeAccess":true}, - {"name":"newAvatar_", "allowUnsafeAccess":true}, - {"name":"newDescription_", "allowUnsafeAccess":true}, - {"name":"newInviteLinkAccess_", "allowUnsafeAccess":true}, - {"name":"newInviteLinkPassword_", "allowUnsafeAccess":true}, - {"name":"newMemberAccess_", "allowUnsafeAccess":true}, - {"name":"newMembers_", "allowUnsafeAccess":true}, - {"name":"newPendingMembers_", "allowUnsafeAccess":true}, - {"name":"newRequestingMembers_", "allowUnsafeAccess":true}, - {"name":"newTimer_", "allowUnsafeAccess":true}, - {"name":"newTitle_", "allowUnsafeAccess":true}, - {"name":"promotePendingMembers_", "allowUnsafeAccess":true}, - {"name":"promoteRequestingMembers_", "allowUnsafeAccess":true}, - {"name":"revision_", "allowUnsafeAccess":true} + {"name":"deleteMembers_"}, + {"name":"deletePendingMembers_"}, + {"name":"deleteRequestingMembers_"}, + {"name":"editor_"}, + {"name":"modifiedProfileKeys_"}, + {"name":"modifyMemberRoles_"}, + {"name":"newAttributeAccess_"}, + {"name":"newAvatar_"}, + {"name":"newDescription_"}, + {"name":"newInviteLinkAccess_"}, + {"name":"newInviteLinkPassword_"}, + {"name":"newIsAnnouncementGroup_"}, + {"name":"newMemberAccess_"}, + {"name":"newMembers_"}, + {"name":"newPendingMembers_"}, + {"name":"newRequestingMembers_"}, + {"name":"newTimer_"}, + {"name":"newTitle_"}, + {"name":"promotePendingMembers_"}, + {"name":"promoteRequestingMembers_"}, + {"name":"revision_"} ] }, { "name":"org.signal.storageservice.protos.groups.local.DecryptedMember", "fields":[ - {"name":"joinedAtRevision_", "allowUnsafeAccess":true}, - {"name":"profileKey_", "allowUnsafeAccess":true}, - {"name":"role_", "allowUnsafeAccess":true}, - {"name":"uuid_", "allowUnsafeAccess":true} + {"name":"joinedAtRevision_"}, + {"name":"profileKey_"}, + {"name":"role_"}, + {"name":"uuid_"} ] }, { "name":"org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole", "fields":[ - {"name":"role_", "allowUnsafeAccess":true}, - {"name":"uuid_", "allowUnsafeAccess":true} + {"name":"role_"}, + {"name":"uuid_"} ] }, { "name":"org.signal.storageservice.protos.groups.local.DecryptedPendingMember", "fields":[ - {"name":"addedByUuid_", "allowUnsafeAccess":true}, - {"name":"role_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true}, - {"name":"uuidCipherText_", "allowUnsafeAccess":true}, - {"name":"uuid_", "allowUnsafeAccess":true} + {"name":"addedByUuid_"}, + {"name":"role_"}, + {"name":"timestamp_"}, + {"name":"uuidCipherText_"}, + {"name":"uuid_"} ] }, { "name":"org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval", "fields":[ - {"name":"uuidCipherText_", "allowUnsafeAccess":true}, - {"name":"uuid_", "allowUnsafeAccess":true} + {"name":"uuidCipherText_"}, + {"name":"uuid_"} ] }, { "name":"org.signal.storageservice.protos.groups.local.DecryptedRequestingMember", "fields":[ - {"name":"profileKey_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true}, - {"name":"uuid_", "allowUnsafeAccess":true} + {"name":"profileKey_"}, + {"name":"timestamp_"}, + {"name":"uuid_"} ] }, { "name":"org.signal.storageservice.protos.groups.local.DecryptedString", - "fields":[{"name":"value_", "allowUnsafeAccess":true}] + "fields":[{"name":"value_"}] }, { "name":"org.signal.storageservice.protos.groups.local.DecryptedTimer", - "fields":[{"name":"duration_", "allowUnsafeAccess":true}] + "fields":[{"name":"duration_"}] }, { "name":"org.whispersystems.libsignal.state.IdentityKeyStore", @@ -1504,78 +1508,78 @@ { "name":"org.whispersystems.signalservice.internal.devices.DeviceNameProtos$DeviceName", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"ciphertext_", "allowUnsafeAccess":true}, - {"name":"ephemeralPublic_", "allowUnsafeAccess":true}, - {"name":"syntheticIv_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"ciphertext_"}, + {"name":"ephemeralPublic_"}, + {"name":"syntheticIv_"} ] }, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.BackupRequest", "fields":[ - {"name":"backupId_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"data_", "allowUnsafeAccess":true}, - {"name":"pin_", "allowUnsafeAccess":true}, - {"name":"serviceId_", "allowUnsafeAccess":true}, - {"name":"token_", "allowUnsafeAccess":true}, - {"name":"tries_", "allowUnsafeAccess":true}, - {"name":"validFrom_", "allowUnsafeAccess":true} + {"name":"backupId_"}, + {"name":"bitField0_"}, + {"name":"data_"}, + {"name":"pin_"}, + {"name":"serviceId_"}, + {"name":"token_"}, + {"name":"tries_"}, + {"name":"validFrom_"} ] }, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"status_", "allowUnsafeAccess":true}, - {"name":"token_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"status_"}, + {"name":"token_"} ] }, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.DeleteRequest", "fields":[ - {"name":"backupId_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"serviceId_", "allowUnsafeAccess":true} + {"name":"backupId_"}, + {"name":"bitField0_"}, + {"name":"serviceId_"} ] }, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.Request", "fields":[ - {"name":"backup_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"delete_", "allowUnsafeAccess":true}, - {"name":"restore_", "allowUnsafeAccess":true} + {"name":"backup_"}, + {"name":"bitField0_"}, + {"name":"delete_"}, + {"name":"restore_"} ] }, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.Response", "fields":[ - {"name":"backup_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"delete_", "allowUnsafeAccess":true}, - {"name":"restore_", "allowUnsafeAccess":true} + {"name":"backup_"}, + {"name":"bitField0_"}, + {"name":"delete_"}, + {"name":"restore_"} ] }, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.RestoreRequest", "fields":[ - {"name":"backupId_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"pin_", "allowUnsafeAccess":true}, - {"name":"serviceId_", "allowUnsafeAccess":true}, - {"name":"token_", "allowUnsafeAccess":true}, - {"name":"validFrom_", "allowUnsafeAccess":true} + {"name":"backupId_"}, + {"name":"bitField0_"}, + {"name":"pin_"}, + {"name":"serviceId_"}, + {"name":"token_"}, + {"name":"validFrom_"} ] }, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"data_", "allowUnsafeAccess":true}, - {"name":"status_", "allowUnsafeAccess":true}, - {"name":"token_", "allowUnsafeAccess":true}, - {"name":"tries_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"data_"}, + {"name":"status_"}, + {"name":"token_"}, + {"name":"tries_"} ] }, { @@ -1685,31 +1689,31 @@ { "name":"org.whispersystems.signalservice.internal.push.ProvisioningProtos$ProvisionEnvelope", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"body_", "allowUnsafeAccess":true}, - {"name":"publicKey_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"body_"}, + {"name":"publicKey_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.ProvisioningProtos$ProvisionMessage", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"identityKeyPrivate_", "allowUnsafeAccess":true}, - {"name":"identityKeyPublic_", "allowUnsafeAccess":true}, - {"name":"number_", "allowUnsafeAccess":true}, - {"name":"profileKey_", "allowUnsafeAccess":true}, - {"name":"provisioningCode_", "allowUnsafeAccess":true}, - {"name":"provisioningVersion_", "allowUnsafeAccess":true}, - {"name":"readReceipts_", "allowUnsafeAccess":true}, - {"name":"userAgent_", "allowUnsafeAccess":true}, - {"name":"uuid_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"identityKeyPrivate_"}, + {"name":"identityKeyPublic_"}, + {"name":"number_"}, + {"name":"profileKey_"}, + {"name":"provisioningCode_"}, + {"name":"provisioningVersion_"}, + {"name":"readReceipts_"}, + {"name":"userAgent_"}, + {"name":"uuid_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.ProvisioningProtos$ProvisioningUuid", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"uuid_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"uuid_"} ] }, { @@ -1743,287 +1747,287 @@ { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$AttachmentPointer", "fields":[ - {"name":"attachmentIdentifierCase_", "allowUnsafeAccess":true}, - {"name":"attachmentIdentifier_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"blurHash_", "allowUnsafeAccess":true}, - {"name":"caption_", "allowUnsafeAccess":true}, - {"name":"cdnNumber_", "allowUnsafeAccess":true}, - {"name":"contentType_", "allowUnsafeAccess":true}, - {"name":"digest_", "allowUnsafeAccess":true}, - {"name":"fileName_", "allowUnsafeAccess":true}, - {"name":"flags_", "allowUnsafeAccess":true}, - {"name":"height_", "allowUnsafeAccess":true}, - {"name":"key_", "allowUnsafeAccess":true}, - {"name":"size_", "allowUnsafeAccess":true}, - {"name":"thumbnail_", "allowUnsafeAccess":true}, - {"name":"uploadTimestamp_", "allowUnsafeAccess":true}, - {"name":"width_", "allowUnsafeAccess":true} + {"name":"attachmentIdentifierCase_"}, + {"name":"attachmentIdentifier_"}, + {"name":"bitField0_"}, + {"name":"blurHash_"}, + {"name":"caption_"}, + {"name":"cdnNumber_"}, + {"name":"contentType_"}, + {"name":"digest_"}, + {"name":"fileName_"}, + {"name":"flags_"}, + {"name":"height_"}, + {"name":"key_"}, + {"name":"size_"}, + {"name":"thumbnail_"}, + {"name":"uploadTimestamp_"}, + {"name":"width_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage", "fields":[ - {"name":"answer_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"busy_", "allowUnsafeAccess":true}, - {"name":"destinationDeviceId_", "allowUnsafeAccess":true}, - {"name":"hangup_", "allowUnsafeAccess":true}, - {"name":"iceUpdate_", "allowUnsafeAccess":true}, - {"name":"legacyHangup_", "allowUnsafeAccess":true}, - {"name":"multiRing_", "allowUnsafeAccess":true}, - {"name":"offer_", "allowUnsafeAccess":true}, - {"name":"opaque_", "allowUnsafeAccess":true} + {"name":"answer_"}, + {"name":"bitField0_"}, + {"name":"busy_"}, + {"name":"destinationDeviceId_"}, + {"name":"hangup_"}, + {"name":"iceUpdate_"}, + {"name":"legacyHangup_"}, + {"name":"multiRing_"}, + {"name":"offer_"}, + {"name":"opaque_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$Hangup", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"deviceId_", "allowUnsafeAccess":true}, - {"name":"id_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"deviceId_"}, + {"name":"id_"}, + {"name":"type_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$IceUpdate", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"id_", "allowUnsafeAccess":true}, - {"name":"line_", "allowUnsafeAccess":true}, - {"name":"mid_", "allowUnsafeAccess":true}, - {"name":"opaque_", "allowUnsafeAccess":true}, - {"name":"sdp_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"id_"}, + {"name":"line_"}, + {"name":"mid_"}, + {"name":"opaque_"}, + {"name":"sdp_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$Offer", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"id_", "allowUnsafeAccess":true}, - {"name":"opaque_", "allowUnsafeAccess":true}, - {"name":"sdp_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"id_"}, + {"name":"opaque_"}, + {"name":"sdp_"}, + {"name":"type_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$Opaque", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"data_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"data_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$ContactDetails", "fields":[ - {"name":"archived_", "allowUnsafeAccess":true}, - {"name":"avatar_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"blocked_", "allowUnsafeAccess":true}, - {"name":"color_", "allowUnsafeAccess":true}, - {"name":"expireTimer_", "allowUnsafeAccess":true}, - {"name":"inboxPosition_", "allowUnsafeAccess":true}, - {"name":"name_", "allowUnsafeAccess":true}, - {"name":"number_", "allowUnsafeAccess":true}, - {"name":"profileKey_", "allowUnsafeAccess":true}, - {"name":"uuid_", "allowUnsafeAccess":true}, - {"name":"verified_", "allowUnsafeAccess":true} + {"name":"archived_"}, + {"name":"avatar_"}, + {"name":"bitField0_"}, + {"name":"blocked_"}, + {"name":"color_"}, + {"name":"expireTimer_"}, + {"name":"inboxPosition_"}, + {"name":"name_"}, + {"name":"number_"}, + {"name":"profileKey_"}, + {"name":"uuid_"}, + {"name":"verified_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$ContactDetails$Avatar", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"contentType_", "allowUnsafeAccess":true}, - {"name":"length_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"contentType_"}, + {"name":"length_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Content", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"callMessage_", "allowUnsafeAccess":true}, - {"name":"dataMessage_", "allowUnsafeAccess":true}, - {"name":"decryptionErrorMessage_", "allowUnsafeAccess":true}, - {"name":"nullMessage_", "allowUnsafeAccess":true}, - {"name":"receiptMessage_", "allowUnsafeAccess":true}, - {"name":"senderKeyDistributionMessage_", "allowUnsafeAccess":true}, - {"name":"syncMessage_", "allowUnsafeAccess":true}, - {"name":"typingMessage_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"callMessage_"}, + {"name":"dataMessage_"}, + {"name":"decryptionErrorMessage_"}, + {"name":"nullMessage_"}, + {"name":"receiptMessage_"}, + {"name":"senderKeyDistributionMessage_"}, + {"name":"syncMessage_"}, + {"name":"typingMessage_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage", "fields":[ - {"name":"attachments_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"bodyRanges_", "allowUnsafeAccess":true}, - {"name":"body_", "allowUnsafeAccess":true}, - {"name":"contact_", "allowUnsafeAccess":true}, - {"name":"delete_", "allowUnsafeAccess":true}, - {"name":"expireTimer_", "allowUnsafeAccess":true}, - {"name":"flags_", "allowUnsafeAccess":true}, - {"name":"groupCallUpdate_", "allowUnsafeAccess":true}, - {"name":"groupV2_", "allowUnsafeAccess":true}, - {"name":"group_", "allowUnsafeAccess":true}, - {"name":"isViewOnce_", "allowUnsafeAccess":true}, - {"name":"payment_", "allowUnsafeAccess":true}, - {"name":"preview_", "allowUnsafeAccess":true}, - {"name":"profileKey_", "allowUnsafeAccess":true}, - {"name":"quote_", "allowUnsafeAccess":true}, - {"name":"reaction_", "allowUnsafeAccess":true}, - {"name":"requiredProtocolVersion_", "allowUnsafeAccess":true}, - {"name":"sticker_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true} + {"name":"attachments_"}, + {"name":"bitField0_"}, + {"name":"bodyRanges_"}, + {"name":"body_"}, + {"name":"contact_"}, + {"name":"delete_"}, + {"name":"expireTimer_"}, + {"name":"flags_"}, + {"name":"groupCallUpdate_"}, + {"name":"groupV2_"}, + {"name":"group_"}, + {"name":"isViewOnce_"}, + {"name":"payment_"}, + {"name":"preview_"}, + {"name":"profileKey_"}, + {"name":"quote_"}, + {"name":"reaction_"}, + {"name":"requiredProtocolVersion_"}, + {"name":"sticker_"}, + {"name":"timestamp_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$BodyRange", "fields":[ - {"name":"associatedValueCase_", "allowUnsafeAccess":true}, - {"name":"associatedValue_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"length_", "allowUnsafeAccess":true}, - {"name":"start_", "allowUnsafeAccess":true} + {"name":"associatedValueCase_"}, + {"name":"associatedValue_"}, + {"name":"bitField0_"}, + {"name":"length_"}, + {"name":"start_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact", "fields":[ - {"name":"address_", "allowUnsafeAccess":true}, - {"name":"avatar_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"email_", "allowUnsafeAccess":true}, - {"name":"name_", "allowUnsafeAccess":true}, - {"name":"number_", "allowUnsafeAccess":true}, - {"name":"organization_", "allowUnsafeAccess":true} + {"name":"address_"}, + {"name":"avatar_"}, + {"name":"bitField0_"}, + {"name":"email_"}, + {"name":"name_"}, + {"name":"number_"}, + {"name":"organization_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Avatar", "fields":[ - {"name":"avatar_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"isProfile_", "allowUnsafeAccess":true} + {"name":"avatar_"}, + {"name":"bitField0_"}, + {"name":"isProfile_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Email", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"label_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true}, - {"name":"value_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"label_"}, + {"name":"type_"}, + {"name":"value_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Name", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"displayName_", "allowUnsafeAccess":true}, - {"name":"familyName_", "allowUnsafeAccess":true}, - {"name":"givenName_", "allowUnsafeAccess":true}, - {"name":"middleName_", "allowUnsafeAccess":true}, - {"name":"prefix_", "allowUnsafeAccess":true}, - {"name":"suffix_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"displayName_"}, + {"name":"familyName_"}, + {"name":"givenName_"}, + {"name":"middleName_"}, + {"name":"prefix_"}, + {"name":"suffix_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Phone", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"label_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true}, - {"name":"value_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"label_"}, + {"name":"type_"}, + {"name":"value_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$PostalAddress", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"city_", "allowUnsafeAccess":true}, - {"name":"country_", "allowUnsafeAccess":true}, - {"name":"label_", "allowUnsafeAccess":true}, - {"name":"neighborhood_", "allowUnsafeAccess":true}, - {"name":"pobox_", "allowUnsafeAccess":true}, - {"name":"postcode_", "allowUnsafeAccess":true}, - {"name":"region_", "allowUnsafeAccess":true}, - {"name":"street_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"city_"}, + {"name":"country_"}, + {"name":"label_"}, + {"name":"neighborhood_"}, + {"name":"pobox_"}, + {"name":"postcode_"}, + {"name":"region_"}, + {"name":"street_"}, + {"name":"type_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Delete", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"targetSentTimestamp_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"targetSentTimestamp_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$GroupCallUpdate", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"eraId_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"eraId_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Preview", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"date_", "allowUnsafeAccess":true}, - {"name":"description_", "allowUnsafeAccess":true}, - {"name":"image_", "allowUnsafeAccess":true}, - {"name":"title_", "allowUnsafeAccess":true}, - {"name":"url_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"date_"}, + {"name":"description_"}, + {"name":"image_"}, + {"name":"title_"}, + {"name":"url_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Quote", "fields":[ - {"name":"attachments_", "allowUnsafeAccess":true}, - {"name":"authorE164_", "allowUnsafeAccess":true}, - {"name":"authorUuid_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"bodyRanges_", "allowUnsafeAccess":true}, - {"name":"id_", "allowUnsafeAccess":true}, - {"name":"text_", "allowUnsafeAccess":true} + {"name":"attachments_"}, + {"name":"authorE164_"}, + {"name":"authorUuid_"}, + {"name":"bitField0_"}, + {"name":"bodyRanges_"}, + {"name":"id_"}, + {"name":"text_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Reaction", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"emoji_", "allowUnsafeAccess":true}, - {"name":"remove_", "allowUnsafeAccess":true}, - {"name":"targetAuthorUuid_", "allowUnsafeAccess":true}, - {"name":"targetSentTimestamp_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"emoji_"}, + {"name":"remove_"}, + {"name":"targetAuthorUuid_"}, + {"name":"targetSentTimestamp_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Sticker", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"data_", "allowUnsafeAccess":true}, - {"name":"emoji_", "allowUnsafeAccess":true}, - {"name":"packId_", "allowUnsafeAccess":true}, - {"name":"packKey_", "allowUnsafeAccess":true}, - {"name":"stickerId_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"data_"}, + {"name":"emoji_"}, + {"name":"packId_"}, + {"name":"packKey_"}, + {"name":"stickerId_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Envelope", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"content_", "allowUnsafeAccess":true}, - {"name":"legacyMessage_", "allowUnsafeAccess":true}, - {"name":"relay_", "allowUnsafeAccess":true}, - {"name":"serverGuid_", "allowUnsafeAccess":true}, - {"name":"serverTimestamp_", "allowUnsafeAccess":true}, - {"name":"sourceDevice_", "allowUnsafeAccess":true}, - {"name":"sourceE164_", "allowUnsafeAccess":true}, - {"name":"sourceUuid_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"content_"}, + {"name":"legacyMessage_"}, + {"name":"relay_"}, + {"name":"serverGuid_"}, + {"name":"serverTimestamp_"}, + {"name":"sourceDevice_"}, + {"name":"sourceE164_"}, + {"name":"sourceUuid_"}, + {"name":"timestamp_"}, + {"name":"type_"} ] }, { @@ -2048,153 +2052,153 @@ { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$GroupContextV2", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"groupChange_", "allowUnsafeAccess":true}, - {"name":"masterKey_", "allowUnsafeAccess":true}, - {"name":"revision_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"groupChange_"}, + {"name":"masterKey_"}, + {"name":"revision_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$NullMessage", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"padding_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"padding_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$ReceiptMessage", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"timestamp_"}, + {"name":"type_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"blocked_", "allowUnsafeAccess":true}, - {"name":"configuration_", "allowUnsafeAccess":true}, - {"name":"contacts_", "allowUnsafeAccess":true}, - {"name":"fetchLatest_", "allowUnsafeAccess":true}, - {"name":"groups_", "allowUnsafeAccess":true}, - {"name":"keys_", "allowUnsafeAccess":true}, - {"name":"messageRequestResponse_", "allowUnsafeAccess":true}, - {"name":"outgoingPayment_", "allowUnsafeAccess":true}, - {"name":"padding_", "allowUnsafeAccess":true}, - {"name":"read_", "allowUnsafeAccess":true}, - {"name":"request_", "allowUnsafeAccess":true}, - {"name":"sent_", "allowUnsafeAccess":true}, - {"name":"stickerPackOperation_", "allowUnsafeAccess":true}, - {"name":"verified_", "allowUnsafeAccess":true}, - {"name":"viewOnceOpen_", "allowUnsafeAccess":true}, - {"name":"viewed_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"blocked_"}, + {"name":"configuration_"}, + {"name":"contacts_"}, + {"name":"fetchLatest_"}, + {"name":"groups_"}, + {"name":"keys_"}, + {"name":"messageRequestResponse_"}, + {"name":"outgoingPayment_"}, + {"name":"padding_"}, + {"name":"read_"}, + {"name":"request_"}, + {"name":"sent_"}, + {"name":"stickerPackOperation_"}, + {"name":"verified_"}, + {"name":"viewOnceOpen_"}, + {"name":"viewed_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Blocked", "fields":[ - {"name":"groupIds_", "allowUnsafeAccess":true}, - {"name":"numbers_", "allowUnsafeAccess":true}, - {"name":"uuids_", "allowUnsafeAccess":true} + {"name":"groupIds_"}, + {"name":"numbers_"}, + {"name":"uuids_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Contacts", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"blob_", "allowUnsafeAccess":true}, - {"name":"complete_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"blob_"}, + {"name":"complete_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$FetchLatest", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"type_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Keys", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"storageService_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"storageService_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Read", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"senderE164_", "allowUnsafeAccess":true}, - {"name":"senderUuid_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"senderE164_"}, + {"name":"senderUuid_"}, + {"name":"timestamp_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Request", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"type_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Sent", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"destinationE164_", "allowUnsafeAccess":true}, - {"name":"destinationUuid_", "allowUnsafeAccess":true}, - {"name":"expirationStartTimestamp_", "allowUnsafeAccess":true}, - {"name":"isRecipientUpdate_", "allowUnsafeAccess":true}, - {"name":"message_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true}, - {"name":"unidentifiedStatus_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"destinationE164_"}, + {"name":"destinationUuid_"}, + {"name":"expirationStartTimestamp_"}, + {"name":"isRecipientUpdate_"}, + {"name":"message_"}, + {"name":"timestamp_"}, + {"name":"unidentifiedStatus_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Sent$UnidentifiedDeliveryStatus", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"destinationE164_", "allowUnsafeAccess":true}, - {"name":"destinationUuid_", "allowUnsafeAccess":true}, - {"name":"unidentified_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"destinationE164_"}, + {"name":"destinationUuid_"}, + {"name":"unidentified_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$StickerPackOperation", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"packId_", "allowUnsafeAccess":true}, - {"name":"packKey_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"packId_"}, + {"name":"packKey_"}, + {"name":"type_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Viewed", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"senderE164_", "allowUnsafeAccess":true}, - {"name":"senderUuid_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"senderE164_"}, + {"name":"senderUuid_"}, + {"name":"timestamp_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$TypingMessage", "fields":[ - {"name":"action_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"groupId_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true} + {"name":"action_"}, + {"name":"bitField0_"}, + {"name":"groupId_"}, + {"name":"timestamp_"} ] }, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Verified", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"destinationE164_", "allowUnsafeAccess":true}, - {"name":"destinationUuid_", "allowUnsafeAccess":true}, - {"name":"identityKey_", "allowUnsafeAccess":true}, - {"name":"nullMessage_", "allowUnsafeAccess":true}, - {"name":"state_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"destinationE164_"}, + {"name":"destinationUuid_"}, + {"name":"identityKey_"}, + {"name":"nullMessage_"}, + {"name":"state_"} ] }, { @@ -2206,34 +2210,34 @@ { "name":"org.whispersystems.signalservice.internal.serialize.protos.AddressProto", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"e164_", "allowUnsafeAccess":true}, - {"name":"relay_", "allowUnsafeAccess":true}, - {"name":"uuid_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"e164_"}, + {"name":"relay_"}, + {"name":"uuid_"} ] }, { "name":"org.whispersystems.signalservice.internal.serialize.protos.MetadataProto", "fields":[ - {"name":"address_", "allowUnsafeAccess":true}, - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"groupId_", "allowUnsafeAccess":true}, - {"name":"needsReceipt_", "allowUnsafeAccess":true}, - {"name":"senderDevice_", "allowUnsafeAccess":true}, - {"name":"serverDeliveredTimestamp_", "allowUnsafeAccess":true}, - {"name":"serverGuid_", "allowUnsafeAccess":true}, - {"name":"serverReceivedTimestamp_", "allowUnsafeAccess":true}, - {"name":"timestamp_", "allowUnsafeAccess":true} + {"name":"address_"}, + {"name":"bitField0_"}, + {"name":"groupId_"}, + {"name":"needsReceipt_"}, + {"name":"senderDevice_"}, + {"name":"serverDeliveredTimestamp_"}, + {"name":"serverGuid_"}, + {"name":"serverReceivedTimestamp_"}, + {"name":"timestamp_"} ] }, { "name":"org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"dataCase_", "allowUnsafeAccess":true}, - {"name":"data_", "allowUnsafeAccess":true}, - {"name":"localAddress_", "allowUnsafeAccess":true}, - {"name":"metadata_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"dataCase_"}, + {"name":"data_"}, + {"name":"localAddress_"}, + {"name":"metadata_"} ] }, { @@ -2251,32 +2255,32 @@ { "name":"org.whispersystems.signalservice.internal.websocket.WebSocketProtos$WebSocketMessage", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"request_", "allowUnsafeAccess":true}, - {"name":"response_", "allowUnsafeAccess":true}, - {"name":"type_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"request_"}, + {"name":"response_"}, + {"name":"type_"} ] }, { "name":"org.whispersystems.signalservice.internal.websocket.WebSocketProtos$WebSocketRequestMessage", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"body_", "allowUnsafeAccess":true}, - {"name":"headers_", "allowUnsafeAccess":true}, - {"name":"id_", "allowUnsafeAccess":true}, - {"name":"path_", "allowUnsafeAccess":true}, - {"name":"verb_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"body_"}, + {"name":"headers_"}, + {"name":"id_"}, + {"name":"path_"}, + {"name":"verb_"} ] }, { "name":"org.whispersystems.signalservice.internal.websocket.WebSocketProtos$WebSocketResponseMessage", "fields":[ - {"name":"bitField0_", "allowUnsafeAccess":true}, - {"name":"body_", "allowUnsafeAccess":true}, - {"name":"headers_", "allowUnsafeAccess":true}, - {"name":"id_", "allowUnsafeAccess":true}, - {"name":"message_", "allowUnsafeAccess":true}, - {"name":"status_", "allowUnsafeAccess":true} + {"name":"bitField0_"}, + {"name":"body_"}, + {"name":"headers_"}, + {"name":"id_"}, + {"name":"message_"}, + {"name":"status_"} ] }, { @@ -2364,6 +2368,10 @@ "name":"sun.security.provider.certpath.PKIXCertPathValidator", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"sun.security.rsa.PSSParameters", + "methods":[{"name":"","parameterTypes":[] }] +}, { "name":"sun.security.rsa.RSAKeyFactory$Legacy", "methods":[{"name":"","parameterTypes":[] }] From 7089912fb0943cf3b69db338a7b89ad47a482a00 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 15 Aug 2021 21:30:21 +0200 Subject: [PATCH 0738/2005] Remove registrationLockV1 code The corresponding endpoint has been removed on the Signal server --- lib/src/main/java/org/asamk/signal/manager/Manager.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 327e876d..0fcb42d2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -472,9 +472,6 @@ public class Manager implements Closeable { account.setRegistrationLockPin(pin.get(), masterKey); } else { - // Remove legacy registration lock - dependencies.getAccountManager().removeRegistrationLockV1(); - // Remove KBS Pin pinHelper.removeRegistrationLockPin(); From 89d498f87d91c600ee13a52d867695932d8265ec Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 16 Aug 2021 19:55:30 +0200 Subject: [PATCH 0739/2005] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f8eee725..9658dcba 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # signal-cli signal-cli is a commandline interface for [libsignal-service-java](https://github.com/WhisperSystems/libsignal-service-java). It supports registering, verifying, sending and receiving messages. -To be able to link to an existing Signal-Android/signal-cli instance, signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java does not yet support [provisioning as a slave device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). +To be able to link to an existing Signal-Android/signal-cli instance, signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java does not yet support [provisioning as a linked device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For registering you need a phone number where you can receive SMS or incoming calls. signal-cli is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)), that can be used to send messages from any programming language that has dbus bindings. From e00eaf10e8820760d179862c736fb0b826c305d8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 18 Aug 2021 19:37:03 +0200 Subject: [PATCH 0740/2005] Adapt User-Agent string to get rate limit challenges --- src/main/java/org/asamk/signal/BaseConfig.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java index afafc7d7..bb8db7d2 100644 --- a/src/main/java/org/asamk/signal/BaseConfig.java +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -5,7 +5,11 @@ public class BaseConfig { public final static String PROJECT_NAME = BaseConfig.class.getPackage().getImplementationTitle(); public final static String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); - final static String USER_AGENT = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + " " + PROJECT_VERSION; + final static String USER_AGENT_SIGNAL_ANDROID = "Signal-Android/5.12.4"; + final static String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null + ? "signal-cli" + : PROJECT_NAME + "/" + PROJECT_VERSION; + final static String USER_AGENT = USER_AGENT_SIGNAL_ANDROID + " " + USER_AGENT_SIGNAL_CLI; private BaseConfig() { } From 47143a90e18b4e95e84d426bdd1425c2d7f39c4e Mon Sep 17 00:00:00 2001 From: technillogue Date: Mon, 16 Aug 2021 21:17:25 -0400 Subject: [PATCH 0741/2005] reflect config to serialize jsonrpc Closes #687 --- graalvm-config-dir/reflect-config.json | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 854013d1..bba5ef48 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -486,6 +486,36 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name": "org.asamk.signal.jsonrpc.JsonRpcBulkMessage", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name": "org.asamk.signal.jsonrpc.JsonRpcException", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name": "org.asamk.signal.jsonrpc.JsonRpcMessage", + "allDeclaredFields": true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name": "org.asamk.signal.jsonrpc.JsonRpcRequest", + "allDeclaredFields": true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.asamk.signal.jsonrpc.JsonRpcResponse", + "allDeclaredFields": true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore", "allDeclaredFields":true, From af292d8f0ea897ea13470489d51c40acca50fc3e Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 20 Aug 2021 18:42:38 +0200 Subject: [PATCH 0742/2005] Refactor command creation --- src/main/java/org/asamk/signal/App.java | 43 +++++---- .../signal/commands/AddDeviceCommand.java | 15 ++-- .../asamk/signal/commands/BlockCommand.java | 15 ++-- .../org/asamk/signal/commands/CliCommand.java | 8 ++ .../org/asamk/signal/commands/Command.java | 8 +- .../org/asamk/signal/commands/Commands.java | 89 +++++++++---------- .../asamk/signal/commands/DaemonCommand.java | 32 +++---- .../asamk/signal/commands/DbusCommand.java | 9 +- .../signal/commands/ExtendedDbusCommand.java | 7 +- .../signal/commands/GetUserStatusCommand.java | 17 ++-- .../signal/commands/JoinGroupCommand.java | 14 +-- .../asamk/signal/commands/JsonRpcCommand.java | 9 +- .../commands/JsonRpcDispatcherCommand.java | 36 ++++---- .../signal/commands/JsonRpcLocalCommand.java | 12 +-- .../asamk/signal/commands/LinkCommand.java | 13 +-- .../signal/commands/ListContactsCommand.java | 12 +-- .../signal/commands/ListDevicesCommand.java | 13 +-- .../signal/commands/ListGroupsCommand.java | 18 ++-- .../commands/ListIdentitiesCommand.java | 13 +-- .../asamk/signal/commands/LocalCommand.java | 5 +- .../signal/commands/MultiLocalCommand.java | 11 ++- .../signal/commands/ProvisioningCommand.java | 5 +- .../signal/commands/QuitGroupCommand.java | 17 ++-- .../asamk/signal/commands/ReceiveCommand.java | 25 +++--- .../signal/commands/RegisterCommand.java | 8 +- .../signal/commands/RegistrationCommand.java | 2 +- .../signal/commands/RemoteDeleteCommand.java | 24 ++--- .../signal/commands/RemoveDeviceCommand.java | 11 ++- .../signal/commands/RemovePinCommand.java | 11 ++- .../asamk/signal/commands/SendCommand.java | 27 +++--- .../signal/commands/SendContactsCommand.java | 11 ++- .../signal/commands/SendReactionCommand.java | 24 ++--- .../commands/SendSyncRequestCommand.java | 11 ++- .../signal/commands/SendTypingCommand.java | 11 ++- .../asamk/signal/commands/SetPinCommand.java | 11 ++- .../asamk/signal/commands/TrustCommand.java | 11 ++- .../asamk/signal/commands/UnblockCommand.java | 11 ++- .../signal/commands/UnregisterCommand.java | 11 ++- .../signal/commands/UpdateAccountCommand.java | 11 ++- .../signal/commands/UpdateContactCommand.java | 11 ++- .../signal/commands/UpdateGroupCommand.java | 23 +++-- .../signal/commands/UpdateProfileCommand.java | 11 ++- .../commands/UploadStickerPackCommand.java | 13 +-- .../asamk/signal/commands/VerifyCommand.java | 8 +- .../asamk/signal/commands/VersionCommand.java | 11 +-- 45 files changed, 436 insertions(+), 282 deletions(-) create mode 100644 src/main/java/org/asamk/signal/commands/CliCommand.java diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 49172f80..12227a8e 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -96,7 +96,7 @@ public class App { : new PlainTextWriterImpl(System.out); var commandKey = ns.getString("command"); - var command = Commands.getCommand(commandKey, outputWriter); + var command = Commands.getCommand(commandKey); if (command == null) { throw new UserErrorException("Command not implemented!"); } @@ -111,7 +111,7 @@ public class App { final var useDbusSystem = ns.getBoolean("dbus-system"); if (useDbus || useDbusSystem) { // If username is null, it will connect to the default object path - initDbusClient(command, username, useDbusSystem); + initDbusClient(command, username, useDbusSystem, outputWriter); return; } @@ -142,7 +142,7 @@ public class App { throw new UserErrorException("You cannot specify a username (phone number) when linking"); } - handleProvisioningCommand((ProvisioningCommand) command, dataPath, serviceEnvironment); + handleProvisioningCommand((ProvisioningCommand) command, dataPath, serviceEnvironment, outputWriter); return; } @@ -150,7 +150,11 @@ public class App { var usernames = Manager.getAllLocalUsernames(dataPath); if (command instanceof MultiLocalCommand) { - handleMultiLocalCommand((MultiLocalCommand) command, dataPath, serviceEnvironment, usernames); + handleMultiLocalCommand((MultiLocalCommand) command, + dataPath, + serviceEnvironment, + usernames, + outputWriter); return; } @@ -175,14 +179,17 @@ public class App { throw new UserErrorException("Command only works via dbus"); } - handleLocalCommand((LocalCommand) command, username, dataPath, serviceEnvironment); + handleLocalCommand((LocalCommand) command, username, dataPath, serviceEnvironment, outputWriter); } private void handleProvisioningCommand( - final ProvisioningCommand command, final File dataPath, final ServiceEnvironment serviceEnvironment + final ProvisioningCommand command, + final File dataPath, + final ServiceEnvironment serviceEnvironment, + final OutputWriter outputWriter ) throws CommandException { var pm = ProvisioningManager.init(dataPath, serviceEnvironment, BaseConfig.USER_AGENT); - command.handleCommand(ns, pm); + command.handleCommand(ns, pm, outputWriter); } private void handleRegistrationCommand( @@ -212,10 +219,11 @@ public class App { final LocalCommand command, final String username, final File dataPath, - final ServiceEnvironment serviceEnvironment + final ServiceEnvironment serviceEnvironment, + final OutputWriter outputWriter ) throws CommandException { try (var m = loadManager(username, dataPath, serviceEnvironment)) { - command.handleCommand(ns, m); + command.handleCommand(ns, m, outputWriter); } catch (IOException e) { logger.warn("Cleanup failed", e); } @@ -225,7 +233,8 @@ public class App { final MultiLocalCommand command, final File dataPath, final ServiceEnvironment serviceEnvironment, - final List usernames + final List usernames, + final OutputWriter outputWriter ) throws CommandException { final var managers = new ArrayList(); for (String u : usernames) { @@ -246,7 +255,7 @@ public class App { public RegistrationManager getNewRegistrationManager(String username) throws IOException { return RegistrationManager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT); } - }); + }, outputWriter); for (var m : managers) { try { @@ -286,7 +295,7 @@ public class App { } private void initDbusClient( - final Command command, final String username, final boolean systemBus + final Command command, final String username, final boolean systemBus, final OutputWriter outputWriter ) throws CommandException { try { DBusConnection.DBusBusType busType; @@ -300,7 +309,7 @@ public class App { DbusConfig.getObjectPath(username), Signal.class); - handleCommand(command, ts, dBusConn); + handleCommand(command, ts, dBusConn, outputWriter); } } catch (DBusException | IOException e) { logger.error("Dbus client failed", e); @@ -308,11 +317,13 @@ public class App { } } - private void handleCommand(Command command, Signal ts, DBusConnection dBusConn) throws CommandException { + private void handleCommand( + Command command, Signal ts, DBusConnection dBusConn, OutputWriter outputWriter + ) throws CommandException { if (command instanceof ExtendedDbusCommand) { - ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn); + ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn, outputWriter); } else if (command instanceof DbusCommand) { - ((DbusCommand) command).handleCommand(ns, ts); + ((DbusCommand) command).handleCommand(ns, ts, outputWriter); } else { throw new UserErrorException("Command is not yet implemented via dbus"); } diff --git a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java index 31b3c7ef..1616a01f 100644 --- a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java @@ -21,18 +21,23 @@ public class AddDeviceCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(AddDeviceCommand.class); - public static void attachToSubparser(final Subparser subparser) { + @Override + public String getName() { + return "addDevice"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Link another device to this device. Only works, if this is the master device."); subparser.addArgument("--uri") .required(true) .help("Specify the uri contained in the QR code shown by the new device."); } - public AddDeviceCommand(final OutputWriter outputWriter) { - } - @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { try { m.addDeviceLink(new URI(ns.getString("uri"))); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 7229c2e1..0710a7e5 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -19,17 +19,22 @@ public class BlockCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(BlockCommand.class); - public static void attachToSubparser(final Subparser subparser) { + @Override + public String getName() { + return "block"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Block the given contacts or groups (no messages will be received)"); subparser.addArgument("contact").help("Contact number").nargs("*"); subparser.addArgument("-g", "--group-id", "--group").help("Group ID").nargs("*"); } - public BlockCommand(final OutputWriter outputWriter) { - } - @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { for (var contactNumber : ns.getList("contact")) { try { m.setContactBlocked(contactNumber, true); diff --git a/src/main/java/org/asamk/signal/commands/CliCommand.java b/src/main/java/org/asamk/signal/commands/CliCommand.java new file mode 100644 index 00000000..b5a60d3c --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/CliCommand.java @@ -0,0 +1,8 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Subparser; + +public interface CliCommand extends Command { + + void attachToSubparser(Subparser subparser); +} diff --git a/src/main/java/org/asamk/signal/commands/Command.java b/src/main/java/org/asamk/signal/commands/Command.java index 9d054616..2e4c761d 100644 --- a/src/main/java/org/asamk/signal/commands/Command.java +++ b/src/main/java/org/asamk/signal/commands/Command.java @@ -2,11 +2,13 @@ package org.asamk.signal.commands; import org.asamk.signal.OutputType; -import java.util.Set; +import java.util.List; public interface Command { - default Set getSupportedOutputTypes() { - return Set.of(OutputType.PLAIN_TEXT); + String getName(); + + default List getSupportedOutputTypes() { + return List.of(OutputType.PLAIN_TEXT); } } diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 33caf8ba..d46d6b8b 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -1,74 +1,65 @@ package org.asamk.signal.commands; -import org.asamk.signal.OutputWriter; - import java.util.HashMap; import java.util.Map; import java.util.TreeMap; public class Commands { - private static final Map commands = new HashMap<>(); + private static final Map commands = new HashMap<>(); private static final Map commandSubparserAttacher = new TreeMap<>(); static { - addCommand("addDevice", AddDeviceCommand::new, AddDeviceCommand::attachToSubparser); - addCommand("block", BlockCommand::new, BlockCommand::attachToSubparser); - addCommand("daemon", DaemonCommand::new, DaemonCommand::attachToSubparser); - addCommand("getUserStatus", GetUserStatusCommand::new, GetUserStatusCommand::attachToSubparser); - addCommand("jsonRpc", JsonRpcDispatcherCommand::new, JsonRpcDispatcherCommand::attachToSubparser); - addCommand("link", LinkCommand::new, LinkCommand::attachToSubparser); - addCommand("listContacts", ListContactsCommand::new, ListContactsCommand::attachToSubparser); - addCommand("listDevices", ListDevicesCommand::new, ListDevicesCommand::attachToSubparser); - addCommand("listGroups", ListGroupsCommand::new, ListGroupsCommand::attachToSubparser); - addCommand("listIdentities", ListIdentitiesCommand::new, ListIdentitiesCommand::attachToSubparser); - addCommand("joinGroup", JoinGroupCommand::new, JoinGroupCommand::attachToSubparser); - addCommand("quitGroup", QuitGroupCommand::new, QuitGroupCommand::attachToSubparser); - addCommand("receive", ReceiveCommand::new, ReceiveCommand::attachToSubparser); - addCommand("register", RegisterCommand::new, RegisterCommand::attachToSubparser); - addCommand("removeDevice", RemoveDeviceCommand::new, RemoveDeviceCommand::attachToSubparser); - addCommand("remoteDelete", RemoteDeleteCommand::new, RemoteDeleteCommand::attachToSubparser); - addCommand("removePin", RemovePinCommand::new, RemovePinCommand::attachToSubparser); - addCommand("send", SendCommand::new, SendCommand::attachToSubparser); - addCommand("sendContacts", SendContactsCommand::new, SendContactsCommand::attachToSubparser); - addCommand("sendReaction", SendReactionCommand::new, SendReactionCommand::attachToSubparser); - addCommand("sendSyncRequest", SendSyncRequestCommand::new, SendSyncRequestCommand::attachToSubparser); - addCommand("sendTyping", SendTypingCommand::new, SendTypingCommand::attachToSubparser); - addCommand("setPin", SetPinCommand::new, SetPinCommand::attachToSubparser); - addCommand("trust", TrustCommand::new, TrustCommand::attachToSubparser); - addCommand("unblock", UnblockCommand::new, UnblockCommand::attachToSubparser); - addCommand("unregister", UnregisterCommand::new, UnregisterCommand::attachToSubparser); - addCommand("updateAccount", UpdateAccountCommand::new, UpdateAccountCommand::attachToSubparser); - addCommand("updateContact", UpdateContactCommand::new, UpdateContactCommand::attachToSubparser); - addCommand("updateGroup", UpdateGroupCommand::new, UpdateGroupCommand::attachToSubparser); - addCommand("updateProfile", UpdateProfileCommand::new, UpdateProfileCommand::attachToSubparser); - addCommand("uploadStickerPack", UploadStickerPackCommand::new, UploadStickerPackCommand::attachToSubparser); - addCommand("verify", VerifyCommand::new, VerifyCommand::attachToSubparser); - addCommand("version", VersionCommand::new, null); + addCommand(new AddDeviceCommand()); + addCommand(new BlockCommand()); + addCommand(new DaemonCommand()); + addCommand(new GetUserStatusCommand()); + addCommand(new JoinGroupCommand()); + addCommand(new JsonRpcDispatcherCommand()); + addCommand(new LinkCommand()); + addCommand(new ListContactsCommand()); + addCommand(new ListDevicesCommand()); + addCommand(new ListGroupsCommand()); + addCommand(new ListIdentitiesCommand()); + addCommand(new QuitGroupCommand()); + addCommand(new ReceiveCommand()); + addCommand(new RegisterCommand()); + addCommand(new RemoveDeviceCommand()); + addCommand(new RemoteDeleteCommand()); + addCommand(new RemovePinCommand()); + addCommand(new SendCommand()); + addCommand(new SendContactsCommand()); + addCommand(new SendReactionCommand()); + addCommand(new SendSyncRequestCommand()); + addCommand(new SendTypingCommand()); + addCommand(new SetPinCommand()); + addCommand(new TrustCommand()); + addCommand(new UnblockCommand()); + addCommand(new UnregisterCommand()); + addCommand(new UpdateAccountCommand()); + addCommand(new UpdateContactCommand()); + addCommand(new UpdateGroupCommand()); + addCommand(new UpdateProfileCommand()); + addCommand(new UploadStickerPackCommand()); + addCommand(new VerifyCommand()); + addCommand(new VersionCommand()); } public static Map getCommandSubparserAttachers() { return commandSubparserAttacher; } - public static Command getCommand(String commandKey, OutputWriter outputWriter) { + public static Command getCommand(String commandKey) { if (!commands.containsKey(commandKey)) { return null; } - return commands.get(commandKey).constructCommand(outputWriter); + return commands.get(commandKey); } - private static void addCommand( - String name, CommandConstructor commandConstructor, SubparserAttacher subparserAttacher - ) { - commands.put(name, commandConstructor); - if (subparserAttacher != null) { - commandSubparserAttacher.put(name, subparserAttacher); + private static void addCommand(Command command) { + commands.put(command.getName(), command); + if (command instanceof CliCommand) { + commandSubparserAttacher.put(command.getName(), ((CliCommand) command)::attachToSubparser); } } - - private interface CommandConstructor { - - Command constructCommand(OutputWriter outputWriter); - } } diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 7b6f243d..49489293 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -23,15 +23,19 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; -import java.util.Set; import java.util.concurrent.TimeUnit; public class DaemonCommand implements MultiLocalCommand { private final static Logger logger = LoggerFactory.getLogger(DaemonCommand.class); - private final OutputWriter outputWriter; - public static void attachToSubparser(final Subparser subparser) { + @Override + public String getName() { + return "daemon"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Run in daemon mode and provide an experimental dbus interface."); subparser.addArgument("--system") .action(Arguments.storeTrue()) @@ -41,17 +45,15 @@ public class DaemonCommand implements MultiLocalCommand { .action(Arguments.storeTrue()); } - public DaemonCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public List getSupportedOutputTypes() { + return List.of(OutputType.PLAIN_TEXT, OutputType.JSON); } @Override - public Set getSupportedOutputTypes() { - return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON); - } - - @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); DBusConnection.DBusBusType busType; @@ -63,7 +65,7 @@ public class DaemonCommand implements MultiLocalCommand { try (var conn = DBusConnection.getConnection(busType)) { var objectPath = DbusConfig.getObjectPath(); - var t = run(conn, objectPath, m, ignoreAttachments); + var t = run(conn, objectPath, m, outputWriter, ignoreAttachments); conn.requestBusName(DbusConfig.getBusname()); @@ -79,7 +81,7 @@ public class DaemonCommand implements MultiLocalCommand { @Override public void handleCommand( - final Namespace ns, final List managers, SignalCreator c + final Namespace ns, final List managers, final SignalCreator c, final OutputWriter outputWriter ) throws CommandException { boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); @@ -94,7 +96,7 @@ public class DaemonCommand implements MultiLocalCommand { final var signalControl = new DbusSignalControlImpl(c, m -> { try { final var objectPath = DbusConfig.getObjectPath(m.getUsername()); - return run(conn, objectPath, m, ignoreAttachments); + return run(conn, objectPath, m, outputWriter, ignoreAttachments); } catch (DBusException e) { logger.error("Failed to export object", e); return null; @@ -116,7 +118,7 @@ public class DaemonCommand implements MultiLocalCommand { } private Thread run( - DBusConnection conn, String objectPath, Manager m, boolean ignoreAttachments + DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter, boolean ignoreAttachments ) throws DBusException { conn.exportObject(new DbusSignalImpl(m, objectPath)); diff --git a/src/main/java/org/asamk/signal/commands/DbusCommand.java b/src/main/java/org/asamk/signal/commands/DbusCommand.java index 1a949c81..9f676a39 100644 --- a/src/main/java/org/asamk/signal/commands/DbusCommand.java +++ b/src/main/java/org/asamk/signal/commands/DbusCommand.java @@ -3,15 +3,18 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import org.asamk.Signal; +import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; public interface DbusCommand extends LocalCommand { - void handleCommand(Namespace ns, Signal signal) throws CommandException; + void handleCommand(Namespace ns, Signal signal, OutputWriter outputWriter) throws CommandException; - default void handleCommand(final Namespace ns, final Manager m) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null)); + default void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); } } diff --git a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java index 1d454f4d..444e4cb6 100644 --- a/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java +++ b/src/main/java/org/asamk/signal/commands/ExtendedDbusCommand.java @@ -3,10 +3,13 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import org.asamk.Signal; +import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.freedesktop.dbus.connections.impl.DBusConnection; -public interface ExtendedDbusCommand extends Command { +public interface ExtendedDbusCommand extends CliCommand { - void handleCommand(Namespace ns, Signal signal, DBusConnection dbusconnection) throws CommandException; + void handleCommand( + Namespace ns, Signal signal, DBusConnection dbusconnection, final OutputWriter outputWriter + ) throws CommandException; } diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index 91e6e47c..05bbea47 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -20,19 +20,22 @@ import java.util.stream.Collectors; public class GetUserStatusCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(GetUserStatusCommand.class); - private final OutputWriter outputWriter; - public static void attachToSubparser(final Subparser subparser) { + @Override + public String getName() { + return "getUserStatus"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Check if the specified phone number/s have been registered"); subparser.addArgument("number").help("Phone number").nargs("+"); } - public GetUserStatusCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; - } - @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { // Get a map of registration statuses Map registered; try { diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index e5a872e0..543d9cc7 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -23,19 +23,21 @@ import static org.asamk.signal.util.ErrorUtils.handleSendMessageResults; public class JoinGroupCommand implements JsonRpcLocalCommand { - private final OutputWriter outputWriter; - - public JoinGroupCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "joinGroup"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Join a group via an invitation link."); subparser.addArgument("--uri").required(true).help("Specify the uri with the group invitation link."); } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { final GroupInviteLinkUrl linkUrl; var uri = ns.getString("uri"); try { diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcCommand.java index 394b0f8b..940a89da 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcCommand.java @@ -3,10 +3,11 @@ package org.asamk.signal.commands; import com.fasterxml.jackson.core.type.TypeReference; import org.asamk.signal.OutputType; +import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; -import java.util.Set; +import java.util.List; public interface JsonRpcCommand extends Command { @@ -14,9 +15,9 @@ public interface JsonRpcCommand extends Command { return null; } - void handleCommand(T request, Manager m) throws CommandException; + void handleCommand(T request, Manager m, OutputWriter outputWriter) throws CommandException; - default Set getSupportedOutputTypes() { - return Set.of(OutputType.JSON); + default List getSupportedOutputTypes() { + return List.of(OutputType.JSON); } } diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java index 4ca90628..6b5361e5 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java @@ -31,8 +31,8 @@ import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; +import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; public class JsonRpcDispatcherCommand implements LocalCommand { @@ -43,26 +43,28 @@ public class JsonRpcDispatcherCommand implements LocalCommand { private static final int IO_ERROR = -3; private static final int UNTRUSTED_KEY_ERROR = -4; - private final OutputWriter outputWriter; + @Override + public String getName() { + return "jsonRpc"; + } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Take commands from standard input as line-delimited JSON RPC while receiving messages."); subparser.addArgument("--ignore-attachments") .help("Don’t download attachments of received messages.") .action(Arguments.storeTrue()); } - public JsonRpcDispatcherCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public List getSupportedOutputTypes() { + return List.of(OutputType.JSON); } @Override - public Set getSupportedOutputTypes() { - return Set.of(OutputType.JSON); - } - - @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { final boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); final var objectMapper = Util.createJsonObjectMapper(); @@ -104,7 +106,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand { result[0] = s; }; - var command = Commands.getCommand(method, commandOutputWriter); + var command = Commands.getCommand(method); if (!(command instanceof JsonRpcCommand)) { throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.METHOD_NOT_FOUND, "Method not implemented", @@ -112,7 +114,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand { } try { - parseParamsAndRunCommand(m, objectMapper, params, (JsonRpcCommand) command); + parseParamsAndRunCommand(m, objectMapper, params, commandOutputWriter, (JsonRpcCommand) command); } catch (JsonMappingException e) { throw new JsonRpcException(new JsonRpcResponse.Error(JsonRpcResponse.Error.INVALID_REQUEST, e.getMessage(), @@ -135,7 +137,11 @@ public class JsonRpcDispatcherCommand implements LocalCommand { } private void parseParamsAndRunCommand( - final Manager m, final ObjectMapper objectMapper, final TreeNode params, final JsonRpcCommand command + final Manager m, + final ObjectMapper objectMapper, + final TreeNode params, + final OutputWriter outputWriter, + final JsonRpcCommand command ) throws CommandException, JsonMappingException { T requestParams = null; final var requestType = command.getRequestType(); @@ -148,7 +154,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand { throw new AssertionError(e); } } - command.handleCommand(requestParams, m); + command.handleCommand(requestParams, m, outputWriter); } private Thread receiveMessages( diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java index abe4e74d..06124ffd 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java @@ -5,13 +5,13 @@ import com.fasterxml.jackson.core.type.TypeReference; import net.sourceforge.argparse4j.inf.Namespace; import org.asamk.signal.OutputType; +import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; import org.asamk.signal.util.Util; import java.util.List; import java.util.Map; -import java.util.Set; public interface JsonRpcLocalCommand extends JsonRpcCommand>, LocalCommand { @@ -20,13 +20,15 @@ public interface JsonRpcLocalCommand extends JsonRpcCommand> }; } - default void handleCommand(Map request, Manager m) throws CommandException { + default void handleCommand( + Map request, Manager m, OutputWriter outputWriter + ) throws CommandException { Namespace commandNamespace = new JsonRpcNamespace(request == null ? Map.of() : request); - handleCommand(commandNamespace, m); + handleCommand(commandNamespace, m, outputWriter); } - default Set getSupportedOutputTypes() { - return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON); + default List getSupportedOutputTypes() { + return List.of(OutputType.PLAIN_TEXT, OutputType.JSON); } /** diff --git a/src/main/java/org/asamk/signal/commands/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java index f117436c..9fcaf04d 100644 --- a/src/main/java/org/asamk/signal/commands/LinkCommand.java +++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java @@ -19,19 +19,22 @@ import java.util.concurrent.TimeoutException; public class LinkCommand implements ProvisioningCommand { private final static Logger logger = LoggerFactory.getLogger(LinkCommand.class); - private final OutputWriter outputWriter; - public LinkCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "link"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Link to an existing device, instead of registering a new number."); subparser.addArgument("-n", "--name").help("Specify a name to describe this new device."); } @Override - public void handleCommand(final Namespace ns, final ProvisioningManager m) throws CommandException { + public void handleCommand( + final Namespace ns, final ProvisioningManager m, final OutputWriter outputWriter + ) throws CommandException { final var writer = (PlainTextWriter) outputWriter; var deviceName = ns.getString("name"); diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index f9a5e0c2..aaf0e0b3 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -11,18 +11,18 @@ import static org.asamk.signal.util.Util.getLegacyIdentifier; public class ListContactsCommand implements LocalCommand { - private final OutputWriter outputWriter; - - public ListContactsCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "listContacts"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Show a list of known contacts with names."); } @Override - public void handleCommand(final Namespace ns, final Manager m) { + public void handleCommand(final Namespace ns, final Manager m, final OutputWriter outputWriter) { final var writer = (PlainTextWriter) outputWriter; var contacts = m.getContacts(); diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java index cb2019e2..16f602f1 100644 --- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java @@ -19,18 +19,21 @@ import java.util.List; public class ListDevicesCommand implements LocalCommand { private final static Logger logger = LoggerFactory.getLogger(ListDevicesCommand.class); - private final OutputWriter outputWriter; - public ListDevicesCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "listDevices"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Show a list of linked devices."); } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { final var writer = (PlainTextWriter) outputWriter; List devices; diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index e9da8099..b912263f 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -22,7 +22,13 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(ListGroupsCommand.class); - public static void attachToSubparser(final Subparser subparser) { + @Override + public String getName() { + return "listGroups"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("List group information including names, ids, active status, blocked status and members"); subparser.addArgument("-d", "--detailed") .action(Arguments.storeTrue()) @@ -63,14 +69,10 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { } } - private final OutputWriter outputWriter; - - public ListGroupsCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; - } - @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { final var groups = m.getGroups(); if (outputWriter instanceof JsonWriter) { diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index 2b0b1f96..f996f3b5 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -21,10 +21,10 @@ import java.util.List; public class ListIdentitiesCommand implements LocalCommand { private final static Logger logger = LoggerFactory.getLogger(ListIdentitiesCommand.class); - private final OutputWriter outputWriter; - public ListIdentitiesCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "listIdentities"; } private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, IdentityInfo theirId) { @@ -38,13 +38,16 @@ public class ListIdentitiesCommand implements LocalCommand { digits); } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("List all known identity keys and their trust status, fingerprint and safety number."); subparser.addArgument("-n", "--number").help("Only show identity keys for the given phone number."); } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { final var writer = (PlainTextWriter) outputWriter; var number = ns.getString("number"); diff --git a/src/main/java/org/asamk/signal/commands/LocalCommand.java b/src/main/java/org/asamk/signal/commands/LocalCommand.java index a7c64dc1..b0e9906b 100644 --- a/src/main/java/org/asamk/signal/commands/LocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/LocalCommand.java @@ -2,10 +2,11 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; +import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; -public interface LocalCommand extends Command { +public interface LocalCommand extends CliCommand { - void handleCommand(Namespace ns, Manager m) throws CommandException; + void handleCommand(Namespace ns, Manager m, OutputWriter outputWriter) throws CommandException; } diff --git a/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java b/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java index 58416e50..1c01a6ae 100644 --- a/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/MultiLocalCommand.java @@ -2,6 +2,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; +import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; @@ -9,10 +10,14 @@ import java.util.List; public interface MultiLocalCommand extends LocalCommand { - void handleCommand(Namespace ns, List m, final SignalCreator c) throws CommandException; + void handleCommand( + Namespace ns, List m, SignalCreator c, OutputWriter outputWriter + ) throws CommandException; @Override - default void handleCommand(final Namespace ns, final Manager m) throws CommandException { - handleCommand(ns, List.of(m), null); + default void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + handleCommand(ns, List.of(m), null, outputWriter); } } diff --git a/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java b/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java index 354e4af3..df68de59 100644 --- a/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java +++ b/src/main/java/org/asamk/signal/commands/ProvisioningCommand.java @@ -2,10 +2,11 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; +import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.ProvisioningManager; -public interface ProvisioningCommand extends Command { +public interface ProvisioningCommand extends CliCommand { - void handleCommand(Namespace ns, ProvisioningManager m) throws CommandException; + void handleCommand(Namespace ns, ProvisioningManager m, final OutputWriter outputWriter) throws CommandException; } diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index af5d600c..c5c7472c 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -31,13 +31,14 @@ import static org.asamk.signal.util.ErrorUtils.handleSendMessageResults; public class QuitGroupCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(QuitGroupCommand.class); - private final OutputWriter outputWriter; - public QuitGroupCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "quitGroup"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Send a quit group message to all group members and remove self from member list."); subparser.addArgument("-g", "--group-id", "--group").required(true).help("Specify the recipient group ID."); subparser.addArgument("--delete") @@ -49,7 +50,9 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { final GroupId groupId; try { groupId = Util.decodeGroupId(ns.getString("group-id")); @@ -64,7 +67,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { final var results = m.sendQuitGroupMessage(groupId, groupAdmins == null ? Set.of() : new HashSet<>(groupAdmins)); final var timestamp = results.first(); - outputResult(timestamp); + outputResult(outputWriter, timestamp); handleSendMessageResults(results.second()); } catch (NotAGroupMemberException e) { logger.info("User is not a group member"); @@ -84,7 +87,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { } } - private void outputResult(final long timestamp) { + private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; writer.println("{}", timestamp); diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index c71225e0..82bd5d8f 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -24,16 +24,21 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.Base64; +import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.TimeUnit; public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { private final static Logger logger = LoggerFactory.getLogger(ReceiveCommand.class); - private final OutputWriter outputWriter; - public static void attachToSubparser(final Subparser subparser) { + @Override + public String getName() { + return "receive"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Query the server for new messages."); subparser.addArgument("-t", "--timeout") .type(double.class) @@ -44,17 +49,13 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { .action(Arguments.storeTrue()); } - public ReceiveCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; - } - @Override - public Set getSupportedOutputTypes() { - return Set.of(OutputType.PLAIN_TEXT, OutputType.JSON); + public List getSupportedOutputTypes() { + return List.of(OutputType.PLAIN_TEXT, OutputType.JSON); } public void handleCommand( - final Namespace ns, final Signal signal, DBusConnection dbusconnection + final Namespace ns, final Signal signal, DBusConnection dbusconnection, final OutputWriter outputWriter ) throws CommandException { try { if (outputWriter instanceof JsonWriter) { @@ -137,7 +138,9 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { double timeout = ns.getDouble("timeout"); var returnOnTimeout = true; if (timeout < 0) { diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index cd23c8ab..dad692d2 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; @@ -15,10 +14,13 @@ import java.io.IOException; public class RegisterCommand implements RegistrationCommand { - public RegisterCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "register"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Register a phone number with SMS or voice verification."); subparser.addArgument("-v", "--voice") .help("The verification should be done over voice, not SMS.") diff --git a/src/main/java/org/asamk/signal/commands/RegistrationCommand.java b/src/main/java/org/asamk/signal/commands/RegistrationCommand.java index 425ac71d..ce81845c 100644 --- a/src/main/java/org/asamk/signal/commands/RegistrationCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegistrationCommand.java @@ -5,7 +5,7 @@ import net.sourceforge.argparse4j.inf.Namespace; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.RegistrationManager; -public interface RegistrationCommand extends Command { +public interface RegistrationCommand extends CliCommand { void handleCommand(Namespace ns, RegistrationManager m) throws CommandException; } diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index 37c55fe7..99100407 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -22,13 +22,13 @@ import java.util.Map; public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { - private final OutputWriter outputWriter; - - public RemoteDeleteCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "remoteDelete"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Remotely delete a previously sent message."); subparser.addArgument("-t", "--target-timestamp") .required(true) @@ -39,7 +39,9 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Signal signal) throws CommandException { + public void handleCommand( + final Namespace ns, final Signal signal, final OutputWriter outputWriter + ) throws CommandException { final List recipients = ns.getList("recipient"); final var groupIdString = ns.getString("group-id"); @@ -69,7 +71,7 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } else { timestamp = signal.sendRemoteDeleteMessage(targetTimestamp, recipients); } - outputResult(timestamp); + outputResult(outputWriter, timestamp); } catch (UnknownObject e) { throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); } catch (Signal.Error.InvalidNumber e) { @@ -82,11 +84,13 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null)); + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); } - private void outputResult(final long timestamp) { + private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; writer.println("{}", timestamp); diff --git a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java index 8295acee..1f47e2b3 100644 --- a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java @@ -12,10 +12,13 @@ import java.io.IOException; public class RemoveDeviceCommand implements JsonRpcLocalCommand { - public RemoveDeviceCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "removeDevice"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Remove a linked device."); subparser.addArgument("-d", "--device-id", "--deviceId") .type(int.class) @@ -24,7 +27,9 @@ public class RemoveDeviceCommand implements JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { try { int deviceId = ns.getInt("device-id"); m.removeLinkedDevices(deviceId); diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java index 6f335179..42ca8880 100644 --- a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java @@ -15,15 +15,20 @@ import java.io.IOException; public class RemovePinCommand implements JsonRpcLocalCommand { - public RemovePinCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "removePin"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Remove the registration lock pin."); } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { try { m.setRegistrationLockPin(Optional.absent()); } catch (UnauthenticatedResponseException e) { diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 8459118c..dbb02248 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -30,13 +30,14 @@ import java.util.Map; public class SendCommand implements DbusCommand, JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(SendCommand.class); - private final OutputWriter outputWriter; - public SendCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "send"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Send a message to another user or group."); subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); final var mutuallyExclusiveGroup = subparser.addMutuallyExclusiveGroup(); @@ -53,7 +54,9 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Signal signal) throws CommandException { + public void handleCommand( + final Namespace ns, final Signal signal, final OutputWriter outputWriter + ) throws CommandException { final List recipients = ns.getList("recipient"); final var isEndSession = ns.getBoolean("end-session"); final var groupIdString = ns.getString("group-id"); @@ -106,7 +109,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { try { var timestamp = signal.sendGroupMessage(messageText, attachments, groupId); - outputResult(timestamp); + outputResult(outputWriter, timestamp); return; } catch (DBusExecutionException e) { throw new UnexpectedErrorException("Failed to send group message: " + e.getMessage()); @@ -116,7 +119,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { if (isNoteToSelf) { try { var timestamp = signal.sendNoteToSelfMessage(messageText, attachments); - outputResult(timestamp); + outputResult(outputWriter, timestamp); return; } catch (Signal.Error.UntrustedIdentity e) { throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage()); @@ -127,7 +130,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { try { var timestamp = signal.sendMessage(messageText, attachments, recipients); - outputResult(timestamp); + outputResult(outputWriter, timestamp); } catch (UnknownObject e) { throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); } catch (Signal.Error.UntrustedIdentity e) { @@ -137,7 +140,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { } } - private void outputResult(final long timestamp) { + private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; writer.println("{}", timestamp); @@ -148,7 +151,9 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null)); + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); } } diff --git a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java index edd5db34..0fcae322 100644 --- a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java @@ -14,15 +14,20 @@ import java.io.IOException; public class SendContactsCommand implements JsonRpcLocalCommand { - public SendContactsCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "sendContacts"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Send a synchronization message with the local contacts list to all linked devices."); } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { try { m.sendContacts(); } catch (UntrustedIdentityException e) { diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 44423749..fedded42 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -23,13 +23,13 @@ import java.util.Map; public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { - private final OutputWriter outputWriter; - - public SendReactionCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "sendReaction"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Send reaction to a previously received or sent message."); subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID."); subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); @@ -47,7 +47,9 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Signal signal) throws CommandException { + public void handleCommand( + final Namespace ns, final Signal signal, final OutputWriter outputWriter + ) throws CommandException { final List recipients = ns.getList("recipient"); final var groupIdString = ns.getString("group-id"); @@ -80,7 +82,7 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } else { timestamp = signal.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, recipients); } - outputResult(timestamp); + outputResult(outputWriter, timestamp); } catch (UnknownObject e) { throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); } catch (Signal.Error.InvalidNumber e) { @@ -93,11 +95,13 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null)); + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); } - private void outputResult(final long timestamp) { + private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; writer.println("{}", timestamp); diff --git a/src/main/java/org/asamk/signal/commands/SendSyncRequestCommand.java b/src/main/java/org/asamk/signal/commands/SendSyncRequestCommand.java index 4072be15..e9a2f94e 100644 --- a/src/main/java/org/asamk/signal/commands/SendSyncRequestCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendSyncRequestCommand.java @@ -12,15 +12,20 @@ import java.io.IOException; public class SendSyncRequestCommand implements JsonRpcLocalCommand { - public SendSyncRequestCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "sendSyncRequest"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Send a synchronization request message to master device (for group, contacts, ...)."); } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { try { m.requestAllSyncData(); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index cb4a4f50..d5a68604 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -22,10 +22,13 @@ import java.util.HashSet; public class SendTypingCommand implements JsonRpcLocalCommand { - public SendTypingCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "sendTyping"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help( "Send typing message to trigger a typing indicator for the recipient. Indicator will be shown for 15seconds unless a typing STOP message is sent first."); subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID."); @@ -34,7 +37,9 @@ public class SendTypingCommand implements JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { final var recipients = ns.getList("recipient"); final var groupIdString = ns.getString("group-id"); diff --git a/src/main/java/org/asamk/signal/commands/SetPinCommand.java b/src/main/java/org/asamk/signal/commands/SetPinCommand.java index 3c018cbc..3636a8b1 100644 --- a/src/main/java/org/asamk/signal/commands/SetPinCommand.java +++ b/src/main/java/org/asamk/signal/commands/SetPinCommand.java @@ -15,17 +15,22 @@ import java.io.IOException; public class SetPinCommand implements JsonRpcLocalCommand { - public SetPinCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "setPin"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Set a registration lock pin, to prevent others from registering this number."); subparser.addArgument("pin") .help("The registration lock PIN, that will be required for new registrations (resets after 7 days of inactivity)"); } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { try { var registrationLockPin = ns.getString("pin"); m.setRegistrationLockPin(Optional.of(registrationLockPin)); diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 62f5190c..78f22c19 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -15,10 +15,13 @@ import java.util.Locale; public class TrustCommand implements JsonRpcLocalCommand { - public TrustCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "trust"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Set the trust level of a given number."); subparser.addArgument("number").help("Specify the phone number, for which to set the trust.").required(true); var mutTrust = subparser.addMutuallyExclusiveGroup(); @@ -30,7 +33,9 @@ public class TrustCommand implements JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { var number = ns.getString("number"); if (ns.getBoolean("trust-all-known-keys")) { boolean res; diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 6388aeee..830147bc 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -19,17 +19,22 @@ public class UnblockCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(UnblockCommand.class); - public UnblockCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "unblock"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Unblock the given contacts or groups (messages will be received again)"); subparser.addArgument("contact").help("Contact number").nargs("*"); subparser.addArgument("-g", "--group-id", "--group").help("Group ID").nargs("*"); } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { for (var contactNumber : ns.getList("contact")) { try { m.setContactBlocked(contactNumber, false); diff --git a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java index dbb8b535..cf09c480 100644 --- a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java @@ -13,10 +13,13 @@ import java.io.IOException; public class UnregisterCommand implements LocalCommand { - public UnregisterCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "unregister"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Unregister the current device from the signal server."); subparser.addArgument("--delete-account") .help("Delete account completely from server. CAUTION: Only do this if you won't use this number again!") @@ -24,7 +27,9 @@ public class UnregisterCommand implements LocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { try { if (ns.getBoolean("delete-account")) { m.deleteAccount(); diff --git a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java index 96b90e41..f2ed6a98 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java @@ -12,15 +12,20 @@ import java.io.IOException; public class UpdateAccountCommand implements JsonRpcLocalCommand { - public UpdateAccountCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "updateAccount"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Update the account attributes on the signal server."); } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { try { m.updateAccountAttributes(); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index 687619b0..462ba8d2 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -15,10 +15,13 @@ import java.io.IOException; public class UpdateContactCommand implements JsonRpcLocalCommand { - public UpdateContactCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "updateContact"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Update the details of a given contact"); subparser.addArgument("number").help("Contact number"); subparser.addArgument("-n", "--name").help("New contact name"); @@ -26,7 +29,9 @@ public class UpdateContactCommand implements JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { var number = ns.getString("number"); try { diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 0aa2cf1e..397d1fe3 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -35,13 +35,14 @@ import java.util.List; public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(UpdateGroupCommand.class); - private final OutputWriter outputWriter; - public UpdateGroupCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "updateGroup"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Create or update a group."); subparser.addArgument("-g", "--group-id", "--group").help("Specify the group ID."); subparser.addArgument("-n", "--name").help("Specify the new group name."); @@ -107,7 +108,9 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { GroupId groupId = null; final var groupIdString = ns.getString("group-id"); if (groupIdString != null) { @@ -163,7 +166,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { timestamp = results.first(); ErrorUtils.handleSendMessageResults(results.second()); } - outputResult(timestamp, isNewGroup ? groupId : null); + outputResult(outputWriter, timestamp, isNewGroup ? groupId : null); } catch (AttachmentInvalidException e) { throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); } catch (GroupNotFoundException e) { @@ -178,7 +181,9 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Signal signal) throws CommandException { + public void handleCommand( + final Namespace ns, final Signal signal, final OutputWriter outputWriter + ) throws CommandException { byte[] groupId = null; if (ns.getString("group-id") != null) { try { @@ -209,7 +214,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { try { var newGroupId = signal.updateGroup(groupId, groupName, groupMembers, groupAvatar); if (groupId.length != newGroupId.length) { - outputResult(null, GroupId.unknownVersion(newGroupId)); + outputResult(outputWriter, null, GroupId.unknownVersion(newGroupId)); } } catch (Signal.Error.AttachmentInvalid e) { throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); @@ -218,7 +223,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } } - private void outputResult(final Long timestamp, final GroupId groupId) { + private void outputResult(final OutputWriter outputWriter, final Long timestamp, final GroupId groupId) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; if (groupId != null) { diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index ff9e8996..15c29e85 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -15,10 +15,13 @@ import java.io.IOException; public class UpdateProfileCommand implements JsonRpcLocalCommand { - public UpdateProfileCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "updateProfile"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Set a name, about and avatar image for the user profile"); subparser.addArgument("--given-name", "--name").help("New profile (given) name"); subparser.addArgument("--family-name").help("New profile family name (optional)"); @@ -31,7 +34,9 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand { } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { var givenName = ns.getString("given-name"); var familyName = ns.getString("family-name"); var about = ns.getString("about"); diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java index 993e37c4..a5839238 100644 --- a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -19,20 +19,23 @@ import java.io.IOException; public class UploadStickerPackCommand implements LocalCommand { private final static Logger logger = LoggerFactory.getLogger(UploadStickerPackCommand.class); - private final OutputWriter outputWriter; - public UploadStickerPackCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "uploadStickerPack"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Upload a new sticker pack, consisting of a manifest file and the stickers images."); subparser.addArgument("path") .help("The path of the manifest.json or a zip file containing the sticker pack you wish to upload."); } @Override - public void handleCommand(final Namespace ns, final Manager m) throws CommandException { + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { final var writer = (PlainTextWriter) outputWriter; var path = new File(ns.getString("path")); diff --git a/src/main/java/org/asamk/signal/commands/VerifyCommand.java b/src/main/java/org/asamk/signal/commands/VerifyCommand.java index f32139bd..2f9388ba 100644 --- a/src/main/java/org/asamk/signal/commands/VerifyCommand.java +++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java @@ -3,7 +3,6 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; @@ -17,10 +16,13 @@ import java.io.IOException; public class VerifyCommand implements RegistrationCommand { - public VerifyCommand(final OutputWriter outputWriter) { + @Override + public String getName() { + return "verify"; } - public static void attachToSubparser(final Subparser subparser) { + @Override + public void attachToSubparser(final Subparser subparser) { subparser.help("Verify the number using the code received via SMS or voice."); subparser.addArgument("verificationCode").help("The verification code you received via sms or voice call."); subparser.addArgument("-p", "--pin").help("The registration lock PIN, that was set by the user (Optional)"); diff --git a/src/main/java/org/asamk/signal/commands/VersionCommand.java b/src/main/java/org/asamk/signal/commands/VersionCommand.java index 1b6d6477..fd1e6fb2 100644 --- a/src/main/java/org/asamk/signal/commands/VersionCommand.java +++ b/src/main/java/org/asamk/signal/commands/VersionCommand.java @@ -10,14 +10,15 @@ import java.util.Map; public class VersionCommand implements JsonRpcCommand { - private final OutputWriter outputWriter; - - public VersionCommand(final OutputWriter outputWriter) { - this.outputWriter = outputWriter; + @Override + public String getName() { + return "version"; } @Override - public void handleCommand(final Void request, final Manager m) throws CommandException { + public void handleCommand( + final Void request, final Manager m, final OutputWriter outputWriter + ) throws CommandException { final var jsonWriter = (JsonWriter) outputWriter; jsonWriter.write(Map.of("version", BaseConfig.PROJECT_VERSION)); } From ef2a013db35f6c5ac65546857df27b584e176307 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 20 Aug 2021 18:43:54 +0200 Subject: [PATCH 0743/2005] Let commands specify their own default output if none is provided by the user --- src/main/java/org/asamk/signal/App.java | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 12227a8e..6b31e2bc 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -67,8 +67,7 @@ public class App { parser.addArgument("-o", "--output") .help("Choose to output in plain text or JSON") - .type(Arguments.enumStringType(OutputType.class)) - .setDefault(OutputType.PLAIN_TEXT); + .type(Arguments.enumStringType(OutputType.class)); parser.addArgument("--service-environment") .help("Choose the server environment to use.") @@ -90,19 +89,22 @@ public class App { } public void init() throws CommandException { - var outputType = ns.get("output"); - var outputWriter = outputType == OutputType.JSON - ? new JsonWriterImpl(System.out) - : new PlainTextWriterImpl(System.out); - var commandKey = ns.getString("command"); var command = Commands.getCommand(commandKey); if (command == null) { throw new UserErrorException("Command not implemented!"); } - if (!command.getSupportedOutputTypes().contains(outputType)) { - throw new UserErrorException("Command doesn't support output type " + outputType.toString()); + var outputTypeInput = ns.get("output"); + var outputType = outputTypeInput == null + ? command.getSupportedOutputTypes().stream().findFirst().orElse(null) + : outputTypeInput; + var outputWriter = outputType == null + ? null + : outputType == OutputType.JSON ? new JsonWriterImpl(System.out) : new PlainTextWriterImpl(System.out); + + if (outputWriter != null && !command.getSupportedOutputTypes().contains(outputType)) { + throw new UserErrorException("Command doesn't support output type " + outputType); } var username = ns.getString("username"); From 19f7b5d78d29544829b25f1cea1ebc24c151f1c3 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 20 Aug 2021 19:04:35 +0200 Subject: [PATCH 0744/2005] Log a debug message when dropping json rpc response for request without id --- src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java b/src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java index 67fced0d..0a7017be 100644 --- a/src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java +++ b/src/main/java/org/asamk/signal/jsonrpc/JsonRpcReader.java @@ -70,10 +70,17 @@ public class JsonRpcReader { final var result = requestHandler.apply(request.getMethod(), request.getParams()); if (request.getId() != null) { return JsonRpcResponse.forSuccess(result, request.getId()); + } else { + logger.debug("Command '{}' succeeded but client didn't specify an id, dropping response", + request.getMethod()); } } catch (JsonRpcException e) { if (request.getId() != null) { return JsonRpcResponse.forError(e.getError(), request.getId()); + } else { + logger.debug("Command '{}' failed but client didn't specify an id, dropping error: {}", + request.getMethod(), + e.getMessage()); } } return null; From b77d8206615d991ab31753fd15a8fa894335eb74 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 20 Aug 2021 20:03:59 +0200 Subject: [PATCH 0745/2005] Handle changed identity key correctly when sending message Fixes #686 --- .../org/asamk/signal/manager/Manager.java | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 0fcb42d2..a2152fac 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1594,14 +1594,7 @@ public class Manager implements Closeable { () -> false); for (var r : result) { - if (r.getIdentityFailure() != null) { - final var recipientId = resolveRecipient(r.getAddress()); - final var newIdentity = account.getIdentityKeyStore() - .saveIdentity(recipientId, r.getIdentityFailure().getIdentityKey(), new Date()); - if (newIdentity) { - account.getSessionStore().archiveSessions(recipientId); - } - } + handlePossibleIdentityFailure(r); } return new Pair<>(timestamp, result); @@ -1617,7 +1610,9 @@ public class Manager implements Closeable { final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; messageBuilder.withExpiration(expirationTime); message = messageBuilder.build(); - results.add(sendMessage(recipientId, message)); + final var result = sendMessage(recipientId, message); + handlePossibleIdentityFailure(result); + results.add(result); } return new Pair<>(timestamp, results); } @@ -1630,6 +1625,23 @@ public class Manager implements Closeable { } } + private void handlePossibleIdentityFailure(final SendMessageResult r) { + if (r.getIdentityFailure() != null) { + final var recipientId = resolveRecipient(r.getAddress()); + final var identityKey = r.getIdentityFailure().getIdentityKey(); + if (identityKey != null) { + final var newIdentity = account.getIdentityKeyStore() + .saveIdentity(recipientId, identityKey, new Date()); + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } + } else { + // Retrieve profile to get the current identity key from the server + retrieveEncryptedProfile(recipientId); + } + } + } + private Pair sendSelfMessage( SignalServiceDataMessage.Builder messageBuilder ) throws IOException { From 893b7f7f9daf698d5424f5f9b1a14c383457b431 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 Aug 2021 15:02:14 +0200 Subject: [PATCH 0746/2005] Refactor message sending --- .../org/asamk/signal/manager/Manager.java | 390 +++++------------- .../signal/manager/helper/GroupProvider.java | 9 + .../helper/IdentityFailureHandler.java | 9 + .../RecipientRegistrationRefresher.java | 10 + .../signal/manager/helper/SendHelper.java | 277 +++++++++++++ .../signal/commands/SendContactsCommand.java | 4 - 6 files changed, 404 insertions(+), 295 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/GroupProvider.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/IdentityFailureHandler.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/RecipientRegistrationRefresher.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index a2152fac..c4b69665 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -33,6 +33,7 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; +import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.jobs.Context; import org.asamk.signal.manager.jobs.Job; @@ -86,7 +87,6 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.InvalidMessageStructureException; import org.whispersystems.signalservice.api.SignalSessionLock; -import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; @@ -111,7 +111,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; @@ -120,7 +119,6 @@ import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.ConflictException; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; -import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -179,10 +177,11 @@ public class Manager implements Closeable { private final ExecutorService executor = Executors.newCachedThreadPool(); - private final UnidentifiedAccessHelper unidentifiedAccessHelper; private final ProfileHelper profileHelper; private final GroupV2Helper groupV2Helper; private final PinHelper pinHelper; + private final SendHelper sendHelper; + private final AvatarStore avatarStore; private final AttachmentStore attachmentStore; private final StickerPackStore stickerPackStore; @@ -218,7 +217,7 @@ public class Manager implements Closeable { sessionLock); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); - this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, + final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, account.getProfileStore()::getProfileKey, this::getRecipientProfile, this::getSenderCertificate); @@ -237,6 +236,14 @@ public class Manager implements Closeable { this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + this.sendHelper = new SendHelper(account, + dependencies, + unidentifiedAccessHelper, + this::resolveSignalServiceAddress, + this::resolveRecipient, + this::handleIdentityFailure, + this::getGroup, + this::refreshRegisteredUser); } public String getUsername() { @@ -396,10 +403,7 @@ public class Manager implements Closeable { } account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile); - try { - sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); - } catch (UntrustedIdentityException ignored) { - } + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); } public void unregister() throws IOException { @@ -653,17 +657,6 @@ public class Manager implements Closeable { return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); } - private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { - var g = getGroup(groupId); - if (g == null) { - throw new GroupNotFoundException(groupId); - } - if (!g.isMember(account.getSelfRecipientId())) { - throw new NotAGroupMemberException(groupId, g.getTitle()); - } - return g; - } - private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { var g = getGroup(groupId); if (g == null) { @@ -682,12 +675,12 @@ public class Manager implements Closeable { public Pair> sendGroupMessage( String messageText, List attachments, GroupId groupId ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); + final var messageBuilder = createMessageBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); } - return sendGroupMessage(messageBuilder, groupId); + return sendHelper.sendAsGroupMessage(messageBuilder, groupId); } public Pair> sendGroupMessageReaction( @@ -698,20 +691,9 @@ public class Manager implements Closeable { remove, resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); + final var messageBuilder = createMessageBuilder().withReaction(reaction); - return sendGroupMessage(messageBuilder, groupId); - } - - public Pair> sendGroupMessage( - SignalServiceDataMessage.Builder messageBuilder, GroupId groupId - ) throws IOException, GroupNotFoundException, NotAGroupMemberException { - final var g = getGroupForSending(groupId); - - GroupUtils.setGroupContext(messageBuilder, g); - messageBuilder.withExpiration(g.getMessageExpirationTime()); - - return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId())); + return sendHelper.sendAsGroupMessage(messageBuilder, groupId); } public Pair> sendQuitGroupMessage( @@ -722,7 +704,7 @@ public class Manager implements Closeable { return quitGroupV1((GroupInfoV1) group); } - final var newAdmins = getSignalServiceAddresses(groupAdmins); + final var newAdmins = getRecipientIds(groupAdmins); try { return quitGroupV2((GroupInfoV2) group, newAdmins); } catch (ConflictException e) { @@ -737,10 +719,11 @@ public class Manager implements Closeable { .withId(groupInfoV1.getGroupId().serialize()) .build(); - var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); + var messageBuilder = createMessageBuilder().asGroupMessage(group); groupInfoV1.removeMember(account.getSelfRecipientId()); account.getGroupStore().updateGroup(groupInfoV1); - return sendMessage(messageBuilder, groupInfoV1.getMembersWithout(account.getSelfRecipientId())); + return sendHelper.sendGroupMessage(messageBuilder.build(), + groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } private Pair> quitGroupV2( @@ -760,7 +743,8 @@ public class Manager implements Closeable { groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); account.getGroupStore().updateGroup(groupInfoV2); - return sendMessage(messageBuilder, groupInfoV2.getMembersWithout(account.getSelfRecipientId())); + return sendHelper.sendGroupMessage(messageBuilder.build(), + groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } public void deleteGroup(GroupId groupId) throws IOException { @@ -771,7 +755,7 @@ public class Manager implements Closeable { public Pair> createGroup( String name, List members, File avatarFile ) throws IOException, AttachmentInvalidException, InvalidNumberException { - return createGroup(name, members == null ? null : getSignalServiceAddresses(members), avatarFile); + return createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); } private Pair> createGroup( @@ -807,7 +791,8 @@ public class Manager implements Closeable { messageBuilder = getGroupUpdateMessageBuilder(gv2, null); account.getGroupStore().updateGroup(gv2); - final var result = sendMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); + final var result = sendHelper.sendGroupMessage(messageBuilder.build(), + gv2.getMembersIncludingPendingWithout(selfRecipientId)); return new Pair<>(gv2.getGroupId(), result.second()); } @@ -829,10 +814,10 @@ public class Manager implements Closeable { return updateGroup(groupId, name, description, - members == null ? null : getSignalServiceAddresses(members), - removeMembers == null ? null : getSignalServiceAddresses(removeMembers), - admins == null ? null : getSignalServiceAddresses(admins), - removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins), + members == null ? null : getRecipientIds(members), + removeMembers == null ? null : getRecipientIds(removeMembers), + admins == null ? null : getRecipientIds(admins), + removeAdmins == null ? null : getRecipientIds(removeAdmins), resetGroupLink, groupLinkState, addMemberPermission, @@ -908,7 +893,8 @@ public class Manager implements Closeable { account.getGroupStore().updateGroup(gv1); - return sendMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + return sendHelper.sendGroupMessage(messageBuilder.build(), + gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } private void updateGroupV1Details( @@ -1092,7 +1078,7 @@ public class Manager implements Closeable { final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); account.getGroupStore().updateGroup(group); - return sendMessage(messageBuilder, members); + return sendHelper.sendGroupMessage(messageBuilder.build(), members); } private static int currentTimeDays() { @@ -1122,9 +1108,9 @@ public class Manager implements Closeable { GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { GroupInfoV1 g; - var group = getGroupForSending(groupId); + var group = getGroupForUpdating(groupId); if (!(group instanceof GroupInfoV1)) { - throw new RuntimeException("Received an invalid group request for a v2 group!"); + throw new IOException("Received an invalid group request for a v2 group!"); } g = (GroupInfoV1) group; @@ -1136,7 +1122,7 @@ public class Manager implements Closeable { var messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - return sendMessage(messageBuilder, Set.of(recipientId)); + return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(recipientId)); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { @@ -1157,18 +1143,14 @@ public class Manager implements Closeable { throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); } - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) .withRevision(g.getGroup().getRevision()) .withSignedGroupChange(signedGroupChange); - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); } Pair> sendGroupInfoRequest( @@ -1176,10 +1158,10 @@ public class Manager implements Closeable { ) throws IOException { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); - var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); + var messageBuilder = createMessageBuilder().asGroupMessage(group.build()); // Send group info request message to the recipient who sent us a message with this groupId - return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient))); + return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(resolveRecipient(recipient))); } void sendReceipt( @@ -1189,16 +1171,13 @@ public class Manager implements Closeable { List.of(messageId), System.currentTimeMillis()); - dependencies.getMessageSender() - .sendReceipt(remoteAddress, - unidentifiedAccessHelper.getAccessFor(resolveRecipient(remoteAddress)), - receiptMessage); + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); } public Pair> sendMessage( String messageText, List attachments, List recipients ) throws IOException, AttachmentInvalidException, InvalidNumberException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); + final var messageBuilder = createMessageBuilder().withBody(messageText); if (attachments != null) { var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); @@ -1215,33 +1194,33 @@ public class Manager implements Closeable { messageBuilder.withAttachments(attachmentPointers); } - return sendMessage(messageBuilder, getSignalServiceAddresses(recipients)); + return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); } public Pair sendSelfMessage( String messageText, List attachments ) throws IOException, AttachmentInvalidException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText); + final var messageBuilder = createMessageBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); } - return sendSelfMessage(messageBuilder); + return sendHelper.sendSelfMessage(messageBuilder); } public Pair> sendRemoteDeleteMessage( long targetSentTimestamp, List recipients ) throws IOException, InvalidNumberException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); - return sendMessage(messageBuilder, getSignalServiceAddresses(recipients)); + final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); + return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); } public Pair> sendGroupRemoteDeleteMessage( long targetSentTimestamp, GroupId groupId ) throws IOException, NotAGroupMemberException, GroupNotFoundException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); - return sendGroupMessage(messageBuilder, groupId); + final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); + return sendHelper.sendAsGroupMessage(messageBuilder, groupId); } public Pair> sendMessageReaction( @@ -1252,28 +1231,27 @@ public class Manager implements Closeable { remove, resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); - return sendMessage(messageBuilder, getSignalServiceAddresses(recipients)); + final var messageBuilder = createMessageBuilder().withReaction(reaction); + return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); } public Pair> sendEndSessionMessage(List recipients) throws IOException, InvalidNumberException { - var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); + var messageBuilder = createMessageBuilder().asEndSessionMessage(); - final var signalServiceAddresses = getSignalServiceAddresses(recipients); + final var recipientIds = getRecipientIds(recipients); try { - return sendMessage(messageBuilder, signalServiceAddresses); - } catch (Exception e) { - for (var address : signalServiceAddresses) { - handleEndSession(address); + return sendHelper.sendMessage(messageBuilder, recipientIds); + } finally { + for (var recipientId : recipientIds) { + handleEndSession(recipientId); } - throw e; } } void renewSession(RecipientId recipientId) throws IOException { account.getSessionStore().archiveSessions(recipientId); if (!recipientId.equals(getSelfRecipientId())) { - sendNullMessage(recipientId); + sendHelper.sendNullMessage(recipientId); } } @@ -1323,8 +1301,8 @@ public class Manager implements Closeable { } private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - sendMessage(messageBuilder, Set.of(recipientId)); + final var messageBuilder = createMessageBuilder().asExpirationUpdate(); + sendHelper.sendMessage(messageBuilder, Set.of(recipientId)); } /** @@ -1350,8 +1328,8 @@ public class Manager implements Closeable { } private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - sendGroupMessage(messageBuilder, groupId); + final var messageBuilder = createMessageBuilder().asExpirationUpdate(); + sendHelper.sendAsGroupMessage(messageBuilder, groupId); } /** @@ -1398,11 +1376,7 @@ public class Manager implements Closeable { .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + sendHelper.sendSyncMessage(message); } private void requestSyncContacts() throws IOException { @@ -1410,11 +1384,7 @@ public class Manager implements Closeable { .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + sendHelper.sendSyncMessage(message); } private void requestSyncBlocked() throws IOException { @@ -1422,11 +1392,7 @@ public class Manager implements Closeable { .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + sendHelper.sendSyncMessage(message); } private void requestSyncConfiguration() throws IOException { @@ -1434,11 +1400,7 @@ public class Manager implements Closeable { .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + sendHelper.sendSyncMessage(message); } private void requestSyncKeys() throws IOException { @@ -1446,11 +1408,7 @@ public class Manager implements Closeable { .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + sendHelper.sendSyncMessage(message); } private byte[] getSenderCertificate() { @@ -1469,12 +1427,7 @@ public class Manager implements Closeable { return certificate; } - private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { - var messageSender = dependencies.getMessageSender(); - messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync()); - } - - private Set getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { + private Set getRecipientIds(Collection numbers) throws InvalidNumberException { final var signalServiceAddresses = new HashSet(numbers.size()); final var addressesMissingUuid = new HashSet(); @@ -1537,188 +1490,27 @@ public class Manager implements Closeable { public void sendTypingMessage( TypingAction action, Set recipients ) throws IOException, UntrustedIdentityException, InvalidNumberException { - sendTypingMessageInternal(action, getSignalServiceAddresses(recipients)); - } - - private void sendTypingMessageInternal( - TypingAction action, Set recipientIds - ) throws IOException, UntrustedIdentityException { final var timestamp = System.currentTimeMillis(); var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.absent()); - var messageSender = dependencies.getMessageSender(); - for (var recipientId : recipientIds) { - final var address = resolveSignalServiceAddress(recipientId); - messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); - } + sendHelper.sendTypingMessage(message, getRecipientIds(recipients)); } public void sendGroupTypingMessage( TypingAction action, GroupId groupId ) throws IOException, NotAGroupMemberException, GroupNotFoundException { final var timestamp = System.currentTimeMillis(); - final var g = getGroupForSending(groupId); final var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.of(groupId.serialize())); - final var messageSender = dependencies.getMessageSender(); - final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId())); - final var addresses = recipientIdList.stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList()); - messageSender.sendTyping(addresses, unidentifiedAccessHelper.getAccessFor(recipientIdList), message, null); + sendHelper.sendGroupTypingMessage(message, groupId); } - private Pair> sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Set recipientIds - ) throws IOException { + private SignalServiceDataMessage.Builder createMessageBuilder() { final var timestamp = System.currentTimeMillis(); + + var messageBuilder = SignalServiceDataMessage.newBuilder(); messageBuilder.withTimestamp(timestamp); - - SignalServiceDataMessage message = null; - try { - message = messageBuilder.build(); - if (message.getGroupContext().isPresent()) { - try { - var messageSender = dependencies.getMessageSender(); - final var isRecipientUpdate = false; - final var recipientIdList = new ArrayList<>(recipientIds); - final var addresses = recipientIdList.stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList()); - var result = messageSender.sendDataMessage(addresses, - unidentifiedAccessHelper.getAccessFor(recipientIdList), - isRecipientUpdate, - ContentHint.DEFAULT, - message, - sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), - () -> false); - - for (var r : result) { - handlePossibleIdentityFailure(r); - } - - return new Pair<>(timestamp, result); - } catch (UntrustedIdentityException e) { - return new Pair<>(timestamp, List.of()); - } - } else { - // Send to all individually, so sync messages are sent correctly - messageBuilder.withProfileKey(account.getProfileKey().serialize()); - var results = new ArrayList(recipientIds.size()); - for (var recipientId : recipientIds) { - final var contact = account.getContactStore().getContact(recipientId); - final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; - messageBuilder.withExpiration(expirationTime); - message = messageBuilder.build(); - final var result = sendMessage(recipientId, message); - handlePossibleIdentityFailure(result); - results.add(result); - } - return new Pair<>(timestamp, results); - } - } finally { - if (message != null && message.isEndSession()) { - for (var recipient : recipientIds) { - handleEndSession(recipient); - } - } - } - } - - private void handlePossibleIdentityFailure(final SendMessageResult r) { - if (r.getIdentityFailure() != null) { - final var recipientId = resolveRecipient(r.getAddress()); - final var identityKey = r.getIdentityFailure().getIdentityKey(); - if (identityKey != null) { - final var newIdentity = account.getIdentityKeyStore() - .saveIdentity(recipientId, identityKey, new Date()); - if (newIdentity) { - account.getSessionStore().archiveSessions(recipientId); - } - } else { - // Retrieve profile to get the current identity key from the server - retrieveEncryptedProfile(recipientId); - } - } - } - - private Pair sendSelfMessage( - SignalServiceDataMessage.Builder messageBuilder - ) throws IOException { - final var timestamp = System.currentTimeMillis(); - messageBuilder.withTimestamp(timestamp); - final var recipientId = account.getSelfRecipientId(); - - final var contact = account.getContactStore().getContact(recipientId); - final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; - messageBuilder.withExpiration(expirationTime); - - var message = messageBuilder.build(); - final var result = sendSelfMessage(message); - return new Pair<>(timestamp, result); - } - - private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException { - var messageSender = dependencies.getMessageSender(); - - var recipientId = account.getSelfRecipientId(); - - final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipientId); - var recipient = resolveSignalServiceAddress(recipientId); - var transcript = new SentTranscriptMessage(Optional.of(recipient), - message.getTimestamp(), - message, - message.getExpiresInSeconds(), - Map.of(recipient, unidentifiedAccess.isPresent()), - false); - var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); - - try { - return messageSender.sendSyncMessage(syncMessage, unidentifiedAccess); - } catch (UntrustedIdentityException e) { - return SendMessageResult.identityFailure(recipient, e.getIdentityKey()); - } - } - - private SendMessageResult sendMessage( - RecipientId recipientId, SignalServiceDataMessage message - ) throws IOException { - var messageSender = dependencies.getMessageSender(); - - final var address = resolveSignalServiceAddress(recipientId); - try { - try { - return messageSender.sendDataMessage(address, - unidentifiedAccessHelper.getAccessFor(recipientId), - ContentHint.DEFAULT, - message); - } catch (UnregisteredUserException e) { - final var newRecipientId = refreshRegisteredUser(recipientId); - return messageSender.sendDataMessage(resolveSignalServiceAddress(newRecipientId), - unidentifiedAccessHelper.getAccessFor(newRecipientId), - ContentHint.DEFAULT, - message); - } - } catch (UntrustedIdentityException e) { - return SendMessageResult.identityFailure(address, e.getIdentityKey()); - } - } - - private SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException { - var messageSender = dependencies.getMessageSender(); - - final var address = resolveSignalServiceAddress(recipientId); - try { - try { - return messageSender.sendNullMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId)); - } catch (UnregisteredUserException e) { - final var newRecipientId = refreshRegisteredUser(recipientId); - final var newAddress = resolveSignalServiceAddress(newRecipientId); - return messageSender.sendNullMessage(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId)); - } - } catch (UntrustedIdentityException e) { - return SendMessageResult.identityFailure(address, e.getIdentityKey()); - } + return messageBuilder; } private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException { @@ -2611,7 +2403,7 @@ public class Manager implements Closeable { .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); } - void sendGroups() throws IOException, UntrustedIdentityException { + void sendGroups() throws IOException { var groupsFile = IOUtils.createTempFile(); try { @@ -2645,7 +2437,7 @@ public class Manager implements Closeable { .withLength(groupsFile.length()) .build(); - sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); } } } finally { @@ -2657,7 +2449,7 @@ public class Manager implements Closeable { } } - public void sendContacts() throws IOException, UntrustedIdentityException { + public void sendContacts() throws IOException { var contactsFile = IOUtils.createTempFile(); try { @@ -2713,7 +2505,8 @@ public class Manager implements Closeable { .withLength(contactsFile.length()) .build(); - sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, true))); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, + true))); } } } finally { @@ -2725,7 +2518,7 @@ public class Manager implements Closeable { } } - void sendBlockedList() throws IOException, UntrustedIdentityException { + void sendBlockedList() throws IOException { var addresses = new ArrayList(); for (var record : account.getContactStore().getContacts()) { if (record.second().isBlocked()) { @@ -2738,17 +2531,17 @@ public class Manager implements Closeable { groupIds.add(record.getGroupId().serialize()); } } - sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); } private void sendVerifiedMessage( SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel - ) throws IOException, UntrustedIdentityException { + ) throws IOException { var verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState(), System.currentTimeMillis()); - sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } public List> getContacts() { @@ -2849,13 +2642,28 @@ public class Manager implements Closeable { try { var address = account.getRecipientStore().resolveServiceAddress(recipientId); sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); - } catch (IOException | UntrustedIdentityException e) { + } catch (IOException e) { logger.warn("Failed to send verification sync message: {}", e.getMessage()); } return true; } + private void handleIdentityFailure( + final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure + ) { + final var identityKey = identityFailure.getIdentityKey(); + if (identityKey != null) { + final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } + } else { + // Retrieve profile to get the current identity key from the server + retrieveEncryptedProfile(recipientId); + } + } + public String computeSafetyNumber( SignalServiceAddress theirAddress, IdentityKey theirIdentityKey ) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupProvider.java new file mode 100644 index 00000000..25b86786 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupProvider.java @@ -0,0 +1,9 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.storage.groups.GroupInfo; + +public interface GroupProvider { + + GroupInfo getGroup(GroupId groupId); +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityFailureHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityFailureHandler.java new file mode 100644 index 00000000..1cd31409 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityFailureHandler.java @@ -0,0 +1,9 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.whispersystems.signalservice.api.messages.SendMessageResult; + +public interface IdentityFailureHandler { + + void handleIdentityFailure(RecipientId recipientId, SendMessageResult.IdentityFailure identityFailure); +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientRegistrationRefresher.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientRegistrationRefresher.java new file mode 100644 index 00000000..836d18d7 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientRegistrationRefresher.java @@ -0,0 +1,10 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.storage.recipients.RecipientId; + +import java.io.IOException; + +public interface RecipientRegistrationRefresher { + + RecipientId refreshRecipientRegistration(RecipientId recipientId) throws IOException; +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java new file mode 100644 index 00000000..bc8800cf --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -0,0 +1,277 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.crypto.ContentHint; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class SendHelper { + + private final static Logger logger = LoggerFactory.getLogger(SendHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final UnidentifiedAccessHelper unidentifiedAccessHelper; + private final SignalServiceAddressResolver addressResolver; + private final RecipientResolver recipientResolver; + private final IdentityFailureHandler identityFailureHandler; + private final GroupProvider groupProvider; + private final RecipientRegistrationRefresher recipientRegistrationRefresher; + + public SendHelper( + final SignalAccount account, + final SignalDependencies dependencies, + final UnidentifiedAccessHelper unidentifiedAccessHelper, + final SignalServiceAddressResolver addressResolver, + final RecipientResolver recipientResolver, + final IdentityFailureHandler identityFailureHandler, + final GroupProvider groupProvider, + final RecipientRegistrationRefresher recipientRegistrationRefresher + ) { + this.account = account; + this.dependencies = dependencies; + this.unidentifiedAccessHelper = unidentifiedAccessHelper; + this.addressResolver = addressResolver; + this.recipientResolver = recipientResolver; + this.identityFailureHandler = identityFailureHandler; + this.groupProvider = groupProvider; + this.recipientRegistrationRefresher = recipientRegistrationRefresher; + } + + /** + * Send a single message to one or multiple recipients. + * The message is extended with the current expiration timer for each recipient. + */ + public Pair> sendMessage( + SignalServiceDataMessage.Builder messageBuilder, Set recipientIds + ) throws IOException { + // Send to all individually, so sync messages are sent correctly + messageBuilder.withProfileKey(account.getProfileKey().serialize()); + var results = new ArrayList(recipientIds.size()); + long timestamp = 0; + for (var recipientId : recipientIds) { + final var contact = account.getContactStore().getContact(recipientId); + final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; + messageBuilder.withExpiration(expirationTime); + + final var singleMessage = messageBuilder.build(); + timestamp = singleMessage.getTimestamp(); + final var result = sendMessage(singleMessage, recipientId); + handlePossibleIdentityFailure(result); + + results.add(result); + } + return new Pair<>(timestamp, results); + } + + /** + * Send a group message to the given group + * The message is extended with the current expiration timer for the group and the group context. + */ + public Pair> sendAsGroupMessage( + SignalServiceDataMessage.Builder messageBuilder, GroupId groupId + ) throws IOException, GroupNotFoundException, NotAGroupMemberException { + final var g = getGroupForSending(groupId); + GroupUtils.setGroupContext(messageBuilder, g); + messageBuilder.withExpiration(g.getMessageExpirationTime()); + + final var recipients = g.getMembersWithout(account.getSelfRecipientId()); + return sendGroupMessage(messageBuilder.build(), recipients); + } + + /** + * Send a complete group message to the given recipients (should be current/old/new members) + * This method should only be used for create/update/quit group messages. + */ + public Pair> sendGroupMessage( + final SignalServiceDataMessage message, final Set recipientIds + ) throws IOException { + List result = sendGroupMessageInternal(message, recipientIds); + + for (var r : result) { + handlePossibleIdentityFailure(r); + } + + return new Pair<>(message.getTimestamp(), result); + } + + public void sendReceiptMessage( + final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId + ) throws IOException, UntrustedIdentityException { + final var messageSender = dependencies.getMessageSender(); + messageSender.sendReceipt(addressResolver.resolveSignalServiceAddress(recipientId), + unidentifiedAccessHelper.getAccessFor(recipientId), + receiptMessage); + } + + public SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException { + var messageSender = dependencies.getMessageSender(); + + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + try { + try { + return messageSender.sendNullMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId)); + } catch (UnregisteredUserException e) { + final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); + final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); + return messageSender.sendNullMessage(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId)); + } + } catch (UntrustedIdentityException e) { + return SendMessageResult.identityFailure(address, e.getIdentityKey()); + } + } + + public Pair sendSelfMessage( + SignalServiceDataMessage.Builder messageBuilder + ) throws IOException { + final var recipientId = account.getSelfRecipientId(); + final var contact = account.getContactStore().getContact(recipientId); + final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; + messageBuilder.withExpiration(expirationTime); + + var message = messageBuilder.build(); + final var result = sendSelfMessage(message); + return new Pair<>(message.getTimestamp(), result); + } + + public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) throws IOException { + var messageSender = dependencies.getMessageSender(); + try { + return messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync()); + } catch (UntrustedIdentityException e) { + var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId()); + return SendMessageResult.identityFailure(address, e.getIdentityKey()); + } + } + + public void sendTypingMessage( + SignalServiceTypingMessage message, Set recipientIds + ) throws IOException, UntrustedIdentityException { + var messageSender = dependencies.getMessageSender(); + for (var recipientId : recipientIds) { + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + try { + messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); + } catch (UnregisteredUserException e) { + final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); + final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); + messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message); + } + } + } + + public void sendGroupTypingMessage( + SignalServiceTypingMessage message, GroupId groupId + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + final var g = getGroupForSending(groupId); + final var messageSender = dependencies.getMessageSender(); + final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId())); + final var addresses = recipientIdList.stream() + .map(addressResolver::resolveSignalServiceAddress) + .collect(Collectors.toList()); + messageSender.sendTyping(addresses, unidentifiedAccessHelper.getAccessFor(recipientIdList), message, null); + } + + private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { + var g = groupProvider.getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } + if (!g.isMember(account.getSelfRecipientId())) { + throw new NotAGroupMemberException(groupId, g.getTitle()); + } + return g; + } + + private List sendGroupMessageInternal( + final SignalServiceDataMessage message, final Set recipientIds + ) throws IOException { + try { + var messageSender = dependencies.getMessageSender(); + // isRecipientUpdate is true if we've already sent this message to some recipients in the past, otherwise false. + final var isRecipientUpdate = false; + final var recipientIdList = new ArrayList<>(recipientIds); + final var addresses = recipientIdList.stream() + .map(addressResolver::resolveSignalServiceAddress) + .collect(Collectors.toList()); + return messageSender.sendDataMessage(addresses, + unidentifiedAccessHelper.getAccessFor(recipientIdList), + isRecipientUpdate, + ContentHint.DEFAULT, + message, + sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), + () -> false); + } catch (UntrustedIdentityException e) { + return List.of(); + } + } + + private SendMessageResult sendMessage( + SignalServiceDataMessage message, RecipientId recipientId + ) throws IOException { + var messageSender = dependencies.getMessageSender(); + + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + try { + try { + return messageSender.sendDataMessage(address, + unidentifiedAccessHelper.getAccessFor(recipientId), + ContentHint.DEFAULT, + message); + } catch (UnregisteredUserException e) { + final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); + return messageSender.sendDataMessage(addressResolver.resolveSignalServiceAddress(newRecipientId), + unidentifiedAccessHelper.getAccessFor(newRecipientId), + ContentHint.DEFAULT, + message); + } + } catch (UntrustedIdentityException e) { + return SendMessageResult.identityFailure(address, e.getIdentityKey()); + } + } + + private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException { + var address = account.getSelfAddress(); + var transcript = new SentTranscriptMessage(Optional.of(address), + message.getTimestamp(), + message, + message.getExpiresInSeconds(), + Map.of(address, true), + false); + var syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); + + return sendSyncMessage(syncMessage); + } + + private void handlePossibleIdentityFailure(final SendMessageResult r) { + if (r.getIdentityFailure() != null) { + final var recipientId = recipientResolver.resolveRecipient(r.getAddress()); + identityFailureHandler.handleIdentityFailure(recipientId, r.getIdentityFailure()); + } + } +} diff --git a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java index 0fcae322..07dca322 100644 --- a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java @@ -6,9 +6,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; -import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; import org.asamk.signal.manager.Manager; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import java.io.IOException; @@ -30,8 +28,6 @@ public class SendContactsCommand implements JsonRpcLocalCommand { ) throws CommandException { try { m.sendContacts(); - } catch (UntrustedIdentityException e) { - throw new UntrustedKeyErrorException("SendContacts error: " + e.getMessage()); } catch (IOException e) { throw new IOErrorException("SendContacts error: " + e.getMessage()); } From 70fc2381d3c99a5d262ce7cd4c51b46c6d4dff98 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 Aug 2021 18:31:14 +0200 Subject: [PATCH 0747/2005] Add json output listDevices and uploadStickerPack commands --- .../signal/commands/ListDevicesCommand.java | 49 +++++++++++++++---- .../commands/UploadStickerPackCommand.java | 13 +++-- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java index 16f602f1..40f30681 100644 --- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java @@ -3,6 +3,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; @@ -15,8 +16,9 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; +import java.util.stream.Collectors; -public class ListDevicesCommand implements LocalCommand { +public class ListDevicesCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(ListDevicesCommand.class); @@ -34,8 +36,6 @@ public class ListDevicesCommand implements LocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var writer = (PlainTextWriter) outputWriter; - List devices; try { devices = m.getLinkedDevices(); @@ -44,13 +44,42 @@ public class ListDevicesCommand implements LocalCommand { throw new IOErrorException("Failed to get linked devices: " + e.getMessage()); } - for (var d : devices) { - writer.println("- Device {}{}:", d.getId(), (d.getId() == m.getDeviceId() ? " (this device)" : "")); - writer.indent(w -> { - w.println("Name: {}", d.getName()); - w.println("Created: {}", DateUtils.formatTimestamp(d.getCreated())); - w.println("Last seen: {}", DateUtils.formatTimestamp(d.getLastSeen())); - }); + if (outputWriter instanceof PlainTextWriter) { + final var writer = (PlainTextWriter) outputWriter; + for (var d : devices) { + writer.println("- Device {}{}:", d.getId(), (d.getId() == m.getDeviceId() ? " (this device)" : "")); + writer.indent(w -> { + w.println("Name: {}", d.getName()); + w.println("Created: {}", DateUtils.formatTimestamp(d.getCreated())); + w.println("Last seen: {}", DateUtils.formatTimestamp(d.getLastSeen())); + }); + } + } else { + final var writer = (JsonWriter) outputWriter; + final var jsonDevices = devices.stream() + .map(d -> new JsonDevice(d.getId(), d.getName(), d.getCreated(), d.getLastSeen())) + .collect(Collectors.toList()); + writer.write(jsonDevices); + } + } + + private static final class JsonDevice { + + public final long id; + public final String name; + public final long createdTimestamp; + public final long lastSeenTimestamp; + + private JsonDevice( + final long id, + final String name, + final long createdTimestamp, + final long lastSeenTimestamp + ) { + this.id = id; + this.name = name; + this.createdTimestamp = createdTimestamp; + this.lastSeenTimestamp = lastSeenTimestamp; } } } diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java index a5839238..7af6fed8 100644 --- a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -3,6 +3,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; @@ -15,8 +16,9 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.util.Map; -public class UploadStickerPackCommand implements LocalCommand { +public class UploadStickerPackCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(UploadStickerPackCommand.class); @@ -36,12 +38,17 @@ public class UploadStickerPackCommand implements LocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var writer = (PlainTextWriter) outputWriter; var path = new File(ns.getString("path")); try { var url = m.uploadStickerPack(path); - writer.println("{}", url); + if (outputWriter instanceof PlainTextWriter) { + final var writer = (PlainTextWriter) outputWriter; + writer.println("{}", url); + } else { + final var writer = (JsonWriter) outputWriter; + writer.write(Map.of("url", url)); + } } catch (IOException e) { throw new IOErrorException("Upload error (maybe image size too large):" + e.getMessage()); } catch (StickerPackInvalidException e) { From 11c90fa0324347bf2b7ba56855ccd45898141569 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 Aug 2021 18:37:51 +0200 Subject: [PATCH 0748/2005] Add json output listIdentities command --- .../org/asamk/signal/manager/Manager.java | 16 +++- .../org/asamk/signal/manager/util/Utils.java | 8 +- .../commands/ListIdentitiesCommand.java | 81 +++++++++++++++---- src/main/java/org/asamk/signal/util/Hex.java | 5 ++ src/main/java/org/asamk/signal/util/Util.java | 9 ++- 5 files changed, 94 insertions(+), 25 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index c4b69665..e2306dec 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -2664,14 +2664,22 @@ public class Manager implements Closeable { } } - public String computeSafetyNumber( - SignalServiceAddress theirAddress, IdentityKey theirIdentityKey - ) { - return Utils.computeSafetyNumber(ServiceConfig.capabilities.isUuid(), + public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + final var fingerprint = Utils.computeSafetyNumber(capabilities.isUuid(), account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); + } + + public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + final var fingerprint = Utils.computeSafetyNumber(capabilities.isUuid(), + account.getSelfAddress(), + getIdentityKeyPair().getPublicKey(), + theirAddress, + theirIdentityKey); + return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); } @Deprecated diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index b0a9bbb1..a2466311 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -1,6 +1,7 @@ package org.asamk.signal.manager.util; import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.StreamDetails; @@ -36,7 +37,7 @@ public class Utils { return new StreamDetails(stream, mime, size); } - public static String computeSafetyNumber( + public static Fingerprint computeSafetyNumber( boolean isUuidCapable, SignalServiceAddress ownAddress, IdentityKey ownIdentityKey, @@ -56,18 +57,17 @@ public class Utils { // Version 1: E164 user version = 1; if (!ownAddress.getNumber().isPresent() || !theirAddress.getNumber().isPresent()) { - return "INVALID ID"; + return null; } ownId = ownAddress.getNumber().get().getBytes(); theirId = theirAddress.getNumber().get().getBytes(); } - var fingerprint = new NumericFingerprintGenerator(5200).createFor(version, + return new NumericFingerprintGenerator(5200).createFor(version, ownId, ownIdentityKey, theirId, theirIdentityKey); - return fingerprint.getDisplayableFingerprint().getDisplayText(); } public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) { diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index f996f3b5..49ca2546 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -3,6 +3,7 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; @@ -16,9 +17,12 @@ import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import java.util.Base64; import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; -public class ListIdentitiesCommand implements LocalCommand { +public class ListIdentitiesCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(ListIdentitiesCommand.class); @@ -48,26 +52,71 @@ public class ListIdentitiesCommand implements LocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var writer = (PlainTextWriter) outputWriter; - var number = ns.getString("number"); - if (number == null) { - for (var identity : m.getIdentities()) { - printIdentityFingerprint(writer, m, identity); - } - return; - } - List identities; - try { - identities = m.getIdentities(number); - } catch (InvalidNumberException e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); + if (number == null) { + identities = m.getIdentities(); + } else { + try { + identities = m.getIdentities(number); + } catch (InvalidNumberException e) { + throw new UserErrorException("Invalid number: " + e.getMessage()); + } } - for (var id : identities) { - printIdentityFingerprint(writer, m, id); + if (outputWriter instanceof PlainTextWriter) { + final var writer = (PlainTextWriter) outputWriter; + for (var id : identities) { + printIdentityFingerprint(writer, m, id); + } + } else { + final var writer = (JsonWriter) outputWriter; + final var jsonIdentities = identities.stream().map(id -> { + final var address = m.resolveSignalServiceAddress(id.getRecipientId()); + var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(address, id.getIdentityKey())); + var scannableSafetyNumber = m.computeSafetyNumberForScanning(address, id.getIdentityKey()); + return new JsonIdentity(address.getNumber().orNull(), + address.getUuid().transform(UUID::toString).orNull(), + Hex.toString(id.getFingerprint()), + safetyNumber, + scannableSafetyNumber == null + ? null + : Base64.getEncoder().encodeToString(scannableSafetyNumber), + id.getTrustLevel().name(), + id.getDateAdded().getTime()); + }).collect(Collectors.toList()); + + writer.write(jsonIdentities); + } + } + + private static final class JsonIdentity { + + public final String number; + public final String uuid; + public final String fingerprint; + public final String safetyNumber; + public final String scannableSafetyNumber; + public final String trustLevel; + public final long addedTimestamp; + + private JsonIdentity( + final String number, + final String uuid, + final String fingerprint, + final String safetyNumber, + final String scannableSafetyNumber, + final String trustLevel, + final long addedTimestamp + ) { + this.number = number; + this.uuid = uuid; + this.fingerprint = fingerprint; + this.safetyNumber = safetyNumber; + this.scannableSafetyNumber = scannableSafetyNumber; + this.trustLevel = trustLevel; + this.addedTimestamp = addedTimestamp; } } } diff --git a/src/main/java/org/asamk/signal/util/Hex.java b/src/main/java/org/asamk/signal/util/Hex.java index f5f7a6ad..596f23a9 100644 --- a/src/main/java/org/asamk/signal/util/Hex.java +++ b/src/main/java/org/asamk/signal/util/Hex.java @@ -8,11 +8,16 @@ public class Hex { } public static String toString(byte[] bytes) { + if (bytes.length == 0) { + return ""; + } + var buf = new StringBuffer(); for (final var aByte : bytes) { appendHexChar(buf, aByte); buf.append(" "); } + buf.deleteCharAt(buf.length() - 1); return buf.toString(); } diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 31c6b68e..0afe0910 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -45,11 +45,18 @@ public class Util { } public static String formatSafetyNumber(String digits) { + if (digits == null) { + return null; + } + final var partCount = 12; var partSize = digits.length() / partCount; var f = new StringBuilder(digits.length() + partCount); for (var i = 0; i < partCount; i++) { - f.append(digits, i * partSize, (i * partSize) + partSize).append(" "); + f.append(digits, i * partSize, (i * partSize) + partSize); + if (i != partCount - 1) { + f.append(" "); + } } return f.toString(); } From a18d6b3fe47fb5ea2e42d048eb1cc9eef41e6694 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 21 Aug 2021 19:03:51 +0200 Subject: [PATCH 0749/2005] Add json output listContacts command --- .../signal/commands/ListContactsCommand.java | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index aaf0e0b3..b39f9ec9 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -3,13 +3,17 @@ package org.asamk.signal.commands; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; +import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.manager.Manager; +import java.util.UUID; +import java.util.stream.Collectors; + import static org.asamk.signal.util.Util.getLegacyIdentifier; -public class ListContactsCommand implements LocalCommand { +public class ListContactsCommand implements JsonRpcLocalCommand { @Override public String getName() { @@ -23,14 +27,56 @@ public class ListContactsCommand implements LocalCommand { @Override public void handleCommand(final Namespace ns, final Manager m, final OutputWriter outputWriter) { - final var writer = (PlainTextWriter) outputWriter; - var contacts = m.getContacts(); - for (var c : contacts) { - writer.println("Number: {} Name: {} Blocked: {}", - getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())), - c.second().getName(), - c.second().isBlocked()); + + if (outputWriter instanceof PlainTextWriter) { + final var writer = (PlainTextWriter) outputWriter; + for (var c : contacts) { + final var contact = c.second(); + writer.println("Number: {} Name: {} Blocked: {} Message expiration: {}", + getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())), + contact.getName(), + contact.isBlocked(), + contact.getMessageExpirationTime() == 0 + ? "disabled" + : contact.getMessageExpirationTime() + "s"); + } + } else { + final var writer = (JsonWriter) outputWriter; + final var jsonContacts = contacts.stream().map(contactPair -> { + final var address = m.resolveSignalServiceAddress(contactPair.first()); + final var contact = contactPair.second(); + return new JsonContact(address.getNumber().orNull(), + address.getUuid().transform(UUID::toString).orNull(), + contact.getName(), + contact.isBlocked(), + contact.getMessageExpirationTime()); + }).collect(Collectors.toList()); + + writer.write(jsonContacts); + } + } + + private static final class JsonContact { + + public final String number; + public final String uuid; + public final String name; + public final boolean isBlocked; + public final int messageExpirationTime; + + private JsonContact( + final String number, + final String uuid, + final String name, + final boolean isBlocked, + final int messageExpirationTime + ) { + this.number = number; + this.uuid = uuid; + this.name = name; + this.isBlocked = isBlocked; + this.messageExpirationTime = messageExpirationTime; } } } From b745f1f9024d8600408a6ca34e6eb66377e89f2c Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Aug 2021 08:54:21 +0200 Subject: [PATCH 0750/2005] Trim zero bytes from profile fields --- .../org/asamk/signal/manager/util/ProfileUtils.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java index 213f516c..f6f76c2c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java @@ -18,8 +18,8 @@ public class ProfileUtils { var profileCipher = new ProfileCipher(profileKey); try { var name = decrypt(encryptedProfile.getName(), profileCipher); - var about = decrypt(encryptedProfile.getAbout(), profileCipher); - var aboutEmoji = decrypt(encryptedProfile.getAboutEmoji(), profileCipher); + var about = trimZeros(decrypt(encryptedProfile.getAbout(), profileCipher)); + var aboutEmoji = trimZeros(decrypt(encryptedProfile.getAboutEmoji(), profileCipher)); final var nameParts = splitName(name); return new Profile(System.currentTimeMillis(), @@ -95,4 +95,13 @@ public class ProfileUtils { return new Pair<>(parts[0], parts[1]); } } + + static String trimZeros(String str) { + if (str == null) { + return null; + } + + int pos = str.indexOf(0); + return pos == -1 ? str : str.substring(0, pos); + } } From 610e32aa52ffd71ce66ee313b559e5ab57fd12fa Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Aug 2021 09:55:53 +0200 Subject: [PATCH 0751/2005] Implement announcement groups --- .../org/asamk/signal/manager/Manager.java | 23 ++++++++++++++----- .../signal/manager/helper/GroupV2Helper.java | 8 +++++++ man/signal-cli.1.adoc | 4 ++++ .../signal/commands/UpdateGroupCommand.java | 9 +++++++- .../org/asamk/signal/dbus/DbusSignalImpl.java | 1 + 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index e2306dec..e4342f9a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -809,7 +809,8 @@ public class Manager implements Closeable { GroupPermission addMemberPermission, GroupPermission editDetailsPermission, File avatarFile, - Integer expirationTimer + Integer expirationTimer, + Boolean isAnnouncementGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { return updateGroup(groupId, name, @@ -823,7 +824,8 @@ public class Manager implements Closeable { addMemberPermission, editDetailsPermission, avatarFile, - expirationTimer); + expirationTimer, + isAnnouncementGroup); } private Pair> updateGroup( @@ -839,7 +841,8 @@ public class Manager implements Closeable { final GroupPermission addMemberPermission, final GroupPermission editDetailsPermission, final File avatarFile, - final Integer expirationTimer + final Integer expirationTimer, + final Boolean isAnnouncementGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { var group = getGroupForUpdating(groupId); @@ -857,7 +860,8 @@ public class Manager implements Closeable { addMemberPermission, editDetailsPermission, avatarFile, - expirationTimer); + expirationTimer, + isAnnouncementGroup); } catch (ConflictException e) { // Detected conflicting update, refreshing group and trying again group = getGroup(groupId, true); @@ -873,7 +877,8 @@ public class Manager implements Closeable { addMemberPermission, editDetailsPermission, avatarFile, - expirationTimer); + expirationTimer, + isAnnouncementGroup); } } @@ -948,7 +953,8 @@ public class Manager implements Closeable { final GroupPermission addMemberPermission, final GroupPermission editDetailsPermission, final File avatarFile, - Integer expirationTimer + final Integer expirationTimer, + final Boolean isAnnouncementGroup ) throws IOException { Pair> result = null; if (group.isPendingMember(account.getSelfRecipientId())) { @@ -1034,6 +1040,11 @@ public class Manager implements Closeable { result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); } + if (isAnnouncementGroup != null) { + var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + if (name != null || description != null || avatarFile != null) { var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); if (avatarFile != null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 206994f5..e161673e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -407,6 +407,14 @@ public class GroupV2Helper { return commitChange(groupInfoV2, change); } + public Pair setIsAnnouncementGroup( + GroupInfoV2 groupInfoV2, boolean isAnnouncementGroup + ) throws IOException { + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + final var change = groupOperations.createAnnouncementGroupChange(isAnnouncementGroup); + return commitChange(groupInfoV2, change); + } + private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) { switch (state) { case DISABLED: diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 0e3789e5..3df9198b 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -287,6 +287,10 @@ Set permission to add new group members: `every-member`, `only-admins` *--set-permission-edit-details* PERMISSION:: Set permission to edit group details: `every-member`, `only-admins` +*--set-permission-send-messages* PERMISSION:: +Set permission to send messages in group: `every-member`, `only-admins` +Groups where only admins can send messages are also called announcement groups + *-e* EXPIRATION_SECONDS, *--expiration* EXPIRATION_SECONDS:: Set expiration time of messages (seconds). To disable expiration set expiration time to 0. diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 397d1fe3..00da38d4 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -70,6 +70,9 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { subparser.addArgument("--set-permission-edit-details") .help("Set permission to edit group details") .choices("every-member", "only-admins"); + subparser.addArgument("--set-permission-send-messages") + .help("Set permission to send messages") + .choices("every-member", "only-admins"); subparser.addArgument("-e", "--expiration").type(int.class).help("Set expiration time of messages (seconds)"); } @@ -133,6 +136,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { var groupExpiration = ns.getInt("expiration"); var groupAddMemberPermission = getGroupPermission(ns.getString("set-permission-add-member")); var groupEditDetailsPermission = getGroupPermission(ns.getString("set-permission-edit-details")); + var groupSendMessagesPermission = getGroupPermission(ns.getString("set-permission-send-messages")); try { boolean isNewGroup = false; @@ -160,7 +164,10 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { groupAddMemberPermission, groupEditDetailsPermission, groupAvatar == null ? null : new File(groupAvatar), - groupExpiration); + groupExpiration, + groupSendMessagesPermission == null + ? null + : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS); Long timestamp = null; if (results != null) { timestamp = results.first(); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index b88aa81e..a214ef88 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -358,6 +358,7 @@ public class DbusSignalImpl implements Signal { null, null, avatar == null ? null : new File(avatar), + null, null); if (results != null) { checkSendMessageResults(results.first(), results.second()); From 73e137137d272811b01ed053f57e0161e476d90a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Aug 2021 10:17:47 +0200 Subject: [PATCH 0752/2005] Discard messages from non-admins in announcement groups --- .../org/asamk/signal/manager/Manager.java | 49 ++++++++++++------- .../manager/storage/groups/GroupInfo.java | 6 +++ .../manager/storage/groups/GroupInfoV1.java | 5 ++ .../manager/storage/groups/GroupInfoV2.java | 6 +++ 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index e4342f9a..9d0c355b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1874,7 +1874,6 @@ public class Manager implements Closeable { // address/uuid in envelope is sent by server resolveRecipientTrusted(envelope.getSourceAddress()); } - final var notAGroupMember = isNotAGroupMember(envelope, content); if (!envelope.isReceipt()) { try { content = decryptMessage(envelope); @@ -1910,10 +1909,13 @@ public class Manager implements Closeable { queuedActions.addAll(actions); } } + final var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content); if (isMessageBlocked(envelope, content)) { logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); - } else if (notAGroupMember) { - logger.info("Ignoring a message from a non group member: {}", envelope.getTimestamp()); + } else if (notAllowedToSendToGroup) { + logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", + (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), + envelope.getTimestamp()); } else { handler.handleMessage(envelope, content, exception); } @@ -1976,7 +1978,7 @@ public class Manager implements Closeable { return sourceContact != null && sourceContact.isBlocked(); } - private boolean isNotAGroupMember( + private boolean isNotAllowedToSendToGroup( SignalServiceEnvelope envelope, SignalServiceContent content ) { SignalServiceAddress source; @@ -1988,23 +1990,32 @@ public class Manager implements Closeable { return false; } - if (content != null && content.getDataMessage().isPresent()) { - var message = content.getDataMessage().get(); - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - var groupInfo = message.getGroupContext().get().getGroupV1().get(); - if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) { - return false; - } - } - var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); - var group = getGroup(groupId); - if (group != null && !group.isMember(resolveRecipient(source))) { - return true; - } + if (content == null || !content.getDataMessage().isPresent()) { + return false; + } + + var message = content.getDataMessage().get(); + if (!message.getGroupContext().isPresent()) { + return false; + } + + if (message.getGroupContext().get().getGroupV1().isPresent()) { + var groupInfo = message.getGroupContext().get().getGroupV1().get(); + if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) { + return false; } } - return false; + + var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + var group = getGroup(groupId); + if (group == null) { + return false; + } + + final var recipientId = resolveRecipient(source); + return !group.isMember(recipientId) || ( + group.isAnnouncementGroup() && !group.isAdmin(recipientId) + ); } private List handleMessage( diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index 211e0f96..60efc84b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -40,6 +40,8 @@ public abstract class GroupInfo { public abstract int getMessageExpirationTime(); + public abstract boolean isAnnouncementGroup(); + public Set getMembersWithout(RecipientId recipientId) { return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet()); } @@ -54,6 +56,10 @@ public abstract class GroupInfo { return getMembers().contains(recipientId); } + public boolean isAdmin(RecipientId recipientId) { + return getAdminMembers().contains(recipientId); + } + public boolean isPendingMember(RecipientId recipientId) { return getPendingMembers().contains(recipientId); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index 8cf6cb94..49c9a504 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -89,6 +89,11 @@ public class GroupInfoV1 extends GroupInfo { return messageExpirationTime; } + @Override + public boolean isAnnouncementGroup() { + return false; + } + public void addMembers(Collection members) { this.members.addAll(members); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index fe82862e..59cfedb5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -7,6 +7,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.EnabledState; import org.signal.zkgroup.groups.GroupMasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; @@ -146,4 +147,9 @@ public class GroupInfoV2 extends GroupInfo { ? this.group.getDisappearingMessagesTimer().getDuration() : 0; } + + @Override + public boolean isAnnouncementGroup() { + return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED; + } } From 5bbfd3259891e18a11cb878e14a9c17990b13d79 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Aug 2021 11:28:09 +0200 Subject: [PATCH 0753/2005] Extend json output with number and uuid fields --- .../org/asamk/signal/manager/Manager.java | 36 ++++++++++++++----- .../signal/commands/GetUserStatusCommand.java | 25 ++++++++----- .../org/asamk/signal/json/JsonMention.java | 15 ++++++-- .../signal/json/JsonMessageEnvelope.java | 23 +++++++++++- .../java/org/asamk/signal/json/JsonQuote.java | 13 ++++++- .../org/asamk/signal/json/JsonReaction.java | 14 +++++++- .../signal/json/JsonSyncDataMessage.java | 27 ++++++++++++-- .../asamk/signal/json/JsonSyncMessage.java | 5 +-- .../signal/json/JsonSyncReadMessage.java | 22 ++++++++++-- 9 files changed, 148 insertions(+), 32 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 9d0c355b..8aa7ed18 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -327,16 +327,32 @@ public class Manager implements Closeable { * This is used for checking a set of phone numbers for registration on Signal * * @param numbers The set of phone number in question - * @return A map of numbers to booleans. True if registered, false otherwise. Should never be null + * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. * @throws IOException if its unable to get the contacts to check if they're registered */ - public Map areUsersRegistered(Set numbers) throws IOException { + public Map> areUsersRegistered(Set numbers) throws IOException { + Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { + try { + return canonicalizePhoneNumber(n); + } catch (InvalidNumberException e) { + return ""; + } + })); + // Note "contactDetails" has no optionals. It only gives us info on users who are registered - var contactDetails = getRegisteredUsers(numbers); + var contactDetails = getRegisteredUsers(canonicalizedNumbers.values() + .stream() + .filter(s -> !s.isEmpty()) + .collect(Collectors.toSet())); - var registeredUsers = contactDetails.keySet(); + // Store numbers as recipients so we have the number/uuid association + contactDetails.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); - return numbers.stream().collect(Collectors.toMap(x -> x, registeredUsers::contains)); + return numbers.stream().collect(Collectors.toMap(n -> n, n -> { + final var number = canonicalizedNumbers.get(n); + final var uuid = contactDetails.get(number); + return new Pair<>(number.isEmpty() ? null : number, uuid); + })); } public void updateAccountAttributes() throws IOException { @@ -2724,14 +2740,16 @@ public class Manager implements Closeable { return account.getRecipientStore().resolveServiceAddress(recipientId); } - public RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException { - var canonicalizedNumber = UuidUtil.isUuid(identifier) - ? identifier - : PhoneNumberFormatter.formatNumber(identifier, account.getUsername()); + private RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException { + var canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : canonicalizePhoneNumber(identifier); return resolveRecipient(canonicalizedNumber); } + private String canonicalizePhoneNumber(final String number) throws InvalidNumberException { + return PhoneNumberFormatter.formatNumber(number, account.getUsername()); + } + private RecipientId resolveRecipient(final String identifier) { var address = Utils.getSignalServiceAddressFromIdentifier(identifier); diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index 05bbea47..055dac9f 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -11,10 +11,12 @@ import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.manager.Manager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.util.Pair; import java.io.IOException; import java.util.HashSet; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; public class GetUserStatusCommand implements JsonRpcLocalCommand { @@ -37,7 +39,7 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { // Get a map of registration statuses - Map registered; + Map> registered; try { registered = m.areUsersRegistered(new HashSet<>(ns.getList("number"))); } catch (IOException e) { @@ -49,10 +51,11 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { if (outputWriter instanceof JsonWriter) { final var jsonWriter = (JsonWriter) outputWriter; - var jsonUserStatuses = registered.entrySet() - .stream() - .map(entry -> new JsonUserStatus(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); + var jsonUserStatuses = registered.entrySet().stream().map(entry -> { + final var number = entry.getValue().first(); + final var uuid = entry.getValue().second(); + return new JsonUserStatus(entry.getKey(), number, uuid == null ? null : uuid.toString(), uuid != null); + }).collect(Collectors.toList()); jsonWriter.write(jsonUserStatuses); } else { @@ -66,12 +69,18 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { private static final class JsonUserStatus { - public String name; + public final String name; - public boolean isRegistered; + public final String number; - public JsonUserStatus(String name, boolean isRegistered) { + public final String uuid; + + public final boolean isRegistered; + + public JsonUserStatus(String name, String number, String uuid, boolean isRegistered) { this.name = name; + this.number = number; + this.uuid = uuid; this.isRegistered = isRegistered; } } diff --git a/src/main/java/org/asamk/signal/json/JsonMention.java b/src/main/java/org/asamk/signal/json/JsonMention.java index 0fe78bc2..f0c66d00 100644 --- a/src/main/java/org/asamk/signal/json/JsonMention.java +++ b/src/main/java/org/asamk/signal/json/JsonMention.java @@ -6,13 +6,22 @@ import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.util.UUID; + import static org.asamk.signal.util.Util.getLegacyIdentifier; public class JsonMention { @JsonProperty + @Deprecated final String name; + @JsonProperty + final String number; + + @JsonProperty + final String uuid; + @JsonProperty final int start; @@ -20,8 +29,10 @@ public class JsonMention { final int length; JsonMention(SignalServiceDataMessage.Mention mention, Manager m) { - this.name = getLegacyIdentifier(m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), - null))); + final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null)); + this.name = getLegacyIdentifier(address); + this.number = address.getNumber().orNull(); + this.uuid = address.getUuid().transform(UUID::toString).orNull(); this.start = mention.getStart(); this.length = mention.getLength(); } diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index 40f5ed21..c7a3f891 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -10,14 +10,22 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.List; +import java.util.UUID; import static org.asamk.signal.util.Util.getLegacyIdentifier; public class JsonMessageEnvelope { @JsonProperty + @Deprecated final String source; + @JsonProperty + final String sourceNumber; + + @JsonProperty + final String sourceUuid; + @JsonProperty final String sourceName; @@ -55,14 +63,21 @@ public class JsonMessageEnvelope { if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { var source = envelope.getSourceAddress(); this.source = getLegacyIdentifier(source); + this.sourceNumber = source.getNumber().orNull(); + this.sourceUuid = source.getUuid().transform(UUID::toString).orNull(); this.sourceDevice = envelope.getSourceDevice(); this.relay = source.getRelay().orNull(); } else if (envelope.isUnidentifiedSender() && content != null) { - this.source = getLegacyIdentifier(content.getSender()); + final var source = content.getSender(); + this.source = getLegacyIdentifier(source); + this.sourceNumber = source.getNumber().orNull(); + this.sourceUuid = source.getUuid().transform(UUID::toString).orNull(); this.sourceDevice = content.getSenderDevice(); this.relay = null; } else { this.source = null; + this.sourceNumber = null; + this.sourceUuid = null; this.sourceDevice = null; this.relay = null; } @@ -98,6 +113,8 @@ public class JsonMessageEnvelope { public JsonMessageEnvelope(Signal.MessageReceived messageReceived) { source = messageReceived.getSender(); + sourceNumber = null; + sourceUuid = null; sourceName = null; sourceDevice = null; relay = null; @@ -111,6 +128,8 @@ public class JsonMessageEnvelope { public JsonMessageEnvelope(Signal.ReceiptReceived receiptReceived) { source = receiptReceived.getSender(); + sourceNumber = null; + sourceUuid = null; sourceName = null; sourceDevice = null; relay = null; @@ -124,6 +143,8 @@ public class JsonMessageEnvelope { public JsonMessageEnvelope(Signal.SyncMessageReceived messageReceived) { source = messageReceived.getSource(); + sourceNumber = null; + sourceUuid = null; sourceName = null; sourceDevice = null; relay = null; diff --git a/src/main/java/org/asamk/signal/json/JsonQuote.java b/src/main/java/org/asamk/signal/json/JsonQuote.java index f90b492d..ecd31c1a 100644 --- a/src/main/java/org/asamk/signal/json/JsonQuote.java +++ b/src/main/java/org/asamk/signal/json/JsonQuote.java @@ -8,6 +8,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.stream.Collectors; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -18,8 +19,15 @@ public class JsonQuote { final long id; @JsonProperty + @Deprecated final String author; + @JsonProperty + final String authorNumber; + + @JsonProperty + final String authorUuid; + @JsonProperty final String text; @@ -32,7 +40,10 @@ public class JsonQuote { JsonQuote(SignalServiceDataMessage.Quote quote, Manager m) { this.id = quote.getId(); - this.author = getLegacyIdentifier(m.resolveSignalServiceAddress(quote.getAuthor())); + final var address = m.resolveSignalServiceAddress(quote.getAuthor()); + this.author = getLegacyIdentifier(address); + this.authorNumber = address.getNumber().orNull(); + this.authorUuid = address.getUuid().transform(UUID::toString).orNull(); this.text = quote.getText(); if (quote.getMentions() != null && quote.getMentions().size() > 0) { diff --git a/src/main/java/org/asamk/signal/json/JsonReaction.java b/src/main/java/org/asamk/signal/json/JsonReaction.java index e7d40fbe..ecea15fe 100644 --- a/src/main/java/org/asamk/signal/json/JsonReaction.java +++ b/src/main/java/org/asamk/signal/json/JsonReaction.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction; +import java.util.UUID; + import static org.asamk.signal.util.Util.getLegacyIdentifier; public class JsonReaction { @@ -13,8 +15,15 @@ public class JsonReaction { final String emoji; @JsonProperty + @Deprecated final String targetAuthor; + @JsonProperty + final String targetAuthorNumber; + + @JsonProperty + final String targetAuthorUuid; + @JsonProperty final long targetSentTimestamp; @@ -23,7 +32,10 @@ public class JsonReaction { JsonReaction(Reaction reaction, Manager m) { this.emoji = reaction.getEmoji(); - this.targetAuthor = getLegacyIdentifier(m.resolveSignalServiceAddress(reaction.getTargetAuthor())); + final var address = m.resolveSignalServiceAddress(reaction.getTargetAuthor()); + this.targetAuthor = getLegacyIdentifier(address); + this.targetAuthorNumber = address.getNumber().orNull(); + this.targetAuthorUuid = address.getUuid().transform(UUID::toString).orNull(); this.targetSentTimestamp = reaction.getTargetSentTimestamp(); this.isRemove = reaction.isRemove(); } diff --git a/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java index c9d88790..28c9d936 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java @@ -4,22 +4,43 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.Signal; import org.asamk.signal.manager.Manager; -import org.asamk.signal.util.Util; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; +import java.util.UUID; + +import static org.asamk.signal.util.Util.getLegacyIdentifier; + class JsonSyncDataMessage extends JsonDataMessage { @JsonProperty + @Deprecated final String destination; + @JsonProperty + final String destinationNumber; + + @JsonProperty + final String destinationUuid; + JsonSyncDataMessage(SentTranscriptMessage transcriptMessage, Manager m) { super(transcriptMessage.getMessage(), m); - this.destination = transcriptMessage.getDestination().transform(Util::getLegacyIdentifier).orNull(); + if (transcriptMessage.getDestination().isPresent()) { + final var address = transcriptMessage.getDestination().get(); + this.destination = getLegacyIdentifier(address); + this.destinationNumber = address.getNumber().orNull(); + this.destinationUuid = address.getUuid().transform(UUID::toString).orNull(); + } else { + this.destination = null; + this.destinationNumber = null; + this.destinationUuid = null; + } } JsonSyncDataMessage(Signal.SyncMessageReceived messageReceived) { super(messageReceived); - destination = messageReceived.getDestination(); + this.destination = messageReceived.getDestination(); + this.destinationNumber = null; + this.destinationUuid = null; } } diff --git a/src/main/java/org/asamk/signal/json/JsonSyncMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java index 6e992bcb..5c951f0f 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncMessage.java @@ -12,8 +12,6 @@ import java.util.Base64; import java.util.List; import java.util.stream.Collectors; -import static org.asamk.signal.util.Util.getLegacyIdentifier; - enum JsonSyncMessageType { CONTACTS_SYNC, GROUPS_SYNC, @@ -68,8 +66,7 @@ class JsonSyncMessage { this.readMessages = syncMessage.getRead() .get() .stream() - .map(message -> new JsonSyncReadMessage(getLegacyIdentifier(message.getSender()), - message.getTimestamp())) + .map(JsonSyncReadMessage::new) .collect(Collectors.toList()); } else { this.readMessages = null; diff --git a/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java index d65b0672..df307b45 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java @@ -2,16 +2,32 @@ package org.asamk.signal.json; import com.fasterxml.jackson.annotation.JsonProperty; +import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; + +import java.util.UUID; + +import static org.asamk.signal.util.Util.getLegacyIdentifier; + class JsonSyncReadMessage { @JsonProperty + @Deprecated final String sender; + @JsonProperty + final String senderNumber; + + @JsonProperty + final String senderUuid; + @JsonProperty final long timestamp; - public JsonSyncReadMessage(final String sender, final long timestamp) { - this.sender = sender; - this.timestamp = timestamp; + public JsonSyncReadMessage(final ReadMessage readMessage) { + final var sender = readMessage.getSender(); + this.sender = getLegacyIdentifier(sender); + this.senderNumber = sender.getNumber().orNull(); + this.senderUuid = sender.getUuid().transform(UUID::toString).orNull(); + this.timestamp = readMessage.getTimestamp(); } } From 76942ea4582d8f5fc8b527f994ef0090e37bd111 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Aug 2021 13:01:51 +0200 Subject: [PATCH 0754/2005] Add member uuids to listGroup json output --- CHANGELOG.md | 2 + .../signal/commands/ListGroupsCommand.java | 56 +++++++++++++------ 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e2f55be..8af9e358 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## [Unreleased] ### Breaking changes - Removed deprecated `--json` parameter, use `--output=json` instead +- Json output format of `listGroups` command changed: + Members are now arrays of `{"number":"...","uuid":"..."}` instead of arrays of strings. - Removed deprecated fallback data paths, only `$XDG_DATA_HOME/signal-cli` is used now For those still using the old paths (`$HOME/.config/signal`, `$HOME/.config/textsecure`) you need to move those to the new location. diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index b912263f..91b643fc 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -16,6 +16,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; public class ListGroupsCommand implements JsonRpcLocalCommand { @@ -42,6 +43,14 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { .collect(Collectors.toSet()); } + private static Set resolveJsonMembers(Manager m, Set addresses) { + return addresses.stream() + .map(m::resolveSignalServiceAddress) + .map(address -> new JsonGroupMember(address.getNumber().orNull(), + address.getUuid().transform(UUID::toString).orNull())) + .collect(Collectors.toSet()); + } + private static void printGroupPlainText( PlainTextWriter writer, Manager m, GroupInfo group, boolean detailed ) { @@ -86,10 +95,10 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { group.getDescription(), group.isMember(m.getSelfRecipientId()), group.isBlocked(), - resolveMembers(m, group.getMembers()), - resolveMembers(m, group.getPendingMembers()), - resolveMembers(m, group.getRequestingMembers()), - resolveMembers(m, group.getAdminMembers()), + resolveJsonMembers(m, group.getMembers()), + resolveJsonMembers(m, group.getPendingMembers()), + resolveJsonMembers(m, group.getRequestingMembers()), + resolveJsonMembers(m, group.getAdminMembers()), groupInviteLink == null ? null : groupInviteLink.getUrl()); }).collect(Collectors.toList()); @@ -105,17 +114,17 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { private static final class JsonGroup { - public String id; - public String name; - public String description; - public boolean isMember; - public boolean isBlocked; + public final String id; + public final String name; + public final String description; + public final boolean isMember; + public final boolean isBlocked; - public Set members; - public Set pendingMembers; - public Set requestingMembers; - public Set admins; - public String groupInviteLink; + public final Set members; + public final Set pendingMembers; + public final Set requestingMembers; + public final Set admins; + public final String groupInviteLink; public JsonGroup( String id, @@ -123,10 +132,10 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { String description, boolean isMember, boolean isBlocked, - Set members, - Set pendingMembers, - Set requestingMembers, - Set admins, + Set members, + Set pendingMembers, + Set requestingMembers, + Set admins, String groupInviteLink ) { this.id = id; @@ -142,4 +151,15 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { this.groupInviteLink = groupInviteLink; } } + + private static final class JsonGroupMember { + + public final String number; + public final String uuid; + + private JsonGroupMember(final String number, final String uuid) { + this.number = number; + this.uuid = uuid; + } + } } From e3752e733adaad6f04c7d525e1b465a4b46474c7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Aug 2021 14:25:48 +0200 Subject: [PATCH 0755/2005] Implement sendReceipt command Fixes #305 --- .../asamk/signal/manager/HandleAction.java | 3 +- .../org/asamk/signal/manager/Manager.java | 26 ++++++++- man/signal-cli.1.adoc | 13 +++++ .../org/asamk/signal/commands/Commands.java | 1 + .../signal/commands/SendReceiptCommand.java | 56 +++++++++++++++++++ 5 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/asamk/signal/commands/SendReceiptCommand.java diff --git a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java b/lib/src/main/java/org/asamk/signal/manager/HandleAction.java index 08f51590..2396df06 100644 --- a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/HandleAction.java @@ -4,6 +4,7 @@ import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.util.List; import java.util.Objects; interface HandleAction { @@ -23,7 +24,7 @@ class SendReceiptAction implements HandleAction { @Override public void execute(Manager m) throws Throwable { - m.sendReceipt(address, timestamp); + m.sendDeliveryReceipt(address, List.of(timestamp)); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 8aa7ed18..60a196dc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1191,11 +1191,31 @@ public class Manager implements Closeable { return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(resolveRecipient(recipient))); } - void sendReceipt( - SignalServiceAddress remoteAddress, long messageId + public void sendReadReceipt( + String sender, List messageIds + ) throws IOException, UntrustedIdentityException, InvalidNumberException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, + messageIds, + System.currentTimeMillis()); + + sendHelper.sendReceiptMessage(receiptMessage, canonicalizeAndResolveRecipient(sender)); + } + + public void sendViewedReceipt( + String sender, List messageIds + ) throws IOException, UntrustedIdentityException, InvalidNumberException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, + messageIds, + System.currentTimeMillis()); + + sendHelper.sendReceiptMessage(receiptMessage, canonicalizeAndResolveRecipient(sender)); + } + + void sendDeliveryReceipt( + SignalServiceAddress remoteAddress, List messageIds ) throws IOException, UntrustedIdentityException { var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, - List.of(messageId), + messageIds, System.currentTimeMillis()); sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 3df9198b..de554c02 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -199,6 +199,19 @@ Specify the timestamp of the message to which to react. *-r*, *--remove*:: Remove a reaction. +=== sendReceipt + +Send a read or viewed receipt to a previously received message. + +RECIPIENT:: +Specify the sender’s phone number. + +*-t* TIMESTAMP, *--target-timestamp* TIMESTAMP:: +Specify the timestamp of the message to which to react. + +*--type* TYPE:: +Specify the receipt type, either `read` (the default) or `viewed`. + === sendTyping Send typing message to trigger a typing indicator for the recipient. diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index d46d6b8b..90e8e114 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -30,6 +30,7 @@ public class Commands { addCommand(new SendCommand()); addCommand(new SendContactsCommand()); addCommand(new SendReactionCommand()); + addCommand(new SendReceiptCommand()); addCommand(new SendSyncRequestCommand()); addCommand(new SendTypingCommand()); addCommand(new SetPinCommand()); diff --git a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java new file mode 100644 index 00000000..74f48112 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java @@ -0,0 +1,56 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.OutputWriter; +import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.UserErrorException; +import org.asamk.signal.manager.Manager; +import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; +import org.whispersystems.signalservice.api.util.InvalidNumberException; + +import java.io.IOException; + +public class SendReceiptCommand implements JsonRpcLocalCommand { + + @Override + public String getName() { + return "sendReceipt"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Send a read or viewed receipt to a previously received message."); + subparser.addArgument("recipient").help("Specify the sender's phone number.").required(true); + subparser.addArgument("-t", "--target-timestamp") + .type(long.class) + .nargs("+") + .help("Specify the timestamp of the messages for which a receipt should be sent."); + subparser.addArgument("--type").help("Specify the receipt type.").choices("read", "viewed").setDefault("read"); + } + + @Override + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var recipient = ns.getString("recipient"); + + final var targetTimestamps = ns.getList("target-timestamp"); + final var type = ns.getString("type"); + + try { + if ("read".equals(type)) { + m.sendReadReceipt(recipient, targetTimestamps); + } else if ("viewed".equals(type)) { + m.sendViewedReceipt(recipient, targetTimestamps); + } else { + throw new UserErrorException("Unknown receipt type: " + type); + } + } catch (IOException | UntrustedIdentityException e) { + throw new UserErrorException("Failed to send message: " + e.getMessage()); + } catch (InvalidNumberException e) { + throw new UserErrorException("Invalid number: " + e.getMessage()); + } + } +} From a7c9995655d712368cf1e523fab3feecef72d4e2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Aug 2021 16:53:01 +0200 Subject: [PATCH 0756/2005] Print message expiration time in listGroups command --- .../java/org/asamk/signal/commands/ListGroupsCommand.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 91b643fc..a6a9a2f1 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -58,7 +58,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { final var groupInviteLink = group.getGroupInviteLink(); writer.println( - "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Link: {}", + "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Message expiration: {} Link: {}", group.getGroupId().toBase64(), group.getTitle(), group.getDescription(), @@ -68,6 +68,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { resolveMembers(m, group.getPendingMembers()), resolveMembers(m, group.getRequestingMembers()), resolveMembers(m, group.getAdminMembers()), + group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s", groupInviteLink == null ? '-' : groupInviteLink.getUrl()); } else { writer.println("Id: {} Name: {} Active: {} Blocked: {}", @@ -95,6 +96,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { group.getDescription(), group.isMember(m.getSelfRecipientId()), group.isBlocked(), + group.getMessageExpirationTime(), resolveJsonMembers(m, group.getMembers()), resolveJsonMembers(m, group.getPendingMembers()), resolveJsonMembers(m, group.getRequestingMembers()), @@ -119,6 +121,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { public final String description; public final boolean isMember; public final boolean isBlocked; + public final int messageExpirationTime; public final Set members; public final Set pendingMembers; @@ -132,6 +135,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { String description, boolean isMember, boolean isBlocked, + final int messageExpirationTime, Set members, Set pendingMembers, Set requestingMembers, @@ -143,6 +147,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { this.description = description; this.isMember = isMember; this.isBlocked = isBlocked; + this.messageExpirationTime = messageExpirationTime; this.members = members; this.pendingMembers = pendingMembers; From 0a5e836ab69d52c262ea792ed4b84a82dd8a34ca Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Aug 2021 18:47:20 +0200 Subject: [PATCH 0757/2005] Fix rare null pointer exception when receiving message from untrusted identity --- .../org/asamk/signal/manager/Manager.java | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 60a196dc..cde2714f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1843,7 +1843,7 @@ public class Manager implements Closeable { ) throws IOException, InterruptedException { retryFailedReceivedMessages(handler, ignoreAttachments); - Set queuedActions = null; + Set queuedActions = new HashSet<>(); final var signalWebSocket = dependencies.getSignalWebSocket(); signalWebSocket.connect(); @@ -1872,20 +1872,17 @@ public class Manager implements Closeable { // Received indicator that server queue is empty hasCaughtUpWithOldMessages = true; - if (queuedActions != null) { - for (var action : queuedActions) { - try { - action.execute(this); - } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - logger.warn("Message action failed.", e); + for (var action : queuedActions) { + try { + action.execute(this); + } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); } + logger.warn("Message action failed.", e); } - queuedActions.clear(); - queuedActions = null; } + queuedActions.clear(); // Continue to wait another timeout for new messages continue; @@ -1939,9 +1936,6 @@ public class Manager implements Closeable { } } } else { - if (queuedActions == null) { - queuedActions = new HashSet<>(); - } queuedActions.addAll(actions); } } From 4f67ac674b464b07a9ce022a6b19229d511384e2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Aug 2021 19:23:49 +0200 Subject: [PATCH 0758/2005] Trust an identity with its scannable safety numbers from the other device Attention, the scannable fingerprints are asymetric, so the scannable fingerprints from the local listIdentities command can't be used to trust an identity. The scannable fingerprint must come from the other device. --- .../org/asamk/signal/manager/Manager.java | 38 ++++++-- .../asamk/signal/commands/TrustCommand.java | 86 +++++++++++-------- 2 files changed, 82 insertions(+), 42 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index cde2714f..cc57e061 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -81,6 +81,9 @@ import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; +import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; +import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Pair; @@ -2668,6 +2671,25 @@ public class Manager implements Closeable { TrustLevel.TRUSTED_VERIFIED); } + /** + * Trust this the identity with this scannable safety number + * + * @param name username of the identity + * @param safetyNumber Scannable safety number + */ + public boolean trustIdentityVerifiedSafetyNumber(String name, byte[] safetyNumber) throws InvalidNumberException { + var recipientId = canonicalizeAndResolveRecipient(name); + var address = account.getRecipientStore().resolveServiceAddress(recipientId); + return trustIdentity(recipientId, identityKey -> { + final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); + try { + return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber); + } catch (FingerprintVersionMismatchException | FingerprintParsingException e) { + return false; + } + }, TrustLevel.TRUSTED_VERIFIED); + } + /** * Trust all keys of this identity without verification * @@ -2717,21 +2739,23 @@ public class Manager implements Closeable { } public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final var fingerprint = Utils.computeSafetyNumber(capabilities.isUuid(), - account.getSelfAddress(), - getIdentityKeyPair().getPublicKey(), - theirAddress, - theirIdentityKey); + final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); } public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final var fingerprint = Utils.computeSafetyNumber(capabilities.isUuid(), + final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); + } + + private Fingerprint computeSafetyNumberFingerprint( + final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey + ) { + return Utils.computeSafetyNumber(capabilities.isUuid(), account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); } @Deprecated diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 78f22c19..65487ba7 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -11,6 +11,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.util.Hex; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import java.util.Base64; import java.util.Locale; public class TrustCommand implements JsonRpcLocalCommand { @@ -49,44 +50,59 @@ public class TrustCommand implements JsonRpcLocalCommand { } } else { var safetyNumber = ns.getString("verified-safety-number"); - if (safetyNumber != null) { - safetyNumber = safetyNumber.replaceAll(" ", ""); - if (safetyNumber.length() == 66) { - byte[] fingerprintBytes; - try { - fingerprintBytes = Hex.toByteArray(safetyNumber.toLowerCase(Locale.ROOT)); - } catch (Exception e) { - throw new UserErrorException( - "Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); - } - boolean res; - try { - res = m.trustIdentityVerified(number, fingerprintBytes); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); - } - if (!res) { - throw new UserErrorException( - "Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); - } - } else if (safetyNumber.length() == 60) { - boolean res; - try { - res = m.trustIdentityVerifiedSafetyNumber(number, safetyNumber); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); - } - if (!res) { - throw new UserErrorException( - "Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); - } - } else { + if (safetyNumber == null) { + throw new UserErrorException( + "You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER"); + } + + safetyNumber = safetyNumber.replaceAll(" ", ""); + if (safetyNumber.length() == 66) { + byte[] fingerprintBytes; + try { + fingerprintBytes = Hex.toByteArray(safetyNumber.toLowerCase(Locale.ROOT)); + } catch (Exception e) { + throw new UserErrorException( + "Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); + } + boolean res; + try { + res = m.trustIdentityVerified(number, fingerprintBytes); + } catch (InvalidNumberException e) { + throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); + } + if (!res) { + throw new UserErrorException( + "Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); + } + } else if (safetyNumber.length() == 60) { + boolean res; + try { + res = m.trustIdentityVerifiedSafetyNumber(number, safetyNumber); + } catch (InvalidNumberException e) { + throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); + } + if (!res) { + throw new UserErrorException( + "Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); + } + } else { + final byte[] scannableSafetyNumber; + try { + scannableSafetyNumber = Base64.getDecoder().decode(safetyNumber); + } catch (IllegalArgumentException e) { throw new UserErrorException( "Safety number has invalid format, either specify the old hex fingerprint or the new safety number"); } - } else { - throw new UserErrorException( - "You need to specify the fingerprint/safety number you have verified with -v SAFETY_NUMBER"); + boolean res; + try { + res = m.trustIdentityVerifiedSafetyNumber(number, scannableSafetyNumber); + } catch (InvalidNumberException e) { + throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); + } + if (!res) { + throw new UserErrorException( + "Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); + } } } } From 9a9dd3b217860f7b34bba36cb72a0a69356e5924 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 22 Aug 2021 19:28:13 +0200 Subject: [PATCH 0759/2005] Extend error information in json output for received messages from untrusted identity Fixes #91 --- .../org/asamk/signal/JsonReceiveMessageHandler.java | 3 ++- src/main/java/org/asamk/signal/json/JsonError.java | 4 ++++ .../org/asamk/signal/json/JsonMessageEnvelope.java | 13 ++++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java index 4cade799..73c88947 100644 --- a/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonReceiveMessageHandler.java @@ -28,8 +28,9 @@ public class JsonReceiveMessageHandler implements Manager.ReceiveMessageHandler if (exception != null) { object.put("error", new JsonError(exception)); } + if (envelope != null) { - object.put("envelope", new JsonMessageEnvelope(envelope, content, m)); + object.put("envelope", new JsonMessageEnvelope(envelope, content, exception, m)); } jsonWriter.write(object); diff --git a/src/main/java/org/asamk/signal/json/JsonError.java b/src/main/java/org/asamk/signal/json/JsonError.java index d8b3e5f5..45274dea 100644 --- a/src/main/java/org/asamk/signal/json/JsonError.java +++ b/src/main/java/org/asamk/signal/json/JsonError.java @@ -7,7 +7,11 @@ public class JsonError { @JsonProperty final String message; + @JsonProperty + final String type; + public JsonError(Throwable exception) { this.message = exception.getMessage(); + this.type = exception.getClass().getSimpleName(); } } diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index c7a3f891..e53b5ca5 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.Signal; import org.asamk.signal.manager.Manager; +import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -59,7 +60,9 @@ public class JsonMessageEnvelope { @JsonInclude(JsonInclude.Include.NON_NULL) final JsonTypingMessage typingMessage; - public JsonMessageEnvelope(SignalServiceEnvelope envelope, SignalServiceContent content, Manager m) { + public JsonMessageEnvelope( + SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception, Manager m + ) { if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { var source = envelope.getSourceAddress(); this.source = getLegacyIdentifier(source); @@ -74,6 +77,14 @@ public class JsonMessageEnvelope { this.sourceUuid = source.getUuid().transform(UUID::toString).orNull(); this.sourceDevice = content.getSenderDevice(); this.relay = null; + } else if (exception instanceof ProtocolUntrustedIdentityException) { + var e = (ProtocolUntrustedIdentityException) exception; + final var source = m.resolveSignalServiceAddress(e.getSender()); + this.source = getLegacyIdentifier(source); + this.sourceNumber = source.getNumber().orNull(); + this.sourceUuid = source.getUuid().transform(UUID::toString).orNull(); + this.sourceDevice = e.getSenderDevice(); + this.relay = null; } else { this.source = null; this.sourceNumber = null; From 6dd1a216062baa6503d158dcaac5ee1a2cd1b43e Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Aug 2021 14:39:40 +0200 Subject: [PATCH 0760/2005] Handle queued actions also when thread is interrupted --- .../org/asamk/signal/manager/Manager.java | 41 +++++++++---------- .../asamk/signal/commands/DaemonCommand.java | 2 - .../commands/JsonRpcDispatcherCommand.java | 2 - .../asamk/signal/commands/ReceiveCommand.java | 1 - 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index cc57e061..80c5fbbb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1790,16 +1790,7 @@ public class Manager implements Closeable { queuedActions.addAll(actions); } } - for (var action : queuedActions) { - try { - action.execute(this); - } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - logger.warn("Message action failed.", e); - } - } + handleQueuedActions(queuedActions); } private List retryFailedReceivedMessage( @@ -1843,7 +1834,7 @@ public class Manager implements Closeable { boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler - ) throws IOException, InterruptedException { + ) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); Set queuedActions = new HashSet<>(); @@ -1875,16 +1866,7 @@ public class Manager implements Closeable { // Received indicator that server queue is empty hasCaughtUpWithOldMessages = true; - for (var action : queuedActions) { - try { - action.execute(this); - } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - logger.warn("Message action failed.", e); - } - } + handleQueuedActions(queuedActions); queuedActions.clear(); // Continue to wait another timeout for new messages @@ -1892,7 +1874,8 @@ public class Manager implements Closeable { } } catch (AssertionError e) { if (e.getCause() instanceof InterruptedException) { - throw (InterruptedException) e.getCause(); + Thread.currentThread().interrupt(); + break; } else { throw e; } @@ -1970,6 +1953,20 @@ public class Manager implements Closeable { } } } + handleQueuedActions(queuedActions); + } + + private void handleQueuedActions(final Set queuedActions) { + for (var action : queuedActions) { + try { + action.execute(this); + } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + logger.warn("Message action failed.", e); + } + } } private boolean isMessageBlocked( diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 49489293..0591486c 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -134,8 +134,6 @@ public class DaemonCommand implements MultiLocalCommand { break; } catch (IOException e) { logger.warn("Receiving messages failed, retrying", e); - } catch (InterruptedException ignored) { - break; } } }); diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java index 6b5361e5..16d0cf71 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java @@ -168,8 +168,6 @@ public class JsonRpcDispatcherCommand implements LocalCommand { break; } catch (IOException e) { logger.warn("Receiving messages failed, retrying", e); - } catch (InterruptedException e) { - break; } } }); diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 82bd5d8f..f248d662 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -158,7 +158,6 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { handler); } catch (IOException e) { throw new IOErrorException("Error while receiving messages: " + e.getMessage()); - } catch (InterruptedException ignored) { } } } From 6c3106db5df80a23514864405829d20beb04955f Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 23 Aug 2021 15:50:03 +0200 Subject: [PATCH 0761/2005] Add new --trust-new-identities global parameter Closes #360 --- CHANGELOG.md | 3 ++ .../org/asamk/signal/manager/Manager.java | 9 +++- .../signal/manager/ProvisioningManager.java | 6 ++- .../signal/manager/RegistrationManager.java | 6 ++- .../signal/manager/storage/SignalAccount.java | 43 ++++++++++++----- .../storage/identities/IdentityKeyStore.java | 16 +++++-- .../storage/identities/TrustNewIdentity.java | 7 +++ man/signal-cli.1.adoc | 7 +++ src/main/java/org/asamk/signal/App.java | 48 ++++++++++++++----- .../org/asamk/signal/TrustNewIdentityCli.java | 22 +++++++++ 10 files changed, 132 insertions(+), 35 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/storage/identities/TrustNewIdentity.java create mode 100644 src/main/java/org/asamk/signal/TrustNewIdentityCli.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 8af9e358..e2ba6843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ - Removed deprecated fallback data paths, only `$XDG_DATA_HOME/signal-cli` is used now For those still using the old paths (`$HOME/.config/signal`, `$HOME/.config/textsecure`) you need to move those to the new location. +### Added +- New global parameter `--trust-new-identities=always` to allow trusting any new identity key without verification + ## [0.8.5] - 2021-08-07 ### Added - Source name is included in JSON receive output (Thanks @technillogue) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 80c5fbbb..60ee4071 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -43,6 +43,7 @@ import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; @@ -270,7 +271,11 @@ public class Manager implements Closeable { } public static Manager init( - String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent + String username, + File settingsPath, + ServiceEnvironment serviceEnvironment, + String userAgent, + final TrustNewIdentity trustNewIdentity ) throws IOException, NotRegisteredException { var pathConfig = PathConfig.createDefault(settingsPath); @@ -278,7 +283,7 @@ public class Manager implements Closeable { throw new NotRegisteredException(); } - var account = SignalAccount.load(pathConfig.getDataPath(), username, true); + var account = SignalAccount.load(pathConfig.getDataPath(), username, true, trustNewIdentity); if (!account.isRegistered()) { throw new NotRegisteredException(); diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 7801e999..80c214f7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -20,6 +20,7 @@ import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.util.KeyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -121,7 +122,8 @@ public class ProvisioningManager { deviceId, ret.getIdentity(), registrationId, - profileKey); + profileKey, + TrustNewIdentity.ON_FIRST_USE); Manager m = null; try { @@ -161,7 +163,7 @@ public class ProvisioningManager { private boolean canRelinkExistingAccount(final String number) throws IOException { final SignalAccount signalAccount; try { - signalAccount = SignalAccount.load(pathConfig.getDataPath(), number, false); + signalAccount = SignalAccount.load(pathConfig.getDataPath(), number, false, TrustNewIdentity.ON_FIRST_USE); } catch (IOException e) { logger.debug("Account in use or failed to load.", e); return false; diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 653f9cb4..95d43fd6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -21,6 +21,7 @@ import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.util.KeyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -102,12 +103,13 @@ public class RegistrationManager implements Closeable { username, identityKey, registrationId, - profileKey); + profileKey, + TrustNewIdentity.ON_FIRST_USE); return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); } - var account = SignalAccount.load(pathConfig.getDataPath(), username, true); + var account = SignalAccount.load(pathConfig.getDataPath(), username, true, TrustNewIdentity.ON_FIRST_USE); return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 9b61fbb7..477e02dc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -10,6 +10,7 @@ import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.GroupStore; import org.asamk.signal.manager.storage.identities.IdentityKeyStore; +import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.messageCache.MessageCache; import org.asamk.signal.manager.storage.prekeys.PreKeyStore; import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore; @@ -106,12 +107,14 @@ public class SignalAccount implements Closeable { this.lock = lock; } - public static SignalAccount load(File dataPath, String username, boolean waitForLock) throws IOException { + public static SignalAccount load( + File dataPath, String username, boolean waitForLock, final TrustNewIdentity trustNewIdentity + ) throws IOException { final var fileName = getFileName(dataPath, username); final var pair = openFileChannel(fileName, waitForLock); try { var account = new SignalAccount(pair.first(), pair.second()); - account.load(dataPath); + account.load(dataPath, trustNewIdentity); account.migrateLegacyConfigs(); if (!username.equals(account.getUsername())) { @@ -128,7 +131,12 @@ public class SignalAccount implements Closeable { } public static SignalAccount create( - File dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey + File dataPath, + String username, + IdentityKeyPair identityKey, + int registrationId, + ProfileKey profileKey, + final TrustNewIdentity trustNewIdentity ) throws IOException { IOUtils.createPrivateDirectories(dataPath); var fileName = getFileName(dataPath, username); @@ -142,7 +150,7 @@ public class SignalAccount implements Closeable { account.username = username; account.profileKey = profileKey; - account.initStores(dataPath, identityKey, registrationId); + account.initStores(dataPath, identityKey, registrationId, trustNewIdentity); account.groupStore = new GroupStore(getGroupCachePath(dataPath, username), account.recipientStore::resolveRecipient, account::saveGroupStore); @@ -157,7 +165,10 @@ public class SignalAccount implements Closeable { } private void initStores( - final File dataPath, final IdentityKeyPair identityKey, final int registrationId + final File dataPath, + final IdentityKeyPair identityKey, + final int registrationId, + final TrustNewIdentity trustNewIdentity ) throws IOException { recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients); @@ -167,7 +178,8 @@ public class SignalAccount implements Closeable { identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username), recipientStore::resolveRecipient, identityKey, - registrationId); + registrationId, + trustNewIdentity); signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, @@ -186,7 +198,8 @@ public class SignalAccount implements Closeable { int deviceId, IdentityKeyPair identityKey, int registrationId, - ProfileKey profileKey + ProfileKey profileKey, + final TrustNewIdentity trustNewIdentity ) throws IOException { IOUtils.createPrivateDirectories(dataPath); var fileName = getFileName(dataPath, username); @@ -199,10 +212,11 @@ public class SignalAccount implements Closeable { deviceId, identityKey, registrationId, - profileKey); + profileKey, + trustNewIdentity); } - final var account = load(dataPath, username, true); + final var account = load(dataPath, username, true, trustNewIdentity); account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey); account.recipientStore.resolveRecipientTrusted(account.getSelfAddress()); account.sessionStore.archiveAllSessions(); @@ -227,7 +241,8 @@ public class SignalAccount implements Closeable { int deviceId, IdentityKeyPair identityKey, int registrationId, - ProfileKey profileKey + ProfileKey profileKey, + final TrustNewIdentity trustNewIdentity ) throws IOException { var fileName = getFileName(dataPath, username); IOUtils.createPrivateFile(fileName); @@ -237,7 +252,7 @@ public class SignalAccount implements Closeable { account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey); - account.initStores(dataPath, identityKey, registrationId); + account.initStores(dataPath, identityKey, registrationId, trustNewIdentity); account.groupStore = new GroupStore(getGroupCachePath(dataPath, username), account.recipientStore::resolveRecipient, account::saveGroupStore); @@ -339,7 +354,9 @@ public class SignalAccount implements Closeable { return !(!f.exists() || f.isDirectory()); } - private void load(File dataPath) throws IOException { + private void load( + File dataPath, final TrustNewIdentity trustNewIdentity + ) throws IOException { JsonNode rootNode; synchronized (fileChannel) { fileChannel.position(0); @@ -428,7 +445,7 @@ public class SignalAccount implements Closeable { migratedLegacyConfig = true; } - initStores(dataPath, identityKeyPair, registrationId); + initStores(dataPath, identityKeyPair, registrationId, trustNewIdentity); migratedLegacyConfig = loadLegacyStores(rootNode, legacySignalProtocolStore) || migratedLegacyConfig; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java index d1cfceda..0cbdf347 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java @@ -42,17 +42,20 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden private final RecipientResolver resolver; private final IdentityKeyPair identityKeyPair; private final int localRegistrationId; + private final TrustNewIdentity trustNewIdentity; public IdentityKeyStore( final File identitiesPath, final RecipientResolver resolver, final IdentityKeyPair identityKeyPair, - final int localRegistrationId + final int localRegistrationId, + final TrustNewIdentity trustNewIdentity ) { this.identitiesPath = identitiesPath; this.resolver = resolver; this.identityKeyPair = identityKeyPair; this.localRegistrationId = localRegistrationId; + this.trustNewIdentity = trustNewIdentity; } @Override @@ -80,7 +83,10 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden return false; } - final var trustLevel = identityInfo == null ? TrustLevel.TRUSTED_UNVERIFIED : TrustLevel.UNTRUSTED; + final var trustLevel = trustNewIdentity == TrustNewIdentity.ALWAYS || ( + trustNewIdentity == TrustNewIdentity.ON_FIRST_USE && identityInfo == null + ) ? TrustLevel.TRUSTED_UNVERIFIED : TrustLevel.UNTRUSTED; + logger.debug("Storing new identity for recipient {} with trust {}", recipientId, trustLevel); final var newIdentityInfo = new IdentityInfo(recipientId, identityKey, trustLevel, added); storeIdentityLocked(recipientId, newIdentityInfo); return true; @@ -108,13 +114,17 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden @Override public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) { + if (trustNewIdentity == TrustNewIdentity.ALWAYS) { + return true; + } + var recipientId = resolveRecipient(address.getName()); synchronized (cachedIdentities) { final var identityInfo = loadIdentityLocked(recipientId); if (identityInfo == null) { // Identity not found - return true; + return trustNewIdentity == TrustNewIdentity.ON_FIRST_USE; } // TODO implement possibility for different handling of incoming/outgoing trust decisions diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/TrustNewIdentity.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/TrustNewIdentity.java new file mode 100644 index 00000000..b2db73a3 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/TrustNewIdentity.java @@ -0,0 +1,7 @@ +package org.asamk.signal.manager.storage.identities; + +public enum TrustNewIdentity { + ALWAYS, + ON_FIRST_USE, + NEVER +} diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index de554c02..b8251eb1 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -58,6 +58,13 @@ Make request via system dbus. *-o* OUTPUT-MODE, *--output* OUTPUT-MODE:: Specify if you want commands to output in either "plain-text" mode or in "json". Defaults to "plain-text" +*--trust-new-identities* TRUST-MODE:: +Choose when to trust new identities: +- `on-first-use` (default): Trust the first seen identity key from new users, + changed keys must be verified manually +- `always`: Trust any new identity key without verification +- `never`: Don't trust any unknown identity key, every key must be verified manually + == Commands === register diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 6b31e2bc..1ff1a909 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -24,6 +24,7 @@ import org.asamk.signal.manager.ProvisioningManager; import org.asamk.signal.manager.RegistrationManager; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; +import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.util.IOUtils; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; @@ -74,6 +75,11 @@ public class App { .type(Arguments.enumStringType(ServiceEnvironmentCli.class)) .setDefault(ServiceEnvironmentCli.LIVE); + parser.addArgument("--trust-new-identities") + .help("Choose when to trust new identities.") + .type(Arguments.enumStringType(TrustNewIdentityCli.class)) + .setDefault(TrustNewIdentityCli.ON_FIRST_USE); + var subparsers = parser.addSubparsers().title("subcommands").dest("command"); Commands.getCommandSubparserAttachers().forEach((key, value) -> { @@ -125,11 +131,6 @@ public class App { dataPath = getDefaultDataPath(); } - final var serviceEnvironmentCli = ns.get("service-environment"); - final var serviceEnvironment = serviceEnvironmentCli == ServiceEnvironmentCli.LIVE - ? ServiceEnvironment.LIVE - : ServiceEnvironment.SANDBOX; - if (!ServiceConfig.getCapabilities().isGv2()) { logger.warn("WARNING: Support for new group V2 is disabled," + " because the required native library dependency is missing: libzkgroup"); @@ -139,6 +140,16 @@ public class App { throw new UserErrorException("Missing required native library dependency: libsignal-client"); } + final var serviceEnvironmentCli = ns.get("service-environment"); + final var serviceEnvironment = serviceEnvironmentCli == ServiceEnvironmentCli.LIVE + ? ServiceEnvironment.LIVE + : ServiceEnvironment.SANDBOX; + + final var trustNewIdentityCli = ns.get("trust-new-identities"); + final var trustNewIdentity = trustNewIdentityCli == TrustNewIdentityCli.ON_FIRST_USE + ? TrustNewIdentity.ON_FIRST_USE + : trustNewIdentityCli == TrustNewIdentityCli.ALWAYS ? TrustNewIdentity.ALWAYS : TrustNewIdentity.NEVER; + if (command instanceof ProvisioningCommand) { if (username != null) { throw new UserErrorException("You cannot specify a username (phone number) when linking"); @@ -156,7 +167,8 @@ public class App { dataPath, serviceEnvironment, usernames, - outputWriter); + outputWriter, + trustNewIdentity); return; } @@ -181,7 +193,12 @@ public class App { throw new UserErrorException("Command only works via dbus"); } - handleLocalCommand((LocalCommand) command, username, dataPath, serviceEnvironment, outputWriter); + handleLocalCommand((LocalCommand) command, + username, + dataPath, + serviceEnvironment, + outputWriter, + trustNewIdentity); } private void handleProvisioningCommand( @@ -222,9 +239,10 @@ public class App { final String username, final File dataPath, final ServiceEnvironment serviceEnvironment, - final OutputWriter outputWriter + final OutputWriter outputWriter, + final TrustNewIdentity trustNewIdentity ) throws CommandException { - try (var m = loadManager(username, dataPath, serviceEnvironment)) { + try (var m = loadManager(username, dataPath, serviceEnvironment, trustNewIdentity)) { command.handleCommand(ns, m, outputWriter); } catch (IOException e) { logger.warn("Cleanup failed", e); @@ -236,12 +254,13 @@ public class App { final File dataPath, final ServiceEnvironment serviceEnvironment, final List usernames, - final OutputWriter outputWriter + final OutputWriter outputWriter, + final TrustNewIdentity trustNewIdentity ) throws CommandException { final var managers = new ArrayList(); for (String u : usernames) { try { - managers.add(loadManager(u, dataPath, serviceEnvironment)); + managers.add(loadManager(u, dataPath, serviceEnvironment, trustNewIdentity)); } catch (CommandException e) { logger.warn("Ignoring {}: {}", u, e.getMessage()); } @@ -269,11 +288,14 @@ public class App { } private Manager loadManager( - final String username, final File dataPath, final ServiceEnvironment serviceEnvironment + final String username, + final File dataPath, + final ServiceEnvironment serviceEnvironment, + final TrustNewIdentity trustNewIdentity ) throws CommandException { Manager manager; try { - manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT); + manager = Manager.init(username, dataPath, serviceEnvironment, BaseConfig.USER_AGENT, trustNewIdentity); } catch (NotRegisteredException e) { throw new UserErrorException("User " + username + " is not registered."); } catch (Throwable e) { diff --git a/src/main/java/org/asamk/signal/TrustNewIdentityCli.java b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java new file mode 100644 index 00000000..5cc36bbd --- /dev/null +++ b/src/main/java/org/asamk/signal/TrustNewIdentityCli.java @@ -0,0 +1,22 @@ +package org.asamk.signal; + +public enum TrustNewIdentityCli { + ALWAYS { + @Override + public String toString() { + return "always"; + } + }, + ON_FIRST_USE { + @Override + public String toString() { + return "on-first-use"; + } + }, + NEVER { + @Override + public String toString() { + return "never"; + } + }, +} From 8c661c23be6e2232af902f0d20cd7e747077ab9d Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 24 Aug 2021 12:37:40 +0200 Subject: [PATCH 0762/2005] Accept single values for jsonrpc requests where a list is expected --- .../asamk/signal/commands/JsonRpcLocalCommand.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java index 06124ffd..24b45ee8 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java @@ -40,6 +40,7 @@ public interface JsonRpcLocalCommand extends JsonRpcCommand> super(attrs); } + @Override public T get(String dest) { final T value = super.get(dest); if (value != null) { @@ -52,9 +53,13 @@ public interface JsonRpcLocalCommand extends JsonRpcCommand> @Override public List getList(final String dest) { - final List value = super.getList(dest); - if (value != null) { - return value; + try { + final List value = super.getList(dest); + if (value != null) { + return value; + } + } catch (ClassCastException e) { + return List.of(this.get(dest)); } return super.getList(dest + "s"); From 23a006c31154fffe5a20c5daece08428fb2447d5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Aug 2021 12:23:07 +0200 Subject: [PATCH 0763/2005] Enable announcement group capability --- .../java/org/asamk/signal/manager/config/ServiceConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index 3567bd7f..3f97be6b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -39,7 +39,7 @@ public class ServiceConfig { false, zkGroupAvailable, false, - false); + true); try { TrustStore contactTrustStore = new IasTrustStore(); From cd7172ee57049d292e7916ef58886d74bc82fcf7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 24 Aug 2021 12:36:09 +0200 Subject: [PATCH 0764/2005] Refactor message send methods --- .../org/asamk/signal/manager/Manager.java | 36 ++++++++++--------- .../org/asamk/signal/manager/api/Message.java | 22 ++++++++++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 7 ++-- 3 files changed, 45 insertions(+), 20 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/Message.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 60ee4071..ae3f6aed 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -17,6 +17,7 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; @@ -697,12 +698,10 @@ public class Manager implements Closeable { } public Pair> sendGroupMessage( - String messageText, List attachments, GroupId groupId + Message message, GroupId groupId ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - final var messageBuilder = createMessageBuilder().withBody(messageText); - if (attachments != null) { - messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); - } + final var messageBuilder = createMessageBuilder(); + applyMessage(messageBuilder, message); return sendHelper.sendAsGroupMessage(messageBuilder, groupId); } @@ -1230,11 +1229,19 @@ public class Manager implements Closeable { } public Pair> sendMessage( - String messageText, List attachments, List recipients + Message message, List recipients ) throws IOException, AttachmentInvalidException, InvalidNumberException { - final var messageBuilder = createMessageBuilder().withBody(messageText); - if (attachments != null) { - var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); + final var messageBuilder = createMessageBuilder(); + applyMessage(messageBuilder, message); + return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); + } + + private void applyMessage( + final SignalServiceDataMessage.Builder messageBuilder, final Message message + ) throws AttachmentInvalidException, IOException { + messageBuilder.withBody(message.getMessageText()); + if (message.getAttachments() != null) { + var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(message.getAttachments()); // Upload attachments here, so we only upload once even for multiple recipients var messageSender = dependencies.getMessageSender(); @@ -1249,16 +1256,11 @@ public class Manager implements Closeable { messageBuilder.withAttachments(attachmentPointers); } - return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); } - public Pair sendSelfMessage( - String messageText, List attachments - ) throws IOException, AttachmentInvalidException { - final var messageBuilder = createMessageBuilder().withBody(messageText); - if (attachments != null) { - messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); - } + public Pair sendSelfMessage(final Message message) throws IOException, AttachmentInvalidException { + final var messageBuilder = createMessageBuilder(); + applyMessage(messageBuilder, message); return sendHelper.sendSelfMessage(messageBuilder); } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Message.java b/lib/src/main/java/org/asamk/signal/manager/api/Message.java new file mode 100644 index 00000000..dee18524 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/Message.java @@ -0,0 +1,22 @@ +package org.asamk.signal.manager.api; + +import java.util.List; + +public class Message { + + private final String messageText; + private final List attachments; + + public Message(final String messageText, final List attachments) { + this.messageText = messageText; + this.attachments = attachments; + } + + public String getMessageText() { + return messageText; + } + + public List getAttachments() { + return attachments; + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index a214ef88..ab2bcda0 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -5,6 +5,7 @@ import org.asamk.signal.BaseConfig; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; +import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupNotFoundException; @@ -100,7 +101,7 @@ public class DbusSignalImpl implements Signal { @Override public long sendMessage(final String message, final List attachments, final List recipients) { try { - final var results = m.sendMessage(message, attachments, recipients); + final var results = m.sendMessage(new Message(message, attachments), recipients); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (InvalidNumberException e) { @@ -188,7 +189,7 @@ public class DbusSignalImpl implements Signal { final String message, final List attachments ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { try { - final var results = m.sendSelfMessage(message, attachments); + final var results = m.sendSelfMessage(new Message(message, attachments)); checkSendMessageResult(results.first(), results.second()); return results.first(); } catch (AttachmentInvalidException e) { @@ -213,7 +214,7 @@ public class DbusSignalImpl implements Signal { @Override public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { try { - var results = m.sendGroupMessage(message, attachments, GroupId.unknownVersion(groupId)); + var results = m.sendGroupMessage(new Message(message, attachments), GroupId.unknownVersion(groupId)); checkSendMessageResults(results.first(), results.second()); return results.first(); } catch (IOException e) { From 467a48bac508b56f84dce7ee0b81a22fd0d32161 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Aug 2021 12:22:53 +0200 Subject: [PATCH 0765/2005] Add RecipientIdentifier as external Manager interface --- .../org/asamk/signal/manager/Manager.java | 380 +++++++++--------- .../manager/api/RecipientIdentifier.java | 112 ++++++ .../manager/api/SendGroupMessageResults.java | 26 ++ .../manager/api/SendMessageResults.java | 27 ++ .../signal/manager/helper/SendHelper.java | 62 ++- src/main/java/org/asamk/Signal.java | 29 +- .../asamk/signal/ReceiveMessageHandler.java | 13 +- .../asamk/signal/commands/BlockCommand.java | 21 +- .../signal/commands/JoinGroupCommand.java | 2 +- .../commands/ListIdentitiesCommand.java | 9 +- .../signal/commands/QuitGroupCommand.java | 25 +- .../signal/commands/RemoteDeleteCommand.java | 73 ++-- .../asamk/signal/commands/SendCommand.java | 102 +++-- .../signal/commands/SendReactionCommand.java | 89 ++-- .../signal/commands/SendReceiptCommand.java | 7 +- .../signal/commands/SendTypingCommand.java | 47 +-- .../asamk/signal/commands/TrustCommand.java | 33 +- .../asamk/signal/commands/UnblockCommand.java | 24 +- .../signal/commands/UpdateContactCommand.java | 11 +- .../signal/commands/UpdateGroupCommand.java | 59 +-- .../org/asamk/signal/dbus/DbusSignalImpl.java | 273 ++++++++----- .../signal/json/JsonMessageEnvelope.java | 3 +- .../org/asamk/signal/util/CommandUtil.java | 99 +++++ .../org/asamk/signal/util/ErrorUtils.java | 21 +- src/main/java/org/asamk/signal/util/Util.java | 6 - 25 files changed, 958 insertions(+), 595 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/SendGroupMessageResults.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/SendMessageResults.java create mode 100644 src/main/java/org/asamk/signal/util/CommandUtil.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index ae3f6aed..92dcf4d6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -18,6 +18,9 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.SendGroupMessageResults; +import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; @@ -156,6 +159,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collection; import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -354,9 +358,6 @@ public class Manager implements Closeable { .filter(s -> !s.isEmpty()) .collect(Collectors.toSet())); - // Store numbers as recipients so we have the number/uuid association - contactDetails.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); - return numbers.stream().collect(Collectors.toMap(n -> n, n -> { final var number = canonicalizedNumbers.get(n); final var uuid = contactDetails.get(number); @@ -697,31 +698,9 @@ public class Manager implements Closeable { return account.getGroupStore().getGroups(); } - public Pair> sendGroupMessage( - Message message, GroupId groupId - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - final var messageBuilder = createMessageBuilder(); - applyMessage(messageBuilder, message); - - return sendHelper.sendAsGroupMessage(messageBuilder, groupId); - } - - public Pair> sendGroupMessageReaction( - String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId - ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException { - var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor); - var reaction = new SignalServiceDataMessage.Reaction(emoji, - remove, - resolveSignalServiceAddress(targetAuthorRecipientId), - targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withReaction(reaction); - - return sendHelper.sendAsGroupMessage(messageBuilder, groupId); - } - - public Pair> sendQuitGroupMessage( - GroupId groupId, Set groupAdmins - ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException { + public SendGroupMessageResults sendQuitGroupMessage( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { var group = getGroupForUpdating(groupId); if (group instanceof GroupInfoV1) { return quitGroupV1((GroupInfoV1) group); @@ -737,19 +716,19 @@ public class Manager implements Closeable { } } - private Pair> quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { + private SendGroupMessageResults quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) .withId(groupInfoV1.getGroupId().serialize()) .build(); - var messageBuilder = createMessageBuilder().asGroupMessage(group); + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); groupInfoV1.removeMember(account.getSelfRecipientId()); account.getGroupStore().updateGroup(groupInfoV1); - return sendHelper.sendGroupMessage(messageBuilder.build(), + return sendGroupMessage(messageBuilder, groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } - private Pair> quitGroupV2( + private SendGroupMessageResults quitGroupV2( final GroupInfoV2 groupInfoV2, final Set newAdmins ) throws LastGroupAdminException, IOException { final var currentAdmins = groupInfoV2.getAdminMembers(); @@ -764,9 +743,10 @@ public class Manager implements Closeable { } final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); - var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); account.getGroupStore().updateGroup(groupInfoV2); - return sendHelper.sendGroupMessage(messageBuilder.build(), + + var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); + return sendGroupMessage(messageBuilder, groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } @@ -775,13 +755,13 @@ public class Manager implements Closeable { avatarStore.deleteGroupAvatar(groupId); } - public Pair> createGroup( - String name, List members, File avatarFile - ) throws IOException, AttachmentInvalidException, InvalidNumberException { - return createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); + public Pair createGroup( + String name, Set members, File avatarFile + ) throws IOException, AttachmentInvalidException { + return createGroupInternal(name, members == null ? null : getRecipientIds(members), avatarFile); } - private Pair> createGroup( + private Pair createGroupInternal( String name, Set members, File avatarFile ) throws IOException, AttachmentInvalidException { final var selfRecipientId = account.getSelfRecipientId(); @@ -794,13 +774,12 @@ public class Manager implements Closeable { members == null ? Set.of() : members, avatarFile); - SignalServiceDataMessage.Builder messageBuilder; if (gv2Pair == null) { // Failed to create v2 group, creating v1 group instead var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); gv1.addMembers(List.of(selfRecipientId)); final var result = updateGroupV1(gv1, name, members, avatarFile); - return new Pair<>(gv1.getGroupId(), result.second()); + return new Pair<>(gv1.getGroupId(), result); } final var gv2 = gv2Pair.first(); @@ -811,22 +790,23 @@ public class Manager implements Closeable { avatarStore.storeGroupAvatar(gv2.getGroupId(), outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); } - messageBuilder = getGroupUpdateMessageBuilder(gv2, null); + account.getGroupStore().updateGroup(gv2); - final var result = sendHelper.sendGroupMessage(messageBuilder.build(), - gv2.getMembersIncludingPendingWithout(selfRecipientId)); - return new Pair<>(gv2.getGroupId(), result.second()); + final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null); + + final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); + return new Pair<>(gv2.getGroupId(), result); } - public Pair> updateGroup( + public SendGroupMessageResults updateGroup( GroupId groupId, String name, String description, - List members, - List removeMembers, - List admins, - List removeAdmins, + Set members, + Set removeMembers, + Set admins, + Set removeAdmins, boolean resetGroupLink, GroupLinkState groupLinkState, GroupPermission addMemberPermission, @@ -834,8 +814,8 @@ public class Manager implements Closeable { File avatarFile, Integer expirationTimer, Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { - return updateGroup(groupId, + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { + return updateGroupInternal(groupId, name, description, members == null ? null : getRecipientIds(members), @@ -851,7 +831,7 @@ public class Manager implements Closeable { isAnnouncementGroup); } - private Pair> updateGroup( + private SendGroupMessageResults updateGroupInternal( final GroupId groupId, final String name, final String description, @@ -913,16 +893,15 @@ public class Manager implements Closeable { return result; } - private Pair> updateGroupV1( + private SendGroupMessageResults updateGroupV1( final GroupInfoV1 gv1, final String name, final Set members, final File avatarFile ) throws IOException, AttachmentInvalidException { updateGroupV1Details(gv1, name, members, avatarFile); - var messageBuilder = getGroupUpdateMessageBuilder(gv1); account.getGroupStore().updateGroup(gv1); - return sendHelper.sendGroupMessage(messageBuilder.build(), - gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + var messageBuilder = getGroupUpdateMessageBuilder(gv1); + return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } private void updateGroupV1Details( @@ -963,7 +942,7 @@ public class Manager implements Closeable { } } - private Pair> updateGroupV2( + private SendGroupMessageResults updateGroupV2( final GroupInfoV2 group, final String name, final String description, @@ -979,7 +958,7 @@ public class Manager implements Closeable { final Integer expirationTimer, final Boolean isAnnouncementGroup ) throws IOException { - Pair> result = null; + SendGroupMessageResults result = null; if (group.isPendingMember(account.getSelfRecipientId())) { var groupGroupChangePair = groupV2Helper.acceptInvite(group); result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); @@ -1080,7 +1059,7 @@ public class Manager implements Closeable { return result; } - public Pair> joinGroup( + public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), @@ -1094,25 +1073,74 @@ public class Manager implements Closeable { if (group.getGroup() == null) { // Only requested member, can't send update to group members - return new Pair<>(group.getGroupId(), List.of()); + return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of())); } final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); - return new Pair<>(group.getGroupId(), result.second()); + return new Pair<>(group.getGroupId(), result); } - private Pair> sendUpdateGroupV2Message( + private SendGroupMessageResults sendUpdateGroupV2Message( GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange ) throws IOException { final var selfRecipientId = account.getSelfRecipientId(); final var members = group.getMembersIncludingPendingWithout(selfRecipientId); group.setGroup(newDecryptedGroup, this::resolveRecipient); members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId)); + account.getGroupStore().updateGroup(group); final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); - account.getGroupStore().updateGroup(group); - return sendHelper.sendGroupMessage(messageBuilder.build(), members); + return sendGroupMessage(messageBuilder, members); + } + + public SendMessageResults sendMessage( + SignalServiceDataMessage.Builder messageBuilder, Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + var results = new HashMap>(); + long timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); + for (final var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + final var result = sendHelper.sendMessage(messageBuilder, recipientId); + results.put(recipient, List.of(result)); + } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { + final var result = sendHelper.sendSelfMessage(messageBuilder); + results.put(recipient, List.of(result)); + } else if (recipient instanceof RecipientIdentifier.Group) { + final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId); + results.put(recipient, result); + } + } + return new SendMessageResults(timestamp, results); + } + + public void sendTypingMessage( + SignalServiceTypingMessage.Action action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + final var timestamp = System.currentTimeMillis(); + for (var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + sendHelper.sendTypingMessage(message, recipientId); + } else if (recipient instanceof RecipientIdentifier.Group) { + final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); + sendHelper.sendGroupTypingMessage(message, groupId); + } + } + } + + private SendGroupMessageResults sendGroupMessage( + final SignalServiceDataMessage.Builder messageBuilder, final Set members + ) throws IOException { + final var timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); + final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members); + return new SendGroupMessageResults(timestamp, results); } private static int currentTimeDays() { @@ -1138,7 +1166,7 @@ public class Manager implements Closeable { } } - Pair> sendGroupInfoMessage( + SendGroupMessageResults sendGroupInfoMessage( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { GroupInfoV1 g; @@ -1156,7 +1184,7 @@ public class Manager implements Closeable { var messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(recipientId)); + return sendGroupMessage(messageBuilder, Set.of(recipientId)); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { @@ -1177,45 +1205,49 @@ public class Manager implements Closeable { throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); } - return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.getMessageExpirationTime()); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) .withRevision(g.getGroup().getRevision()) .withSignedGroupChange(signedGroupChange); - return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.getMessageExpirationTime()); } - Pair> sendGroupInfoRequest( + SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); - var messageBuilder = createMessageBuilder().asGroupMessage(group.build()); + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); // Send group info request message to the recipient who sent us a message with this groupId - return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(resolveRecipient(recipient))); + return sendGroupMessage(messageBuilder, Set.of(resolveRecipient(recipient))); } public void sendReadReceipt( - String sender, List messageIds - ) throws IOException, UntrustedIdentityException, InvalidNumberException { + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, System.currentTimeMillis()); - sendHelper.sendReceiptMessage(receiptMessage, canonicalizeAndResolveRecipient(sender)); + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); } public void sendViewedReceipt( - String sender, List messageIds - ) throws IOException, UntrustedIdentityException, InvalidNumberException { + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, messageIds, System.currentTimeMillis()); - sendHelper.sendReceiptMessage(receiptMessage, canonicalizeAndResolveRecipient(sender)); + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); } void sendDeliveryReceipt( @@ -1228,12 +1260,12 @@ public class Manager implements Closeable { sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); } - public Pair> sendMessage( - Message message, List recipients - ) throws IOException, AttachmentInvalidException, InvalidNumberException { - final var messageBuilder = createMessageBuilder(); + public SendMessageResults sendMessage( + Message message, Set recipients + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException { + final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); - return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); + return sendMessage(messageBuilder, recipients); } private void applyMessage( @@ -1258,48 +1290,41 @@ public class Manager implements Closeable { } } - public Pair sendSelfMessage(final Message message) throws IOException, AttachmentInvalidException { - final var messageBuilder = createMessageBuilder(); - applyMessage(messageBuilder, message); - return sendHelper.sendSelfMessage(messageBuilder); - } - - public Pair> sendRemoteDeleteMessage( - long targetSentTimestamp, List recipients - ) throws IOException, InvalidNumberException { - var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); - return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); - } - - public Pair> sendGroupRemoteDeleteMessage( - long targetSentTimestamp, GroupId groupId + public SendMessageResults sendRemoteDeleteMessage( + long targetSentTimestamp, Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); - return sendHelper.sendAsGroupMessage(messageBuilder, groupId); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); + return sendMessage(messageBuilder, recipients); } - public Pair> sendMessageReaction( - String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients - ) throws IOException, InvalidNumberException { - var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor); + public SendMessageResults sendMessageReaction( + String emoji, + boolean remove, + RecipientIdentifier.Single targetAuthor, + long targetSentTimestamp, + Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + var targetAuthorRecipientId = resolveRecipient(targetAuthor); var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); - final var messageBuilder = createMessageBuilder().withReaction(reaction); - return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); + return sendMessage(messageBuilder, recipients); } - public Pair> sendEndSessionMessage(List recipients) throws IOException, InvalidNumberException { - var messageBuilder = createMessageBuilder().asEndSessionMessage(); + public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { + var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); - final var recipientIds = getRecipientIds(recipients); try { - return sendHelper.sendMessage(messageBuilder, recipientIds); + return sendMessage(messageBuilder, + recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new AssertionError(e); } finally { - for (var recipientId : recipientIds) { + for (var recipient : recipients) { + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); handleEndSession(recipientId); } } @@ -1312,23 +1337,25 @@ public class Manager implements Closeable { } } - public void setContactName(String number, String name) throws InvalidNumberException, NotMasterDeviceException { + public void setContactName( + RecipientIdentifier.Single recipient, String name + ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - final var recipientId = canonicalizeAndResolveRecipient(number); + final var recipientId = resolveRecipient(recipient); var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); account.getContactStore().storeContact(recipientId, builder.withName(name).build()); } public void setContactBlocked( - String number, boolean blocked - ) throws InvalidNumberException, NotMasterDeviceException { + RecipientIdentifier.Single recipient, boolean blocked + ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - setContactBlocked(canonicalizeAndResolveRecipient(number), blocked); + setContactBlocked(resolveRecipient(recipient), blocked); } private void setContactBlocked(RecipientId recipientId, boolean blocked) { @@ -1357,20 +1384,20 @@ public class Manager implements Closeable { .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); } - private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException { - final var messageBuilder = createMessageBuilder().asExpirationUpdate(); - sendHelper.sendMessage(messageBuilder, Set.of(recipientId)); - } - /** * Change the expiration timer for a contact */ public void setExpirationTimer( - String number, int messageExpirationTimer - ) throws IOException, InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(number); + RecipientIdentifier.Single recipient, int messageExpirationTimer + ) throws IOException { + var recipientId = resolveRecipient(recipient); setExpirationTimer(recipientId, messageExpirationTimer); - sendExpirationTimerUpdate(recipientId); + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + try { + sendMessage(messageBuilder, Set.of(recipient)); + } catch (NotAGroupMemberException | GroupNotFoundException e) { + throw new AssertionError(e); + } } /** @@ -1385,7 +1412,7 @@ public class Manager implements Closeable { } private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { - final var messageBuilder = createMessageBuilder().asExpirationUpdate(); + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); sendHelper.sendAsGroupMessage(messageBuilder, groupId); } @@ -1395,7 +1422,7 @@ public class Manager implements Closeable { * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file * @return if successful, returns the URL to install the sticker pack in the signal app */ - public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException { + public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); var messageSender = dependencies.getMessageSender(); @@ -1414,7 +1441,7 @@ public class Manager implements Closeable { "pack_id=" + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) + "&pack_key=" - + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)).toString(); + + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)); } catch (URISyntaxException e) { throw new AssertionError(e); } @@ -1484,12 +1511,12 @@ public class Manager implements Closeable { return certificate; } - private Set getRecipientIds(Collection numbers) throws InvalidNumberException { - final var signalServiceAddresses = new HashSet(numbers.size()); + private Set getRecipientIds(Collection recipients) { + final var signalServiceAddresses = new HashSet(recipients.size()); final var addressesMissingUuid = new HashSet(); - for (var number : numbers) { - final var resolvedAddress = resolveSignalServiceAddress(canonicalizeAndResolveRecipient(number)); + for (var number : recipients) { + final var resolvedAddress = resolveSignalServiceAddress(resolveRecipient(number)); if (resolvedAddress.getUuid().isPresent()) { signalServiceAddresses.add(resolvedAddress); } else { @@ -1534,40 +1561,26 @@ public class Manager implements Closeable { } private Map getRegisteredUsers(final Set numbers) throws IOException { + final Map registeredUsers; try { - return dependencies.getAccountManager() + registeredUsers = dependencies.getAccountManager() .getRegisteredUsers(ServiceConfig.getIasKeyStore(), numbers, serviceEnvironmentConfig.getCdsMrenclave()); } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { throw new IOException(e); } + + // Store numbers as recipients so we have the number/uuid association + registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); + + return registeredUsers; } public void sendTypingMessage( - TypingAction action, Set recipients - ) throws IOException, UntrustedIdentityException, InvalidNumberException { - final var timestamp = System.currentTimeMillis(); - var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.absent()); - sendHelper.sendTypingMessage(message, getRecipientIds(recipients)); - } - - public void sendGroupTypingMessage( - TypingAction action, GroupId groupId - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { - final var timestamp = System.currentTimeMillis(); - final var message = new SignalServiceTypingMessage(action.toSignalService(), - timestamp, - Optional.of(groupId.serialize())); - sendHelper.sendGroupTypingMessage(message, groupId); - } - - private SignalServiceDataMessage.Builder createMessageBuilder() { - final var timestamp = System.currentTimeMillis(); - - var messageBuilder = SignalServiceDataMessage.newBuilder(); - messageBuilder.withTimestamp(timestamp); - return messageBuilder; + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + sendTypingMessage(action.toSignalService(), recipients); } private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException { @@ -2005,8 +2018,8 @@ public class Manager implements Closeable { return false; } - public boolean isContactBlocked(final String identifier) throws InvalidNumberException { - final var recipientId = canonicalizeAndResolveRecipient(identifier); + public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { + final var recipientId = resolveRecipient(recipient); return isContactBlocked(recipientId); } @@ -2607,8 +2620,8 @@ public class Manager implements Closeable { return account.getContactStore().getContacts(); } - public String getContactOrProfileName(String number) throws InvalidNumberException { - final var recipientId = canonicalizeAndResolveRecipient(number); + public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { + final var recipientId = resolveRecipient(recipientIdentifier); final var recipient = account.getRecipientStore().getRecipient(recipientId); if (recipient == null) { return null; @@ -2643,19 +2656,19 @@ public class Manager implements Closeable { return account.getIdentityKeyStore().getIdentities(); } - public List getIdentities(String number) throws InvalidNumberException { - final var identity = account.getIdentityKeyStore().getIdentity(canonicalizeAndResolveRecipient(number)); + public List getIdentities(RecipientIdentifier.Single recipient) { + final var identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); return identity == null ? List.of() : List.of(identity); } /** * Trust this the identity with this fingerprint * - * @param name username of the identity + * @param recipient username of the identity * @param fingerprint Fingerprint */ - public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { + var recipientId = resolveRecipient(recipient); return trustIdentity(recipientId, identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), TrustLevel.TRUSTED_VERIFIED); @@ -2664,11 +2677,11 @@ public class Manager implements Closeable { /** * Trust this the identity with this safety number * - * @param name username of the identity + * @param recipient username of the identity * @param safetyNumber Safety number */ - public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { + var recipientId = resolveRecipient(recipient); var address = account.getRecipientStore().resolveServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), @@ -2678,11 +2691,11 @@ public class Manager implements Closeable { /** * Trust this the identity with this scannable safety number * - * @param name username of the identity + * @param recipient username of the identity * @param safetyNumber Scannable safety number */ - public boolean trustIdentityVerifiedSafetyNumber(String name, byte[] safetyNumber) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { + var recipientId = resolveRecipient(recipient); var address = account.getRecipientStore().resolveServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> { final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); @@ -2697,10 +2710,10 @@ public class Manager implements Closeable { /** * Trust all keys of this identity without verification * - * @param name username of the identity + * @param recipient username of the identity */ - public boolean trustIdentityAllKeys(String name) throws InvalidNumberException { - var recipientId = canonicalizeAndResolveRecipient(name); + public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { + var recipientId = resolveRecipient(recipient); return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); } @@ -2782,12 +2795,6 @@ public class Manager implements Closeable { return account.getRecipientStore().resolveServiceAddress(recipientId); } - private RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException { - var canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : canonicalizePhoneNumber(identifier); - - return resolveRecipient(canonicalizedNumber); - } - private String canonicalizePhoneNumber(final String number) throws InvalidNumberException { return PhoneNumberFormatter.formatNumber(number, account.getUsername()); } @@ -2798,6 +2805,17 @@ public class Manager implements Closeable { return resolveRecipient(address); } + private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) { + final SignalServiceAddress address; + if (recipient instanceof RecipientIdentifier.Uuid) { + address = new SignalServiceAddress(((RecipientIdentifier.Uuid) recipient).uuid, null); + } else { + address = new SignalServiceAddress(null, ((RecipientIdentifier.Number) recipient).number); + } + + return resolveRecipient(address); + } + public RecipientId resolveRecipient(SignalServiceAddress address) { return account.getRecipientStore().resolveRecipient(address); } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java new file mode 100644 index 00000000..cbcf1724 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -0,0 +1,112 @@ +package org.asamk.signal.manager.api; + +import org.asamk.signal.manager.groups.GroupId; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.UUID; + +public abstract class RecipientIdentifier { + + public static class NoteToSelf extends RecipientIdentifier { + + @Override + public boolean equals(final Object obj) { + return obj instanceof NoteToSelf; + } + + @Override + public int hashCode() { + return 5; + } + } + + public abstract static class Single extends RecipientIdentifier { + + public static Single fromString(String identifier, String localNumber) throws InvalidNumberException { + return UuidUtil.isUuid(identifier) + ? new Uuid(UUID.fromString(identifier)) + : new Number(PhoneNumberFormatter.formatNumber(identifier, localNumber)); + } + + public static Single fromAddress(SignalServiceAddress address) { + return address.getUuid().isPresent() + ? new Uuid(address.getUuid().get()) + : new Number(address.getNumber().get()); + } + } + + public static class Uuid extends Single { + + public final UUID uuid; + + public Uuid(final UUID uuid) { + this.uuid = uuid; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Uuid uuid1 = (Uuid) o; + + return uuid.equals(uuid1.uuid); + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } + } + + public static class Number extends Single { + + public final String number; + + public Number(final String number) { + this.number = number; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Number number1 = (Number) o; + + return number.equals(number1.number); + } + + @Override + public int hashCode() { + return number.hashCode(); + } + } + + public static class Group extends RecipientIdentifier { + + public final GroupId groupId; + + public Group(final GroupId groupId) { + this.groupId = groupId; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Group group = (Group) o; + + return groupId.equals(group.groupId); + } + + @Override + public int hashCode() { + return groupId.hashCode(); + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/SendGroupMessageResults.java b/lib/src/main/java/org/asamk/signal/manager/api/SendGroupMessageResults.java new file mode 100644 index 00000000..d5c9ef91 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/SendGroupMessageResults.java @@ -0,0 +1,26 @@ +package org.asamk.signal.manager.api; + +import org.whispersystems.signalservice.api.messages.SendMessageResult; + +import java.util.List; + +public class SendGroupMessageResults { + + private final long timestamp; + private final List results; + + public SendGroupMessageResults( + final long timestamp, final List results + ) { + this.timestamp = timestamp; + this.results = results; + } + + public long getTimestamp() { + return timestamp; + } + + public List getResults() { + return results; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/SendMessageResults.java b/lib/src/main/java/org/asamk/signal/manager/api/SendMessageResults.java new file mode 100644 index 00000000..ff323919 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/SendMessageResults.java @@ -0,0 +1,27 @@ +package org.asamk.signal.manager.api; + +import org.whispersystems.signalservice.api.messages.SendMessageResult; + +import java.util.List; +import java.util.Map; + +public class SendMessageResults { + + private final long timestamp; + private final Map> results; + + public SendMessageResults( + final long timestamp, final Map> results + ) { + this.timestamp = timestamp; + this.results = results; + } + + public long getTimestamp() { + return timestamp; + } + + public Map> getResults() { + return results; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index bc8800cf..b04ff40d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -11,7 +11,6 @@ import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.ContentHint; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; @@ -67,36 +66,34 @@ public class SendHelper { * Send a single message to one or multiple recipients. * The message is extended with the current expiration timer for each recipient. */ - public Pair> sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Set recipientIds + public SendMessageResult sendMessage( + final SignalServiceDataMessage.Builder messageBuilder, final RecipientId recipientId ) throws IOException { - // Send to all individually, so sync messages are sent correctly + final var contact = account.getContactStore().getContact(recipientId); + final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; + messageBuilder.withExpiration(expirationTime); messageBuilder.withProfileKey(account.getProfileKey().serialize()); - var results = new ArrayList(recipientIds.size()); - long timestamp = 0; - for (var recipientId : recipientIds) { - final var contact = account.getContactStore().getContact(recipientId); - final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; - messageBuilder.withExpiration(expirationTime); - final var singleMessage = messageBuilder.build(); - timestamp = singleMessage.getTimestamp(); - final var result = sendMessage(singleMessage, recipientId); - handlePossibleIdentityFailure(result); - - results.add(result); - } - return new Pair<>(timestamp, results); + final var message = messageBuilder.build(); + final var result = sendMessage(message, recipientId); + handlePossibleIdentityFailure(result); + return result; } /** * Send a group message to the given group * The message is extended with the current expiration timer for the group and the group context. */ - public Pair> sendAsGroupMessage( + public List sendAsGroupMessage( SignalServiceDataMessage.Builder messageBuilder, GroupId groupId ) throws IOException, GroupNotFoundException, NotAGroupMemberException { final var g = getGroupForSending(groupId); + return sendAsGroupMessage(messageBuilder, g); + } + + private List sendAsGroupMessage( + final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g + ) throws IOException { GroupUtils.setGroupContext(messageBuilder, g); messageBuilder.withExpiration(g.getMessageExpirationTime()); @@ -108,7 +105,7 @@ public class SendHelper { * Send a complete group message to the given recipients (should be current/old/new members) * This method should only be used for create/update/quit group messages. */ - public Pair> sendGroupMessage( + public List sendGroupMessage( final SignalServiceDataMessage message, final Set recipientIds ) throws IOException { List result = sendGroupMessageInternal(message, recipientIds); @@ -117,7 +114,7 @@ public class SendHelper { handlePossibleIdentityFailure(r); } - return new Pair<>(message.getTimestamp(), result); + return result; } public void sendReceiptMessage( @@ -146,7 +143,7 @@ public class SendHelper { } } - public Pair sendSelfMessage( + public SendMessageResult sendSelfMessage( SignalServiceDataMessage.Builder messageBuilder ) throws IOException { final var recipientId = account.getSelfRecipientId(); @@ -155,8 +152,7 @@ public class SendHelper { messageBuilder.withExpiration(expirationTime); var message = messageBuilder.build(); - final var result = sendSelfMessage(message); - return new Pair<>(message.getTimestamp(), result); + return sendSelfMessage(message); } public SendMessageResult sendSyncMessage(SignalServiceSyncMessage message) throws IOException { @@ -170,18 +166,16 @@ public class SendHelper { } public void sendTypingMessage( - SignalServiceTypingMessage message, Set recipientIds + SignalServiceTypingMessage message, RecipientId recipientId ) throws IOException, UntrustedIdentityException { var messageSender = dependencies.getMessageSender(); - for (var recipientId : recipientIds) { - final var address = addressResolver.resolveSignalServiceAddress(recipientId); - try { - messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); - } catch (UnregisteredUserException e) { - final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); - final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); - messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message); - } + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + try { + messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); + } catch (UnregisteredUserException e) { + final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); + final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); + messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message); } } diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index e7a21b88..cd101929 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -31,7 +31,7 @@ public interface Signal extends DBusInterface { long sendGroupRemoteDeleteMessage( long targetSentTimestamp, byte[] groupId - ) throws Error.Failure, Error.GroupNotFound; + ) throws Error.Failure, Error.GroupNotFound, Error.InvalidGroupId; long sendMessageReaction( String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, String recipient @@ -49,11 +49,11 @@ public interface Signal extends DBusInterface { long sendGroupMessage( String message, List attachments, byte[] groupId - ) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid; + ) throws Error.GroupNotFound, Error.Failure, Error.AttachmentInvalid, Error.InvalidGroupId; long sendGroupMessageReaction( String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, byte[] groupId - ) throws Error.GroupNotFound, Error.Failure, Error.InvalidNumber; + ) throws Error.GroupNotFound, Error.Failure, Error.InvalidNumber, Error.InvalidGroupId; String getContactName(String number) throws Error.InvalidNumber; @@ -61,17 +61,17 @@ public interface Signal extends DBusInterface { void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; - void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound; + void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId; List getGroupIds(); - String getGroupName(byte[] groupId); + String getGroupName(byte[] groupId) throws Error.InvalidGroupId; - List getGroupMembers(byte[] groupId); + List getGroupMembers(byte[] groupId) throws Error.InvalidGroupId; byte[] updateGroup( byte[] groupId, String name, List members, String avatar - ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound; + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId; boolean isRegistered(); @@ -85,15 +85,15 @@ public interface Signal extends DBusInterface { List getContactNumber(final String name) throws Error.Failure; - void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure; + void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.InvalidGroupId; boolean isContactBlocked(final String number) throws Error.InvalidNumber; - boolean isGroupBlocked(final byte[] groupId); + boolean isGroupBlocked(final byte[] groupId) throws Error.InvalidGroupId; - boolean isMember(final byte[] groupId); + boolean isMember(final byte[] groupId) throws Error.InvalidGroupId; - void joinGroup(final String groupLink) throws Error.Failure; + byte[] joinGroup(final String groupLink) throws Error.Failure; class MessageReceived extends DBusSignal { @@ -235,6 +235,13 @@ public interface Signal extends DBusInterface { } } + class InvalidGroupId extends DBusExecutionException { + + public InvalidGroupId(final String message) { + super(message); + } + } + class InvalidNumber extends DBusExecutionException { public InvalidNumber(final String message) { diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 323b6edf..0d8f312f 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -1,6 +1,7 @@ package org.asamk.signal; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.util.DateUtils; @@ -18,7 +19,6 @@ import org.whispersystems.signalservice.api.messages.calls.SignalServiceCallMess import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.ArrayList; import java.util.Base64; @@ -450,7 +450,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final PlainTextWriter writer, final SignalServiceDataMessage.Reaction reaction ) { writer.println("Emoji: {}", reaction.getEmoji()); - writer.println("Target author: {}", formatContact(m.resolveSignalServiceAddress(reaction.getTargetAuthor()))); + writer.println("Target author: {}", formatContact(reaction.getTargetAuthor())); writer.println("Target timestamp: {}", DateUtils.formatTimestamp(reaction.getTargetSentTimestamp())); writer.println("Is remove: {}", reaction.isRemove()); } @@ -459,7 +459,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final PlainTextWriter writer, final SignalServiceDataMessage.Quote quote ) { writer.println("Id: {}", quote.getId()); - writer.println("Author: {}", getLegacyIdentifier(m.resolveSignalServiceAddress(quote.getAuthor()))); + writer.println("Author: {}", formatContact(quote.getAuthor())); writer.println("Text: {}", quote.getText()); if (quote.getMentions() != null && quote.getMentions().size() > 0) { writer.println("Mentions:"); @@ -678,12 +678,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } private String formatContact(SignalServiceAddress address) { + address = m.resolveSignalServiceAddress(address); final var number = getLegacyIdentifier(address); - String name = null; - try { - name = m.getContactOrProfileName(number); - } catch (InvalidNumberException ignored) { - } + final var name = m.getContactOrProfileName(RecipientIdentifier.Single.fromAddress(address)); if (name == null || name.isEmpty()) { return number; } else { diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 0710a7e5..7326c398 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -8,12 +8,10 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupNotFoundException; -import org.asamk.signal.util.Util; +import org.asamk.signal.util.CommandUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.InvalidNumberException; public class BlockCommand implements JsonRpcLocalCommand { @@ -35,23 +33,22 @@ public class BlockCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - for (var contactNumber : ns.getList("contact")) { + final var contacts = ns.getList("contact"); + for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getUsername())) { try { - m.setContactBlocked(contactNumber, true); - } catch (InvalidNumberException e) { - logger.warn("Invalid number {}: {}", contactNumber, e.getMessage()); + m.setContactBlocked(contact, true); } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); } } - if (ns.getList("group-id") != null) { - for (var groupIdString : ns.getList("group-id")) { + final var groupIdStrings = ns.getList("group-id"); + if (groupIdStrings != null) { + for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) { try { - var groupId = Util.decodeGroupId(groupIdString); m.setGroupBlocked(groupId, true); - } catch (GroupIdFormatException | GroupNotFoundException e) { - logger.warn("Invalid group id {}: {}", groupIdString, e.getMessage()); + } catch (GroupNotFoundException e) { + logger.warn("Group not found {}: {}", groupId.toBase64(), e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index 543d9cc7..8d651592 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -70,7 +70,7 @@ public class JoinGroupCommand implements JsonRpcLocalCommand { writer.println("Joined group \"{}\"", newGroupId.toBase64()); } } - handleSendMessageResults(results.second()); + handleSendMessageResults(results.second().getResults()); } catch (GroupPatchNotAcceptedException e) { throw new UserErrorException("Failed to join group, maybe already a member"); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index 49ca2546..c859996e 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -7,15 +7,14 @@ import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; -import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.Hex; import org.asamk.signal.util.Util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.Base64; import java.util.List; @@ -58,11 +57,7 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { if (number == null) { identities = m.getIdentities(); } else { - try { - identities = m.getIdentities(number); - } catch (InvalidNumberException e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); - } + identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getUsername())); } if (outputWriter instanceof PlainTextWriter) { diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index c5c7472c..c39f298d 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -11,20 +11,15 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.util.Util; +import org.asamk.signal.util.CommandUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import static org.asamk.signal.util.ErrorUtils.handleSendMessageResults; @@ -53,22 +48,16 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final GroupId groupId; - try { - groupId = Util.decodeGroupId(ns.getString("group-id")); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } + final var groupId = CommandUtil.getGroupId(ns.getString("group-id")); - var groupAdmins = ns.getList("admin"); + var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), m.getUsername()); try { try { - final var results = m.sendQuitGroupMessage(groupId, - groupAdmins == null ? Set.of() : new HashSet<>(groupAdmins)); - final var timestamp = results.first(); + final var results = m.sendQuitGroupMessage(groupId, groupAdmins); + final var timestamp = results.getTimestamp(); outputResult(outputWriter, timestamp); - handleSendMessageResults(results.second()); + handleSendMessageResults(results.getResults()); } catch (NotAGroupMemberException e) { logger.info("User is not a group member"); } @@ -80,8 +69,6 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { throw new IOErrorException("Failed to send message: " + e.getMessage()); } catch (GroupNotFoundException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse admin number: " + e.getMessage()); } catch (LastGroupAdminException e) { throw new UserErrorException("You need to specify a new admin with --admin: " + e.getMessage()); } diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index 99100407..f033e0b1 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -1,5 +1,6 @@ package org.asamk.signal.commands; +import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; @@ -10,14 +11,15 @@ import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; -import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.groups.GroupIdFormatException; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.util.CommandUtil; +import org.asamk.signal.util.ErrorUtils; import org.freedesktop.dbus.errors.UnknownObject; import org.freedesktop.dbus.exceptions.DBusExecutionException; -import java.util.List; +import java.io.IOException; import java.util.Map; public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { @@ -34,40 +36,62 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { .required(true) .type(long.class) .help("Specify the timestamp of the message to delete."); - subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID."); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); + subparser.addArgument("--note-to-self").action(Arguments.storeTrue()); + } + + @Override + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var recipientStrings = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); + + final var recipientIdentifiers = CommandUtil.getRecipientIdentifiers(m, + isNoteToSelf, + recipientStrings, + groupIdStrings); + + final long targetTimestamp = ns.getLong("target-timestamp"); + + try { + final var results = m.sendRemoteDeleteMessage(targetTimestamp, recipientIdentifiers); + outputResult(outputWriter, results.getTimestamp()); + ErrorUtils.handleSendMessageResults(results.getResults()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new UserErrorException(e.getMessage()); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + } } @Override public void handleCommand( final Namespace ns, final Signal signal, final OutputWriter outputWriter ) throws CommandException { - final List recipients = ns.getList("recipient"); - final var groupIdString = ns.getString("group-id"); + final var recipients = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); final var noRecipients = recipients == null || recipients.isEmpty(); - if (noRecipients && groupIdString == null) { + final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); + if (noRecipients && noGroups) { throw new UserErrorException("No recipients given"); } - if (!noRecipients && groupIdString != null) { + if (!noRecipients && !noGroups) { throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); } final long targetTimestamp = ns.getLong("target-timestamp"); - byte[] groupId = null; - if (groupIdString != null) { - try { - groupId = Util.decodeGroupId(groupIdString).serialize(); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } - } - try { - long timestamp; - if (groupId != null) { - timestamp = signal.sendGroupRemoteDeleteMessage(targetTimestamp, groupId); + long timestamp = 0; + if (!noGroups) { + final var groupIds = CommandUtil.getGroupIds(groupIdStrings); + for (final var groupId : groupIds) { + timestamp = signal.sendGroupRemoteDeleteMessage(targetTimestamp, groupId.serialize()); + } } else { timestamp = signal.sendRemoteDeleteMessage(targetTimestamp, recipients); } @@ -83,13 +107,6 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Manager m, final OutputWriter outputWriter - ) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); - } - private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index dbb02248..a1e4c296 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -12,11 +12,15 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; -import org.asamk.signal.dbus.DbusSignalImpl; +import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.util.CommandUtil; +import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.IOUtils; -import org.asamk.signal.util.Util; import org.freedesktop.dbus.errors.UnknownObject; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; @@ -26,6 +30,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public class SendCommand implements DbusCommand, JsonRpcLocalCommand { @@ -40,9 +45,8 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { public void attachToSubparser(final Subparser subparser) { subparser.help("Send a message to another user or group."); subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); - final var mutuallyExclusiveGroup = subparser.addMutuallyExclusiveGroup(); - mutuallyExclusiveGroup.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID."); - mutuallyExclusiveGroup.addArgument("--note-to-self") + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); + subparser.addArgument("--note-to-self") .help("Send the message to self without notification.") .action(Arguments.storeTrue()); @@ -53,20 +57,77 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { .action(Arguments.storeTrue()); } + @Override + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var recipientStrings = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); + + final var recipientIdentifiers = CommandUtil.getRecipientIdentifiers(m, + isNoteToSelf, + recipientStrings, + groupIdStrings); + + final var isEndSession = ns.getBoolean("end-session"); + if (isEndSession) { + final var singleRecipients = recipientIdentifiers.stream() + .filter(r -> r instanceof RecipientIdentifier.Single) + .map(RecipientIdentifier.Single.class::cast) + .collect(Collectors.toSet()); + if (singleRecipients.isEmpty()) { + throw new UserErrorException("No recipients given"); + } + + try { + m.sendEndSessionMessage(singleRecipients); + return; + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + } + } + + var messageText = ns.getString("message"); + if (messageText == null) { + try { + messageText = IOUtils.readAll(System.in, Charset.defaultCharset()); + } catch (IOException e) { + throw new UserErrorException("Failed to read message from stdin: " + e.getMessage()); + } + } + + List attachments = ns.getList("attachment"); + if (attachments == null) { + attachments = List.of(); + } + + try { + var results = m.sendMessage(new Message(messageText, attachments), recipientIdentifiers); + outputResult(outputWriter, results.getTimestamp()); + ErrorUtils.handleSendMessageResults(results.getResults()); + } catch (AttachmentInvalidException | IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new UserErrorException(e.getMessage()); + } + } + @Override public void handleCommand( final Namespace ns, final Signal signal, final OutputWriter outputWriter ) throws CommandException { - final List recipients = ns.getList("recipient"); + final var recipients = ns.getList("recipient"); final var isEndSession = ns.getBoolean("end-session"); - final var groupIdString = ns.getString("group-id"); + final var groupIdStrings = ns.getList("group-id"); final var isNoteToSelf = ns.getBoolean("note-to-self"); final var noRecipients = recipients == null || recipients.isEmpty(); - if ((noRecipients && isEndSession) || (noRecipients && groupIdString == null && !isNoteToSelf)) { + final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); + if ((noRecipients && isEndSession) || (noRecipients && noGroups && !isNoteToSelf)) { throw new UserErrorException("No recipients given"); } - if (!noRecipients && groupIdString != null) { + if (!noRecipients && !noGroups) { throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); } if (!noRecipients && isNoteToSelf) { @@ -99,16 +160,14 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { attachments = List.of(); } - if (groupIdString != null) { - byte[] groupId; - try { - groupId = Util.decodeGroupId(groupIdString).serialize(); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } + if (!noGroups) { + final var groupIds = CommandUtil.getGroupIds(groupIdStrings); try { - var timestamp = signal.sendGroupMessage(messageText, attachments, groupId); + long timestamp = 0; + for (final var groupId : groupIds) { + timestamp = signal.sendGroupMessage(messageText, attachments, groupId.serialize()); + } outputResult(outputWriter, timestamp); return; } catch (DBusExecutionException e) { @@ -149,11 +208,4 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { writer.write(Map.of("timestamp", timestamp)); } } - - @Override - public void handleCommand( - final Namespace ns, final Manager m, final OutputWriter outputWriter - ) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); - } } diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index fedded42..c8e339a1 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -11,14 +11,15 @@ import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; -import org.asamk.signal.dbus.DbusSignalImpl; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.groups.GroupIdFormatException; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.util.CommandUtil; +import org.asamk.signal.util.ErrorUtils; import org.freedesktop.dbus.errors.UnknownObject; import org.freedesktop.dbus.exceptions.DBusExecutionException; -import java.util.List; +import java.io.IOException; import java.util.Map; public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { @@ -31,8 +32,11 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Send reaction to a previously received or sent message."); - subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID."); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); + subparser.addArgument("--note-to-self") + .help("Send the reaction to self without notification.") + .action(Arguments.storeTrue()); subparser.addArgument("-e", "--emoji") .required(true) .help("Specify the emoji, should be a single unicode grapheme cluster."); @@ -46,39 +50,71 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { subparser.addArgument("-r", "--remove").help("Remove a reaction.").action(Arguments.storeTrue()); } + @Override + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var recipientStrings = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); + + final var recipientIdentifiers = CommandUtil.getRecipientIdentifiers(m, + isNoteToSelf, + recipientStrings, + groupIdStrings); + + final var emoji = ns.getString("emoji"); + final var isRemove = ns.getBoolean("remove"); + final var targetAuthor = ns.getString("target-author"); + final var targetTimestamp = ns.getLong("target-timestamp"); + + try { + final var results = m.sendMessageReaction(emoji, + isRemove, + CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + targetTimestamp, + recipientIdentifiers); + outputResult(outputWriter, results.getTimestamp()); + ErrorUtils.handleSendMessageResults(results.getResults()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new UserErrorException(e.getMessage()); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + } + } + @Override public void handleCommand( final Namespace ns, final Signal signal, final OutputWriter outputWriter ) throws CommandException { - final List recipients = ns.getList("recipient"); - final var groupIdString = ns.getString("group-id"); + final var recipients = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); final var noRecipients = recipients == null || recipients.isEmpty(); - if (noRecipients && groupIdString == null) { + final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); + if (noRecipients && noGroups) { throw new UserErrorException("No recipients given"); } - if (!noRecipients && groupIdString != null) { + if (!noRecipients && !noGroups) { throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); } final var emoji = ns.getString("emoji"); - final boolean isRemove = ns.getBoolean("remove"); + final var isRemove = ns.getBoolean("remove"); final var targetAuthor = ns.getString("target-author"); - final long targetTimestamp = ns.getLong("target-timestamp"); - - byte[] groupId = null; - if (groupIdString != null) { - try { - groupId = Util.decodeGroupId(groupIdString).serialize(); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } - } + final var targetTimestamp = ns.getLong("target-timestamp"); try { - long timestamp; - if (groupId != null) { - timestamp = signal.sendGroupMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, groupId); + long timestamp = 0; + if (!noGroups) { + final var groupIds = CommandUtil.getGroupIds(groupIdStrings); + for (final var groupId : groupIds) { + timestamp = signal.sendGroupMessageReaction(emoji, + isRemove, + targetAuthor, + targetTimestamp, + groupId.serialize()); + } } else { timestamp = signal.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, recipients); } @@ -94,13 +130,6 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Manager m, final OutputWriter outputWriter - ) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); - } - private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java index 74f48112..afdbd4f8 100644 --- a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java @@ -7,8 +7,8 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.CommandUtil; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; @@ -34,7 +34,8 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var recipient = ns.getString("recipient"); + final var recipientString = ns.getString("recipient"); + final var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername()); final var targetTimestamps = ns.getList("target-timestamp"); final var type = ns.getString("type"); @@ -49,8 +50,6 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { } } catch (IOException | UntrustedIdentityException e) { throw new UserErrorException("Failed to send message: " + e.getMessage()); - } catch (InvalidNumberException e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index d5a68604..14139885 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -8,14 +8,12 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; -import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.util.Util; +import org.asamk.signal.util.CommandUtil; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.IOException; import java.util.HashSet; @@ -31,7 +29,7 @@ public class SendTypingCommand implements JsonRpcLocalCommand { public void attachToSubparser(final Subparser subparser) { subparser.help( "Send typing message to trigger a typing indicator for the recipient. Indicator will be shown for 15seconds unless a typing STOP message is sent first."); - subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID."); + subparser.addArgument("-g", "--group-id", "--group").help("Specify the recipient group ID.").nargs("*"); subparser.addArgument("recipient").help("Specify the recipients' phone number.").nargs("*"); subparser.addArgument("-s", "--stop").help("Send a typing STOP message.").action(Arguments.storeTrue()); } @@ -40,40 +38,29 @@ public class SendTypingCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var recipients = ns.getList("recipient"); - final var groupIdString = ns.getString("group-id"); - - final var noRecipients = recipients == null || recipients.isEmpty(); - if (noRecipients && groupIdString == null) { - throw new UserErrorException("No recipients given"); - } - if (!noRecipients && groupIdString != null) { - throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); - } - + final var recipientStrings = ns.getList("recipient"); + final var groupIdStrings = ns.getList("group-id"); final var action = ns.getBoolean("stop") ? TypingAction.STOP : TypingAction.START; - GroupId groupId = null; - if (groupIdString != null) { - try { - groupId = Util.decodeGroupId(groupIdString); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } + final var recipientIdentifiers = new HashSet(); + if (recipientStrings != null) { + final var localNumber = m.getUsername(); + recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber)); + } + if (groupIdStrings != null) { + recipientIdentifiers.addAll(CommandUtil.getGroupIdentifiers(groupIdStrings)); + } + + if (recipientIdentifiers.isEmpty()) { + throw new UserErrorException("No recipients given"); } try { - if (groupId != null) { - m.sendGroupTypingMessage(action, groupId); - } else { - m.sendTypingMessage(action, new HashSet<>(recipients)); - } + m.sendTypingMessage(action, recipientIdentifiers); } catch (IOException | UntrustedIdentityException e) { throw new UserErrorException("Failed to send message: " + e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); - } catch (InvalidNumberException e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 65487ba7..22dcc5d8 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -8,8 +8,8 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.Hex; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.Base64; import java.util.Locale; @@ -37,14 +37,10 @@ public class TrustCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - var number = ns.getString("number"); + var recipentString = ns.getString("number"); + var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getUsername()); if (ns.getBoolean("trust-all-known-keys")) { - boolean res; - try { - res = m.trustIdentityAllKeys(number); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); - } + boolean res = m.trustIdentityAllKeys(recipient); if (!res) { throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct."); } @@ -64,23 +60,13 @@ public class TrustCommand implements JsonRpcLocalCommand { throw new UserErrorException( "Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters."); } - boolean res; - try { - res = m.trustIdentityVerified(number, fingerprintBytes); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); - } + boolean res = m.trustIdentityVerified(recipient, fingerprintBytes); if (!res) { throw new UserErrorException( "Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct."); } } else if (safetyNumber.length() == 60) { - boolean res; - try { - res = m.trustIdentityVerifiedSafetyNumber(number, safetyNumber); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); - } + boolean res = m.trustIdentityVerifiedSafetyNumber(recipient, safetyNumber); if (!res) { throw new UserErrorException( "Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); @@ -93,12 +79,7 @@ public class TrustCommand implements JsonRpcLocalCommand { throw new UserErrorException( "Safety number has invalid format, either specify the old hex fingerprint or the new safety number"); } - boolean res; - try { - res = m.trustIdentityVerifiedSafetyNumber(number, scannableSafetyNumber); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse recipient: " + e.getMessage()); - } + boolean res = m.trustIdentityVerifiedSafetyNumber(recipient, scannableSafetyNumber); if (!res) { throw new UserErrorException( "Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct."); diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 830147bc..c5b9d1ca 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -8,12 +8,10 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupNotFoundException; -import org.asamk.signal.util.Util; +import org.asamk.signal.util.CommandUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.InvalidNumberException; public class UnblockCommand implements JsonRpcLocalCommand { @@ -35,26 +33,20 @@ public class UnblockCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - for (var contactNumber : ns.getList("contact")) { + for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("contact"), m.getUsername())) { try { m.setContactBlocked(contactNumber, false); - } catch (InvalidNumberException e) { - logger.warn("Invalid number: {}", contactNumber); } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); } } - if (ns.getList("group-id") != null) { - for (var groupIdString : ns.getList("group-id")) { - try { - var groupId = Util.decodeGroupId(groupIdString); - m.setGroupBlocked(groupId, false); - } catch (GroupIdFormatException e) { - logger.warn("Invalid group id: {}", groupIdString); - } catch (GroupNotFoundException e) { - logger.warn("Unknown group id: {}", groupIdString); - } + final var groupIdStrings = ns.getList("group-id"); + for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) { + try { + m.setGroupBlocked(groupId, false); + } catch (GroupNotFoundException e) { + logger.warn("Unknown group id: {}", groupId); } } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index 462ba8d2..2b7d5b4b 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -9,7 +9,7 @@ import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; -import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.asamk.signal.util.CommandUtil; import java.io.IOException; @@ -32,20 +32,19 @@ public class UpdateContactCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - var number = ns.getString("number"); + var recipientString = ns.getString("number"); + var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername()); try { var expiration = ns.getInt("expiration"); if (expiration != null) { - m.setExpirationTimer(number, expiration); + m.setExpirationTimer(recipient, expiration); } var name = ns.getString("name"); if (name != null) { - m.setContactName(number, name); + m.setContactName(recipient, name); } - } catch (InvalidNumberException e) { - throw new UserErrorException("Invalid contact number: " + e.getMessage()); } catch (IOException e) { throw new IOErrorException("Update contact error: " + e.getMessage()); } catch (NotMasterDeviceException e) { diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 00da38d4..fc2cfbc0 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -14,17 +14,15 @@ import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; -import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.File; import java.io.IOException; @@ -114,22 +112,17 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - GroupId groupId = null; final var groupIdString = ns.getString("group-id"); - if (groupIdString != null) { - try { - groupId = Util.decodeGroupId(groupIdString); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } - } + var groupId = CommandUtil.getGroupId(groupIdString); + + final var localNumber = m.getUsername(); var groupName = ns.getString("name"); var groupDescription = ns.getString("description"); - var groupMembers = ns.getList("member"); - var groupRemoveMembers = ns.getList("remove-member"); - var groupAdmins = ns.getList("admin"); - var groupRemoveAdmins = ns.getList("remove-admin"); + var groupMembers = CommandUtil.getSingleRecipientIdentifiers(ns.getList("member"), localNumber); + var groupRemoveMembers = CommandUtil.getSingleRecipientIdentifiers(ns.getList("remove-member"), localNumber); + var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), localNumber); + var groupRemoveAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("remove-admin"), localNumber); var groupAvatar = ns.getString("avatar"); var groupResetLink = ns.getBoolean("reset-link"); var groupLinkState = getGroupLinkState(ns.getString("link")); @@ -140,12 +133,14 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { try { boolean isNewGroup = false; + Long timestamp = null; if (groupId == null) { isNewGroup = true; var results = m.createGroup(groupName, groupMembers, groupAvatar == null ? null : new File(groupAvatar)); - ErrorUtils.handleSendMessageResults(results.second()); + timestamp = results.second().getTimestamp(); + ErrorUtils.handleSendMessageResults(results.second().getResults()); groupId = results.first(); groupName = null; groupMembers = null; @@ -168,20 +163,15 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { groupSendMessagesPermission == null ? null : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS); - Long timestamp = null; if (results != null) { - timestamp = results.first(); - ErrorUtils.handleSendMessageResults(results.second()); + timestamp = results.getTimestamp(); + ErrorUtils.handleSendMessageResults(results.getResults()); } outputResult(outputWriter, timestamp, isNewGroup ? groupId : null); } catch (AttachmentInvalidException e) { throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); - } catch (GroupNotFoundException e) { - logger.warn("Unknown group id: {}", groupIdString); - } catch (NotAGroupMemberException e) { - logger.warn("You're not a group member"); - } catch (InvalidNumberException e) { - throw new UserErrorException("Failed to parse member number: " + e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); } @@ -191,17 +181,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Signal signal, final OutputWriter outputWriter ) throws CommandException { - byte[] groupId = null; - if (ns.getString("group-id") != null) { - try { - groupId = Util.decodeGroupId(ns.getString("group-id")).serialize(); - } catch (GroupIdFormatException e) { - throw new UserErrorException("Invalid group id: " + e.getMessage()); - } - } - if (groupId == null) { - groupId = new byte[0]; - } + var groupId = CommandUtil.getGroupId(ns.getString("group-id")); var groupName = ns.getString("name"); if (groupName == null) { @@ -219,8 +199,11 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } try { - var newGroupId = signal.updateGroup(groupId, groupName, groupMembers, groupAvatar); - if (groupId.length != newGroupId.length) { + var newGroupId = signal.updateGroup(groupId == null ? new byte[0] : groupId.serialize(), + groupName, + groupMembers, + groupAvatar); + if (groupId == null) { outputResult(outputWriter, null, GroupId.unknownVersion(newGroupId)); } } catch (Signal.Error.AttachmentInvalid e) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index ab2bcda0..c5be7501 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -6,6 +6,7 @@ import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupNotFoundException; @@ -24,7 +25,10 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -59,57 +63,22 @@ public class DbusSignalImpl implements Signal { return sendMessage(message, attachments, recipients); } - private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException { - var error = ErrorUtils.getErrorMessageFromSendMessageResult(result); - - if (error == null) { - return; - } - - final var message = timestamp + "\nFailed to send message:\n" + error + '\n'; - - if (result.getIdentityFailure() != null) { - throw new Error.UntrustedIdentity(message); - } else { - throw new Error.Failure(message); - } - } - - private static void checkSendMessageResults( - long timestamp, List results - ) throws DBusExecutionException { - if (results.size() == 1) { - checkSendMessageResult(timestamp, results.get(0)); - return; - } - - var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results); - if (errors.size() == 0) { - return; - } - - var message = new StringBuilder(); - message.append(timestamp).append('\n'); - message.append("Failed to send (some) messages:\n"); - for (var error : errors) { - message.append(error).append('\n'); - } - - throw new Error.Failure(message.toString()); - } - @Override public long sendMessage(final String message, final List attachments, final List recipients) { try { - final var results = m.sendMessage(new Message(message, attachments), recipients); - checkSendMessageResults(results.first(), results.second()); - return results.first(); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); + final var results = m.sendMessage(new Message(message, attachments), + getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + .map(RecipientIdentifier.class::cast) + .collect(Collectors.toSet())); + + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); } } @@ -127,13 +96,16 @@ public class DbusSignalImpl implements Signal { final long targetSentTimestamp, final List recipients ) { try { - final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, recipients); - checkSendMessageResults(results.first(), results.second()); - return results.first(); + final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, + getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + .map(RecipientIdentifier.class::cast) + .collect(Collectors.toSet())); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); } } @@ -142,9 +114,10 @@ public class DbusSignalImpl implements Signal { final long targetSentTimestamp, final byte[] groupId ) { try { - final var results = m.sendGroupRemoteDeleteMessage(targetSentTimestamp, GroupId.unknownVersion(groupId)); - checkSendMessageResults(results.first(), results.second()); - return results.first(); + final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, + Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { @@ -174,13 +147,19 @@ public class DbusSignalImpl implements Signal { final List recipients ) { try { - final var results = m.sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients); - checkSendMessageResults(results.first(), results.second()); - return results.first(); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); + final var results = m.sendMessageReaction(emoji, + remove, + getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + targetSentTimestamp, + getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + .map(RecipientIdentifier.class::cast) + .collect(Collectors.toSet())); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); } } @@ -189,34 +168,36 @@ public class DbusSignalImpl implements Signal { final String message, final List attachments ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { try { - final var results = m.sendSelfMessage(new Message(message, attachments)); - checkSendMessageResult(results.first(), results.second()); - return results.first(); + final var results = m.sendMessage(new Message(message, attachments), + Set.of(new RecipientIdentifier.NoteToSelf())); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); } } @Override public void sendEndSessionMessage(final List recipients) { try { - final var results = m.sendEndSessionMessage(recipients); - checkSendMessageResults(results.first(), results.second()); + final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getUsername())); + checkSendMessageResults(results.getTimestamp(), results.getResults()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); } } @Override public long sendGroupMessage(final String message, final List attachments, final byte[] groupId) { try { - var results = m.sendGroupMessage(new Message(message, attachments), GroupId.unknownVersion(groupId)); - checkSendMessageResults(results.first(), results.second()); - return results.first(); + var results = m.sendMessage(new Message(message, attachments), + Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { @@ -235,17 +216,15 @@ public class DbusSignalImpl implements Signal { final byte[] groupId ) { try { - final var results = m.sendGroupMessageReaction(emoji, + final var results = m.sendMessageReaction(emoji, remove, - targetAuthor, + getSingleRecipientIdentifier(targetAuthor, m.getUsername()), targetSentTimestamp, - GroupId.unknownVersion(groupId)); - checkSendMessageResults(results.first(), results.second()); - return results.first(); + Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); + checkSendMessageResults(results.getTimestamp(), results.getResults()); + return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); } @@ -255,19 +234,13 @@ public class DbusSignalImpl implements Signal { // the profile name @Override public String getContactName(final String number) { - try { - return m.getContactOrProfileName(number); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); - } + return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername())); } @Override public void setContactName(final String number, final String name) { try { - m.setContactName(number, name); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); + m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } @@ -276,9 +249,7 @@ public class DbusSignalImpl implements Signal { @Override public void setContactBlocked(final String number, final boolean blocked) { try { - m.setContactBlocked(number, blocked); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); + m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } @@ -287,7 +258,7 @@ public class DbusSignalImpl implements Signal { @Override public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { - m.setGroupBlocked(GroupId.unknownVersion(groupId), blocked); + m.setGroupBlocked(getGroupId(groupId), blocked); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } @@ -305,7 +276,7 @@ public class DbusSignalImpl implements Signal { @Override public String getGroupName(final byte[] groupId) { - var group = m.getGroup(GroupId.unknownVersion(groupId)); + var group = m.getGroup(getGroupId(groupId)); if (group == null) { return ""; } else { @@ -315,7 +286,7 @@ public class DbusSignalImpl implements Signal { @Override public List getGroupMembers(final byte[] groupId) { - var group = m.getGroup(GroupId.unknownVersion(groupId)); + var group = m.getGroup(getGroupId(groupId)); if (group == null) { return List.of(); } else { @@ -336,21 +307,19 @@ public class DbusSignalImpl implements Signal { if (name.isEmpty()) { name = null; } - if (members.isEmpty()) { - members = null; - } if (avatar.isEmpty()) { avatar = null; } + final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername()); if (groupId == null) { - final var results = m.createGroup(name, members, avatar == null ? null : new File(avatar)); - checkSendMessageResults(0, results.second()); + final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar)); + checkSendMessageResults(results.second().getTimestamp(), results.second().getResults()); return results.first().serialize(); } else { - final var results = m.updateGroup(GroupId.unknownVersion(groupId), + final var results = m.updateGroup(getGroupId(groupId), name, null, - members, + memberIdentifiers, null, null, null, @@ -362,7 +331,7 @@ public class DbusSignalImpl implements Signal { null, null); if (results != null) { - checkSendMessageResults(results.first(), results.second()); + checkSendMessageResults(results.getTimestamp(), results.getResults()); } return groupId; } @@ -370,8 +339,6 @@ public class DbusSignalImpl implements Signal { throw new Error.Failure(e.getMessage()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); } @@ -450,26 +417,25 @@ public class DbusSignalImpl implements Signal { @Override public void quitGroup(final byte[] groupId) { - var group = GroupId.unknownVersion(groupId); + var group = getGroupId(groupId); try { m.sendQuitGroupMessage(group, Set.of()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException | LastGroupAdminException e) { throw new Error.Failure(e.getMessage()); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); } } @Override - public void joinGroup(final String groupLink) { + public byte[] joinGroup(final String groupLink) { try { final var linkUrl = GroupInviteLinkUrl.fromUri(groupLink); if (linkUrl == null) { throw new Error.Failure("Group link is invalid:"); } - m.joinGroup(linkUrl); + final var result = m.joinGroup(linkUrl); + return result.first().serialize(); } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupLinkNotActiveException e) { throw new Error.Failure("Group link is invalid: " + e.getMessage()); } catch (GroupInviteLinkUrl.UnknownGroupLinkVersionException e) { @@ -481,16 +447,12 @@ public class DbusSignalImpl implements Signal { @Override public boolean isContactBlocked(final String number) { - try { - return m.isContactBlocked(number); - } catch (InvalidNumberException e) { - throw new Error.InvalidNumber(e.getMessage()); - } + return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername())); } @Override public boolean isGroupBlocked(final byte[] groupId) { - var group = m.getGroup(GroupId.unknownVersion(groupId)); + var group = m.getGroup(getGroupId(groupId)); if (group == null) { return false; } else { @@ -500,11 +462,102 @@ public class DbusSignalImpl implements Signal { @Override public boolean isMember(final byte[] groupId) { - var group = m.getGroup(GroupId.unknownVersion(groupId)); + var group = m.getGroup(getGroupId(groupId)); if (group == null) { return false; } else { return group.isMember(m.getSelfRecipientId()); } } + + private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException { + var error = ErrorUtils.getErrorMessageFromSendMessageResult(result); + + if (error == null) { + return; + } + + final var message = timestamp + "\nFailed to send message:\n" + error + '\n'; + + if (result.getIdentityFailure() != null) { + throw new Error.UntrustedIdentity(message); + } else { + throw new Error.Failure(message); + } + } + + private static void checkSendMessageResults( + long timestamp, Map> results + ) throws DBusExecutionException { + final var sendMessageResults = results.values().stream().findFirst(); + if (results.size() == 1 && sendMessageResults.get().size() == 1) { + checkSendMessageResult(timestamp, sendMessageResults.get().stream().findFirst().get()); + return; + } + + var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results); + if (errors.size() == 0) { + return; + } + + var message = new StringBuilder(); + message.append(timestamp).append('\n'); + message.append("Failed to send (some) messages:\n"); + for (var error : errors) { + message.append(error).append('\n'); + } + + throw new Error.Failure(message.toString()); + } + + private static void checkSendMessageResults( + long timestamp, Collection results + ) throws DBusExecutionException { + if (results.size() == 1) { + checkSendMessageResult(timestamp, results.stream().findFirst().get()); + return; + } + + var errors = ErrorUtils.getErrorMessagesFromSendMessageResults(results); + if (errors.size() == 0) { + return; + } + + var message = new StringBuilder(); + message.append(timestamp).append('\n'); + message.append("Failed to send (some) messages:\n"); + for (var error : errors) { + message.append(error).append('\n'); + } + + throw new Error.Failure(message.toString()); + } + + private static Set getSingleRecipientIdentifiers( + final Collection recipientStrings, final String localNumber + ) throws DBusExecutionException { + final var identifiers = new HashSet(); + for (var recipientString : recipientStrings) { + identifiers.add(getSingleRecipientIdentifier(recipientString, localNumber)); + } + return identifiers; + } + + private static RecipientIdentifier.Single getSingleRecipientIdentifier( + final String recipientString, final String localNumber + ) throws DBusExecutionException { + try { + return RecipientIdentifier.Single.fromString(recipientString, localNumber); + } catch (InvalidNumberException e) { + throw new Error.InvalidNumber(e.getMessage()); + } + } + + private static GroupId getGroupId(byte[] groupId) throws DBusExecutionException { + try { + return GroupId.unknownVersion(groupId); + } catch (Throwable e) { + throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage()); + } + } } diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index e53b5ca5..814952aa 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.Signal; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.RecipientIdentifier; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; @@ -94,7 +95,7 @@ public class JsonMessageEnvelope { } String name; try { - name = m.getContactOrProfileName(this.source); + name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getUsername())); } catch (InvalidNumberException | NullPointerException e) { name = null; } diff --git a/src/main/java/org/asamk/signal/util/CommandUtil.java b/src/main/java/org/asamk/signal/util/CommandUtil.java new file mode 100644 index 00000000..83674876 --- /dev/null +++ b/src/main/java/org/asamk/signal/util/CommandUtil.java @@ -0,0 +1,99 @@ +package org.asamk.signal.util; + +import org.asamk.signal.commands.exceptions.UserErrorException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdFormatException; +import org.whispersystems.signalservice.api.util.InvalidNumberException; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class CommandUtil { + + private CommandUtil() { + } + + public static Set getRecipientIdentifiers( + final Manager m, + final boolean isNoteToSelf, + final List recipientStrings, + final List groupIdStrings + ) throws UserErrorException { + final var recipientIdentifiers = new HashSet(); + if (isNoteToSelf) { + recipientIdentifiers.add(new RecipientIdentifier.NoteToSelf()); + } + if (recipientStrings != null) { + final var localNumber = m.getUsername(); + recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber)); + } + if (groupIdStrings != null) { + recipientIdentifiers.addAll(CommandUtil.getGroupIdentifiers(groupIdStrings)); + } + + if (recipientIdentifiers.isEmpty()) { + throw new UserErrorException("No recipients given"); + } + return recipientIdentifiers; + } + + public static Set getGroupIdentifiers(Collection groupIdStrings) throws UserErrorException { + if (groupIdStrings == null) { + return Set.of(); + } + final var groupIds = new HashSet(); + for (final var groupIdString : groupIdStrings) { + groupIds.add(new RecipientIdentifier.Group(getGroupId(groupIdString))); + } + return groupIds; + } + + public static Set getGroupIds(Collection groupIdStrings) throws UserErrorException { + if (groupIdStrings == null) { + return Set.of(); + } + final var groupIds = new HashSet(); + for (final var groupIdString : groupIdStrings) { + groupIds.add(getGroupId(groupIdString)); + } + return groupIds; + } + + public static GroupId getGroupId(String groupId) throws UserErrorException { + if (groupId == null) { + return null; + } + try { + return GroupId.fromBase64(groupId); + } catch (GroupIdFormatException e) { + throw new UserErrorException("Invalid group id: " + e.getMessage()); + } + } + + public static Set getSingleRecipientIdentifiers( + final Collection recipientStrings, final String localNumber + ) throws UserErrorException { + if (recipientStrings == null) { + return Set.of(); + } + final var identifiers = new HashSet(); + for (var recipientString : recipientStrings) { + identifiers.add(getSingleRecipientIdentifier(recipientString, localNumber)); + } + return identifiers; + } + + public static RecipientIdentifier.Single getSingleRecipientIdentifier( + final String recipientString, final String localNumber + ) throws UserErrorException { + try { + return RecipientIdentifier.Single.fromString(recipientString, localNumber); + } catch (InvalidNumberException e) { + throw new UserErrorException("Invalid phone number '" + recipientString + "': " + e.getMessage()); + } + } +} diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 4fd88819..8a3de142 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -2,13 +2,16 @@ package org.asamk.signal.util; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; +import org.asamk.signal.manager.api.RecipientIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -21,13 +24,27 @@ public class ErrorUtils { } public static void handleSendMessageResults( - List results + Map> mapResults + ) throws CommandException { + List errors = getErrorMessagesFromSendMessageResults(mapResults); + handleSendMessageResultErrors(errors); + } + + public static void handleSendMessageResults( + Collection results ) throws CommandException { var errors = getErrorMessagesFromSendMessageResults(results); handleSendMessageResultErrors(errors); } - public static List getErrorMessagesFromSendMessageResults(List results) { + public static List getErrorMessagesFromSendMessageResults(final Map> mapResults) { + return mapResults.values() + .stream() + .flatMap(results -> getErrorMessagesFromSendMessageResults(results).stream()) + .collect(Collectors.toList()); + } + + public static List getErrorMessagesFromSendMessageResults(Collection results) { var errors = new ArrayList(); for (var result : results) { var error = getErrorMessageFromSendMessageResult(result); diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 0afe0910..01a79dd1 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -5,8 +5,6 @@ import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; -import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdFormatException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -61,10 +59,6 @@ public class Util { return f.toString(); } - public static GroupId decodeGroupId(String groupId) throws GroupIdFormatException { - return GroupId.fromBase64(groupId); - } - public static String getLegacyIdentifier(final SignalServiceAddress address) { return address.getNumber().or(() -> address.getUuid().get().toString()); } From ca52c0103136bc1f0cb140c33e81ff17d47aec2e Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Aug 2021 20:56:41 +0200 Subject: [PATCH 0766/2005] Adapt log level --- lib/src/main/java/org/asamk/signal/manager/Manager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 92dcf4d6..577badc8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -317,7 +317,7 @@ public class Manager implements Closeable { public void checkAccountState() throws IOException { if (account.getLastReceiveTimestamp() == 0) { - logger.warn("The Signal protocol expects that incoming messages are regularly received."); + logger.info("The Signal protocol expects that incoming messages are regularly received."); } else { var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp(); long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS); From 95792be9bcc1068c470630c13e0aebc55ed3bdc7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Aug 2021 21:21:12 +0200 Subject: [PATCH 0767/2005] Align cli param names for recipient --- .../java/org/asamk/signal/commands/BlockCommand.java | 4 ++-- .../asamk/signal/commands/GetUserStatusCommand.java | 10 +++++----- .../org/asamk/signal/commands/SendReceiptCommand.java | 6 ++++-- .../java/org/asamk/signal/commands/TrustCommand.java | 4 ++-- .../java/org/asamk/signal/commands/UnblockCommand.java | 4 ++-- .../asamk/signal/commands/UpdateContactCommand.java | 4 ++-- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 7326c398..105c2016 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -25,7 +25,7 @@ public class BlockCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Block the given contacts or groups (no messages will be received)"); - subparser.addArgument("contact").help("Contact number").nargs("*"); + subparser.addArgument("recipient").help("Contact number").nargs("*"); subparser.addArgument("-g", "--group-id", "--group").help("Group ID").nargs("*"); } @@ -33,7 +33,7 @@ public class BlockCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var contacts = ns.getList("contact"); + final var contacts = ns.getList("recipient"); for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getUsername())) { try { m.setContactBlocked(contact, true); diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index 055dac9f..cf4be085 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -31,7 +31,7 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Check if the specified phone number/s have been registered"); - subparser.addArgument("number").help("Phone number").nargs("+"); + subparser.addArgument("recipient").help("Phone number").nargs("+"); } @Override @@ -41,7 +41,7 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { // Get a map of registration statuses Map> registered; try { - registered = m.areUsersRegistered(new HashSet<>(ns.getList("number"))); + registered = m.areUsersRegistered(new HashSet<>(ns.getList("recipient"))); } catch (IOException e) { logger.debug("Failed to check registered users", e); throw new IOErrorException("Unable to check if users are registered"); @@ -69,7 +69,7 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { private static final class JsonUserStatus { - public final String name; + public final String recipient; public final String number; @@ -77,8 +77,8 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { public final boolean isRegistered; - public JsonUserStatus(String name, String number, String uuid, boolean isRegistered) { - this.name = name; + public JsonUserStatus(String recipient, String number, String uuid, boolean isRegistered) { + this.recipient = recipient; this.number = number; this.uuid = uuid; this.isRegistered = isRegistered; diff --git a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java index afdbd4f8..70e2f015 100644 --- a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java @@ -27,7 +27,9 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { .type(long.class) .nargs("+") .help("Specify the timestamp of the messages for which a receipt should be sent."); - subparser.addArgument("--type").help("Specify the receipt type.").choices("read", "viewed").setDefault("read"); + subparser.addArgument("--type") + .help("Specify the receipt type (default is read receipt).") + .choices("read", "viewed"); } @Override @@ -41,7 +43,7 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { final var type = ns.getString("type"); try { - if ("read".equals(type)) { + if (type == null || "read".equals(type)) { m.sendReadReceipt(recipient, targetTimestamps); } else if ("viewed".equals(type)) { m.sendViewedReceipt(recipient, targetTimestamps); diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 22dcc5d8..aedc2c3e 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -24,7 +24,7 @@ public class TrustCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Set the trust level of a given number."); - subparser.addArgument("number").help("Specify the phone number, for which to set the trust.").required(true); + subparser.addArgument("recipient").help("Specify the phone number, for which to set the trust.").required(true); var mutTrust = subparser.addMutuallyExclusiveGroup(); mutTrust.addArgument("-a", "--trust-all-known-keys") .help("Trust all known keys of this user, only use this for testing.") @@ -37,7 +37,7 @@ public class TrustCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - var recipentString = ns.getString("number"); + var recipentString = ns.getString("recipient"); var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getUsername()); if (ns.getBoolean("trust-all-known-keys")) { boolean res = m.trustIdentityAllKeys(recipient); diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index c5b9d1ca..e931a60e 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -25,7 +25,7 @@ public class UnblockCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Unblock the given contacts or groups (messages will be received again)"); - subparser.addArgument("contact").help("Contact number").nargs("*"); + subparser.addArgument("recipient").help("Contact number").nargs("*"); subparser.addArgument("-g", "--group-id", "--group").help("Group ID").nargs("*"); } @@ -33,7 +33,7 @@ public class UnblockCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("contact"), m.getUsername())) { + for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("recipient"), m.getUsername())) { try { m.setContactBlocked(contactNumber, false); } catch (NotMasterDeviceException e) { diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index 2b7d5b4b..8b9f9aa5 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -23,7 +23,7 @@ public class UpdateContactCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Update the details of a given contact"); - subparser.addArgument("number").help("Contact number"); + subparser.addArgument("recipient").help("Contact number"); subparser.addArgument("-n", "--name").help("New contact name"); subparser.addArgument("-e", "--expiration").type(int.class).help("Set expiration time of messages (seconds)"); } @@ -32,7 +32,7 @@ public class UpdateContactCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - var recipientString = ns.getString("number"); + var recipientString = ns.getString("recipient"); var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername()); try { From 7106a997cf2f2d8325ae4e11dfb8424aa60999eb Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Aug 2021 21:27:20 +0200 Subject: [PATCH 0768/2005] Update tests --- graalvm-config-dir/reflect-config.json | 44 ++++++++++++++++++++++---- run_tests.sh | 44 +++++++++++++++++++++++--- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index bba5ef48..aef740aa 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -337,12 +337,36 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"org.asamk.signal.commands.ListContactsCommand$JsonContact", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.asamk.signal.commands.ListDevicesCommand$JsonDevice", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.asamk.signal.commands.ListGroupsCommand$JsonGroup", "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"org.asamk.signal.commands.ListGroupsCommand$JsonGroupMember", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.asamk.signal.commands.ListIdentitiesCommand$JsonIdentity", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.asamk.signal.json.JsonAttachment", "allDeclaredFields":true, @@ -487,32 +511,38 @@ "allDeclaredConstructors":true }, { - "name": "org.asamk.signal.jsonrpc.JsonRpcBulkMessage", + "name":"org.asamk.signal.jsonrpc.JsonRpcBulkMessage", "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, { - "name": "org.asamk.signal.jsonrpc.JsonRpcException", + "name":"org.asamk.signal.jsonrpc.JsonRpcException", "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, { - "name": "org.asamk.signal.jsonrpc.JsonRpcMessage", - "allDeclaredFields": true, + "name":"org.asamk.signal.jsonrpc.JsonRpcMessage", + "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, { - "name": "org.asamk.signal.jsonrpc.JsonRpcRequest", - "allDeclaredFields": true, + "name":"org.asamk.signal.jsonrpc.JsonRpcRequest", + "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, { "name":"org.asamk.signal.jsonrpc.JsonRpcResponse", - "allDeclaredFields": true, + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.asamk.signal.jsonrpc.JsonRpcResponse$Error", + "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true }, diff --git a/run_tests.sh b/run_tests.sh index 1405ada3..4ee46845 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -23,21 +23,21 @@ PATH_LINK="$PATH_TEST_CONFIG/link" ./gradlew installDist -function run() { +run() { set -x "$SIGNAL_CLI" --service-environment="sandbox" $@ set +x } -function run_main() { +run_main() { run --config="$PATH_MAIN" $@ } -function run_linked() { +run_linked() { run --config="$PATH_LINK" $@ } -function register() { +register() { NUMBER=$1 PIN=$2 echo -n "Enter a captcha token (https://signalcaptchas.org/registration/generate.html): " @@ -52,7 +52,7 @@ function register() { fi } -function link() { +link() { NUMBER=$1 LINK_CODE_FILE="$PATH_TEST_CONFIG/link_code" rm -f "$LINK_CODE_FILE" @@ -76,6 +76,40 @@ register "$NUMBER_2" sleep 5 +# JSON-RPC +FIFO_FILE="${PATH_MAIN}/dbus-fifo" + +rm -f "$FIFO_FILE" +mkfifo "$FIFO_FILE" + +run_main -u "$NUMBER_1" send "$NUMBER_2" -m hi +run_main -u "$NUMBER_2" jsonRpc < "$FIFO_FILE" & + +exec 3<> "$FIFO_FILE" + echo '{"jsonrpc":"2.0","id":"id","method":"updateContact","params":{"recipient":"'"$NUMBER_1"'","name":"NUMBER_1","expiration":10}}' >&3 + echo '{"jsonrpc":"2.0","id":5,"method":"block","params":{"recipient":"'"$NUMBER_1"'"}}' >&3 + echo '{"jsonrpc":"2.0","id":null,"method":"unblock","params":{"recipient":"'"$NUMBER_1"'"}}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"listContacts"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"listGroups"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"listDevices"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"listIdentities"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"sendSyncRequest"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"sendContacts"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"version"}' >&3 + echo '{"jsonrpc":"2.0","id":"id","method":"updateAccount"}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"sendReceipt","params":{"recipient":"'"$NUMBER_1"'","targetTimestamp":1629919505575}}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"sendTyping","params":{"recipient":"'"$NUMBER_1"'"}}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"send","params":{"recipient":"'"$NUMBER_1"'","message":"some text"}}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"send","params":{"recipients":["'"$NUMBER_1"'","'"$NUMBER_2"'"],"message":"some other text"}}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"updateProfile","params":{"givenName":"n1","familyName":"n2","about":"ABA","aboutEmoji":"EMO","avatar":"LICENSE"}}' >&3 + echo '{"jsonrpc":"2.0","id":7,"method":"getUserStatus","params":{"recipient":"'"$NUMBER_1"'"}}' >&3 + + # Error expected: + echo '{"jsonrpc":"2.0","id":7,"method":"sendReceipt","params":{"recipient":5}}' >&3 +exec 3>&- + +wait + run_main -u "$NUMBER_1" setPin "$TEST_PIN_1" run_main -u "$NUMBER_2" removePin From 6ee0a95aa2a9a1c56e9f6b975ffc879d1892759c Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 25 Aug 2021 23:05:46 +0200 Subject: [PATCH 0769/2005] Update URL for reaching Signal chat server --- .../main/java/org/asamk/signal/manager/config/LiveConfig.java | 2 +- .../java/org/asamk/signal/manager/config/SandboxConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index 4298547d..7762a4cb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -32,7 +32,7 @@ class LiveConfig { "fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe"); private final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87"; - private final static String URL = "https://textsecure-service.whispersystems.org"; + private final static String URL = "https://chat.signal.org"; private final static String CDN_URL = "https://cdn.signal.org"; private final static String CDN2_URL = "https://cdn2.signal.org"; private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api.directory.signal.org"; diff --git a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java index 9ca9dc8b..12d87cf5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java @@ -32,7 +32,7 @@ class SandboxConfig { "51a56084c0b21c6b8f62b1bc792ec9bedac4c7c3964bb08ddcab868158c09982"); private final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87"; - private final static String URL = "https://textsecure-service-staging.whispersystems.org"; + private final static String URL = "https://chat.staging.signal.org"; private final static String CDN_URL = "https://cdn-staging.signal.org"; private final static String CDN2_URL = "https://cdn2-staging.signal.org"; private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api-staging.directory.signal.org"; From 944c3327ee4ea552d968fca1db89fa3318cec2d9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 08:47:02 +0200 Subject: [PATCH 0770/2005] Extract GroupHelper --- .../org/asamk/signal/manager/Manager.java | 645 +----------------- .../signal/manager/helper/GroupHelper.java | 628 +++++++++++++++++ .../signal/manager/helper/GroupV2Helper.java | 40 +- 3 files changed, 692 insertions(+), 621 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 577badc8..3857d803 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -34,6 +34,7 @@ import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; @@ -45,7 +46,6 @@ import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; -import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.messageCache.CachedMessage; @@ -60,23 +60,9 @@ import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; -import org.signal.libsignal.metadata.InvalidMetadataMessageException; -import org.signal.libsignal.metadata.InvalidMetadataVersionException; -import org.signal.libsignal.metadata.ProtocolDuplicateMessageException; -import org.signal.libsignal.metadata.ProtocolInvalidKeyException; -import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException; import org.signal.libsignal.metadata.ProtocolInvalidMessageException; -import org.signal.libsignal.metadata.ProtocolInvalidVersionException; -import org.signal.libsignal.metadata.ProtocolLegacyMessageException; -import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; -import org.signal.libsignal.metadata.SelfSendException; -import org.signal.storageservice.protos.groups.GroupChange; -import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.zkgroup.InvalidInputException; -import org.signal.zkgroup.VerificationFailedException; -import org.signal.zkgroup.groups.GroupMasterKey; -import org.signal.zkgroup.groups.GroupSecretParams; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.slf4j.Logger; @@ -93,11 +79,9 @@ import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.InvalidMessageStructureException; import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -107,7 +91,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; -import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; @@ -125,18 +108,15 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.ConflictException; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; -import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; -import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; @@ -187,9 +167,9 @@ public class Manager implements Closeable { private final ExecutorService executor = Executors.newCachedThreadPool(); private final ProfileHelper profileHelper; - private final GroupV2Helper groupV2Helper; private final PinHelper pinHelper; private final SendHelper sendHelper; + private final GroupHelper groupHelper; private final AvatarStore avatarStore; private final AttachmentStore attachmentStore; @@ -235,12 +215,11 @@ public class Manager implements Closeable { dependencies::getProfileService, dependencies::getMessageReceiver, this::resolveSignalServiceAddress); - this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, + final GroupV2Helper groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, account::getSelfRecipientId, dependencies.getGroupsV2Operations(), dependencies.getGroupsV2Api(), - this::getGroupAuthForToday, this::resolveSignalServiceAddress); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); @@ -253,6 +232,13 @@ public class Manager implements Closeable { this::handleIdentityFailure, this::getGroup, this::refreshRegisteredUser); + this.groupHelper = new GroupHelper(account, + dependencies, + sendHelper, + groupV2Helper, + avatarStore, + this::resolveSignalServiceAddress, + this::resolveRecipient); } public String getUsername() { @@ -665,15 +651,6 @@ public class Manager implements Closeable { return ProfileUtils.decryptProfile(profileKey, encryptedProfile); } - private Optional createGroupAvatarAttachment(GroupId groupId) throws IOException { - final var streamDetails = avatarStore.retrieveGroupAvatar(groupId); - if (streamDetails == null) { - return Optional.absent(); - } - - return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); - } - private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { final var streamDetails = avatarStore.retrieveContactAvatar(address); if (streamDetails == null) { @@ -683,17 +660,6 @@ public class Manager implements Closeable { return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); } - private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { - var g = getGroup(groupId); - if (g == null) { - throw new GroupNotFoundException(groupId); - } - if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) { - throw new NotAGroupMemberException(groupId, g.getTitle()); - } - return g; - } - public List getGroups() { return account.getGroupStore().getGroups(); } @@ -701,53 +667,8 @@ public class Manager implements Closeable { public SendGroupMessageResults sendQuitGroupMessage( GroupId groupId, Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - var group = getGroupForUpdating(groupId); - if (group instanceof GroupInfoV1) { - return quitGroupV1((GroupInfoV1) group); - } - final var newAdmins = getRecipientIds(groupAdmins); - try { - return quitGroupV2((GroupInfoV2) group, newAdmins); - } catch (ConflictException e) { - // Detected conflicting update, refreshing group and trying again - group = getGroup(groupId, true); - return quitGroupV2((GroupInfoV2) group, newAdmins); - } - } - - private SendGroupMessageResults quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) - .withId(groupInfoV1.getGroupId().serialize()) - .build(); - - var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); - groupInfoV1.removeMember(account.getSelfRecipientId()); - account.getGroupStore().updateGroup(groupInfoV1); - return sendGroupMessage(messageBuilder, - groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - } - - private SendGroupMessageResults quitGroupV2( - final GroupInfoV2 groupInfoV2, final Set newAdmins - ) throws LastGroupAdminException, IOException { - final var currentAdmins = groupInfoV2.getAdminMembers(); - newAdmins.removeAll(currentAdmins); - newAdmins.retainAll(groupInfoV2.getMembers()); - if (currentAdmins.contains(getSelfRecipientId()) - && currentAdmins.size() == 1 - && groupInfoV2.getMembers().size() > 1 - && newAdmins.size() == 0) { - // Last admin can't leave the group, unless she's also the last member - throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle()); - } - final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); - groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); - account.getGroupStore().updateGroup(groupInfoV2); - - var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); - return sendGroupMessage(messageBuilder, - groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + return groupHelper.quitGroup(groupId, newAdmins); } public void deleteGroup(GroupId groupId) throws IOException { @@ -758,45 +679,7 @@ public class Manager implements Closeable { public Pair createGroup( String name, Set members, File avatarFile ) throws IOException, AttachmentInvalidException { - return createGroupInternal(name, members == null ? null : getRecipientIds(members), avatarFile); - } - - private Pair createGroupInternal( - String name, Set members, File avatarFile - ) throws IOException, AttachmentInvalidException { - final var selfRecipientId = account.getSelfRecipientId(); - if (members != null && members.contains(selfRecipientId)) { - members = new HashSet<>(members); - members.remove(selfRecipientId); - } - - var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name, - members == null ? Set.of() : members, - avatarFile); - - if (gv2Pair == null) { - // Failed to create v2 group, creating v1 group instead - var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); - gv1.addMembers(List.of(selfRecipientId)); - final var result = updateGroupV1(gv1, name, members, avatarFile); - return new Pair<>(gv1.getGroupId(), result); - } - - final var gv2 = gv2Pair.first(); - final var decryptedGroup = gv2Pair.second(); - - gv2.setGroup(decryptedGroup, this::resolveRecipient); - if (avatarFile != null) { - avatarStore.storeGroupAvatar(gv2.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - - account.getGroupStore().updateGroup(gv2); - - final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null); - - final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); - return new Pair<>(gv2.getGroupId(), result); + return groupHelper.createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); } public SendGroupMessageResults updateGroup( @@ -815,7 +698,7 @@ public class Manager implements Closeable { Integer expirationTimer, Boolean isAnnouncementGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - return updateGroupInternal(groupId, + return groupHelper.updateGroup(groupId, name, description, members == null ? null : getRecipientIds(members), @@ -831,267 +714,10 @@ public class Manager implements Closeable { isAnnouncementGroup); } - private SendGroupMessageResults updateGroupInternal( - final GroupId groupId, - final String name, - final String description, - final Set members, - final Set removeMembers, - final Set admins, - final Set removeAdmins, - final boolean resetGroupLink, - final GroupLinkState groupLinkState, - final GroupPermission addMemberPermission, - final GroupPermission editDetailsPermission, - final File avatarFile, - final Integer expirationTimer, - final Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - var group = getGroupForUpdating(groupId); - - if (group instanceof GroupInfoV2) { - try { - return updateGroupV2((GroupInfoV2) group, - name, - description, - members, - removeMembers, - admins, - removeAdmins, - resetGroupLink, - groupLinkState, - addMemberPermission, - editDetailsPermission, - avatarFile, - expirationTimer, - isAnnouncementGroup); - } catch (ConflictException e) { - // Detected conflicting update, refreshing group and trying again - group = getGroup(groupId, true); - return updateGroupV2((GroupInfoV2) group, - name, - description, - members, - removeMembers, - admins, - removeAdmins, - resetGroupLink, - groupLinkState, - addMemberPermission, - editDetailsPermission, - avatarFile, - expirationTimer, - isAnnouncementGroup); - } - } - - final var gv1 = (GroupInfoV1) group; - final var result = updateGroupV1(gv1, name, members, avatarFile); - if (expirationTimer != null) { - setExpirationTimer(gv1, expirationTimer); - } - return result; - } - - private SendGroupMessageResults updateGroupV1( - final GroupInfoV1 gv1, final String name, final Set members, final File avatarFile - ) throws IOException, AttachmentInvalidException { - updateGroupV1Details(gv1, name, members, avatarFile); - - account.getGroupStore().updateGroup(gv1); - - var messageBuilder = getGroupUpdateMessageBuilder(gv1); - return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); - } - - private void updateGroupV1Details( - final GroupInfoV1 g, final String name, final Collection members, final File avatarFile - ) throws IOException { - if (name != null) { - g.name = name; - } - - if (members != null) { - final var newMemberAddresses = members.stream() - .filter(member -> !g.isMember(member)) - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList()); - final var newE164Members = new HashSet(); - for (var member : newMemberAddresses) { - if (!member.getNumber().isPresent()) { - continue; - } - newE164Members.add(member.getNumber().get()); - } - - final var registeredUsers = getRegisteredUsers(newE164Members); - if (registeredUsers.size() != newE164Members.size()) { - // Some of the new members are not registered on Signal - newE164Members.removeAll(registeredUsers.keySet()); - throw new IOException("Failed to add members " - + String.join(", ", newE164Members) - + " to group: Not registered on Signal"); - } - - g.addMembers(members); - } - - if (avatarFile != null) { - avatarStore.storeGroupAvatar(g.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - } - - private SendGroupMessageResults updateGroupV2( - final GroupInfoV2 group, - final String name, - final String description, - final Set members, - final Set removeMembers, - final Set admins, - final Set removeAdmins, - final boolean resetGroupLink, - final GroupLinkState groupLinkState, - final GroupPermission addMemberPermission, - final GroupPermission editDetailsPermission, - final File avatarFile, - final Integer expirationTimer, - final Boolean isAnnouncementGroup - ) throws IOException { - SendGroupMessageResults result = null; - if (group.isPendingMember(account.getSelfRecipientId())) { - var groupGroupChangePair = groupV2Helper.acceptInvite(group); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (members != null) { - final var newMembers = new HashSet<>(members); - newMembers.removeAll(group.getMembers()); - if (newMembers.size() > 0) { - var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - } - - if (removeMembers != null) { - var existingRemoveMembers = new HashSet<>(removeMembers); - existingRemoveMembers.retainAll(group.getMembers()); - existingRemoveMembers.remove(getSelfRecipientId());// self can be removed with sendQuitGroupMessage - if (existingRemoveMembers.size() > 0) { - var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - var pendingRemoveMembers = new HashSet<>(removeMembers); - pendingRemoveMembers.retainAll(group.getPendingMembers()); - if (pendingRemoveMembers.size() > 0) { - var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - } - - if (admins != null) { - final var newAdmins = new HashSet<>(admins); - newAdmins.retainAll(group.getMembers()); - newAdmins.removeAll(group.getAdminMembers()); - if (newAdmins.size() > 0) { - for (var admin : newAdmins) { - var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true); - result = sendUpdateGroupV2Message(group, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - } - } - - if (removeAdmins != null) { - final var existingRemoveAdmins = new HashSet<>(removeAdmins); - existingRemoveAdmins.retainAll(group.getAdminMembers()); - if (existingRemoveAdmins.size() > 0) { - for (var admin : existingRemoveAdmins) { - var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false); - result = sendUpdateGroupV2Message(group, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - } - } - - if (resetGroupLink) { - var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (groupLinkState != null) { - var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (addMemberPermission != null) { - var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (editDetailsPermission != null) { - var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (expirationTimer != null) { - var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (isAnnouncementGroup != null) { - var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup); - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - if (name != null || description != null || avatarFile != null) { - var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); - if (avatarFile != null) { - avatarStore.storeGroupAvatar(group.getGroupId(), - outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); - } - result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); - } - - return result; - } - public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { - final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), - inviteLinkUrl.getPassword()); - final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(), - inviteLinkUrl.getPassword(), - groupJoinInfo); - final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), - groupJoinInfo.getRevision() + 1, - groupChange.toByteArray()); - - if (group.getGroup() == null) { - // Only requested member, can't send update to group members - return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of())); - } - - final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); - - return new Pair<>(group.getGroupId(), result); - } - - private SendGroupMessageResults sendUpdateGroupV2Message( - GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange - ) throws IOException { - final var selfRecipientId = account.getSelfRecipientId(); - final var members = group.getMembersIncludingPendingWithout(selfRecipientId); - group.setGroup(newDecryptedGroup, this::resolveRecipient); - members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId)); - account.getGroupStore().updateGroup(group); - - final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); - return sendGroupMessage(messageBuilder, members); + return groupHelper.joinGroup(inviteLinkUrl); } public SendMessageResults sendMessage( @@ -1134,100 +760,18 @@ public class Manager implements Closeable { } } - private SendGroupMessageResults sendGroupMessage( - final SignalServiceDataMessage.Builder messageBuilder, final Set members - ) throws IOException { - final var timestamp = System.currentTimeMillis(); - messageBuilder.withTimestamp(timestamp); - final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members); - return new SendGroupMessageResults(timestamp, results); - } - - private static int currentTimeDays() { - return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()); - } - - private GroupsV2AuthorizationString getGroupAuthForToday( - final GroupSecretParams groupSecretParams - ) throws IOException { - final var today = currentTimeDays(); - // Returns credentials for the next 7 days - final var credentials = dependencies.getGroupsV2Api().getCredentials(today); - // TODO cache credentials until they expire - var authCredentialResponse = credentials.get(today); - try { - return dependencies.getGroupsV2Api() - .getGroupsV2AuthorizationString(account.getUuid(), - today, - groupSecretParams, - authCredentialResponse); - } catch (VerificationFailedException e) { - throw new IOException(e); - } - } - SendGroupMessageResults sendGroupInfoMessage( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { - GroupInfoV1 g; - var group = getGroupForUpdating(groupId); - if (!(group instanceof GroupInfoV1)) { - throw new IOException("Received an invalid group request for a v2 group!"); - } - g = (GroupInfoV1) group; - final var recipientId = resolveRecipient(recipient); - if (!g.isMember(recipientId)) { - throw new NotAGroupMemberException(groupId, g.name); - } - - var messageBuilder = getGroupUpdateMessageBuilder(g); - - // Send group message only to the recipient who requested it - return sendGroupMessage(messageBuilder, Set.of(recipientId)); - } - - private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) - .withId(g.getGroupId().serialize()) - .withName(g.name) - .withMembers(g.getMembers() - .stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList())); - - try { - final var attachment = createGroupAvatarAttachment(g.getGroupId()); - if (attachment.isPresent()) { - group.withAvatar(attachment.get()); - } - } catch (IOException e) { - throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); - } - - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); - } - - private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { - var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) - .withRevision(g.getGroup().getRevision()) - .withSignedGroupChange(signedGroupChange); - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + return groupHelper.sendGroupInfoMessage(groupId, recipientId); } SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { - var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); - - var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); - - // Send group info request message to the recipient who sent us a message with this groupId - return sendGroupMessage(messageBuilder, Set.of(resolveRecipient(recipient))); + final var recipientId = resolveRecipient(recipient); + return groupHelper.sendGroupInfoRequest(groupId, recipientId); } public void sendReadReceipt( @@ -1361,6 +905,7 @@ public class Manager implements Closeable { private void setContactBlocked(RecipientId recipientId, boolean blocked) { var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + // TODO cycle our profile key account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); } @@ -1371,19 +916,10 @@ public class Manager implements Closeable { } group.setBlocked(blocked); + // TODO cycle our profile key account.getGroupStore().updateGroup(group); } - private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { - var contact = account.getContactStore().getContact(recipientId); - if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { - return; - } - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - account.getContactStore() - .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); - } - /** * Change the expiration timer for a contact */ @@ -1400,20 +936,14 @@ public class Manager implements Closeable { } } - /** - * Change the expiration timer for a group - */ - private void setExpirationTimer( - GroupInfoV1 groupInfoV1, int messageExpirationTimer - ) throws NotAGroupMemberException, GroupNotFoundException, IOException { - groupInfoV1.messageExpirationTime = messageExpirationTimer; - account.getGroupStore().updateGroup(groupInfoV1); - sendExpirationTimerUpdate(groupInfoV1.getGroupId()); - } - - private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { - final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - sendHelper.sendAsGroupMessage(messageBuilder, groupId); + private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { + var contact = account.getContactStore().getContact(recipientId); + if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { + return; + } + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore() + .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); } /** @@ -1583,10 +1113,6 @@ public class Manager implements Closeable { sendTypingMessage(action.toSignalService(), recipients); } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException { - return dependencies.getCipher().decrypt(envelope); - } - private void handleEndSession(RecipientId recipientId) { account.getSessionStore().deleteAllSessions(recipientId); } @@ -1614,7 +1140,7 @@ public class Manager implements Closeable { if (groupInfo.getAvatar().isPresent()) { var avatar = groupInfo.getAvatar().get(); - downloadGroupAvatar(avatar, groupV1.getGroupId()); + downloadGroupAvatar(groupV1.getGroupId(), avatar); } if (groupInfo.getName().isPresent()) { @@ -1658,7 +1184,7 @@ public class Manager implements Closeable { final var groupContext = message.getGroupContext().get().getGroupV2().get(); final var groupMasterKey = groupContext.getMasterKey(); - getOrMigrateGroup(groupMasterKey, + groupHelper.getOrMigrateGroup(groupMasterKey, groupContext.getRevision(), groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); } @@ -1743,65 +1269,6 @@ public class Manager implements Closeable { return actions; } - private GroupInfoV2 getOrMigrateGroup( - final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange - ) { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - - var groupId = GroupUtils.getGroupIdV2(groupSecretParams); - var groupInfo = getGroup(groupId); - final GroupInfoV2 groupInfoV2; - if (groupInfo instanceof GroupInfoV1) { - // Received a v2 group message for a v1 group, we need to locally migrate the group - account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId()); - groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); - logger.info("Locally migrated group {} to group v2, id: {}", - groupInfo.getGroupId().toBase64(), - groupInfoV2.getGroupId().toBase64()); - } else if (groupInfo instanceof GroupInfoV2) { - groupInfoV2 = (GroupInfoV2) groupInfo; - } else { - groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); - } - - if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) { - DecryptedGroup group = null; - if (signedGroupChange != null - && groupInfoV2.getGroup() != null - && groupInfoV2.getGroup().getRevision() + 1 == revision) { - group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), - signedGroupChange, - groupMasterKey); - } - if (group == null) { - group = groupV2Helper.getDecryptedGroup(groupSecretParams); - } - if (group != null) { - storeProfileKeysFromMembers(group); - final var avatar = group.getAvatar(); - if (avatar != null && !avatar.isEmpty()) { - downloadGroupAvatar(groupId, groupSecretParams, avatar); - } - } - groupInfoV2.setGroup(group, this::resolveRecipient); - account.getGroupStore().updateGroup(groupInfoV2); - } - - return groupInfoV2; - } - - private void storeProfileKeysFromMembers(final DecryptedGroup group) { - for (var member : group.getMembersList()) { - final var uuid = UuidUtil.parseOrThrow(member.getUuid().toByteArray()); - final var recipientId = account.getRecipientStore().resolveRecipient(uuid); - try { - account.getProfileStore() - .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray())); - } catch (InvalidInputException ignored) { - } - } - } - private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { Set queuedActions = new HashSet<>(); for (var cachedMessage : account.getMessageCache().getCachedMessages()) { @@ -1824,7 +1291,7 @@ public class Manager implements Closeable { List actions = null; if (!envelope.isReceipt()) { try { - content = decryptMessage(envelope); + content = dependencies.getCipher().decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { if (!envelope.hasSource()) { final var identifier = e.getSender(); @@ -1915,7 +1382,7 @@ public class Manager implements Closeable { } if (!envelope.isReceipt()) { try { - content = decryptMessage(envelope); + content = dependencies.getCipher().decrypt(envelope); } catch (Exception e) { exception = e; } @@ -2157,7 +1624,7 @@ public class Manager implements Closeable { } if (g.getAvatar().isPresent()) { - downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId()); + downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); } syncGroup.archived = g.isArchived(); account.getGroupStore().updateGroup(syncGroup); @@ -2333,7 +1800,7 @@ public class Manager implements Closeable { } } - private void downloadGroupAvatar(SignalServiceAttachment avatar, GroupId groupId) { + private void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { try { avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream)); } catch (IOException e) { @@ -2341,15 +1808,6 @@ public class Manager implements Closeable { } } - private void downloadGroupAvatar(GroupId groupId, GroupSecretParams groupSecretParams, String cdnKey) { - try { - avatarStore.storeGroupAvatar(groupId, - outputStream -> retrieveGroupV2Avatar(groupSecretParams, cdnKey, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); - } - } - private void downloadProfileAvatar( SignalServiceAddress address, String avatarPath, ProfileKey profileKey ) { @@ -2392,29 +1850,6 @@ public class Manager implements Closeable { } } - private void retrieveGroupV2Avatar( - GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream - ) throws IOException { - var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); - - var tmpFile = IOUtils.createTempFile(); - try (InputStream input = dependencies.getMessageReceiver() - .retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { - var encryptedData = IOUtils.readFully(input); - - var decryptedData = groupOperations.decryptAvatar(encryptedData); - outputStream.write(decryptedData); - } finally { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } - } - private void retrieveProfileAvatar( String avatarPath, ProfileKey profileKey, OutputStream outputStream ) throws IOException { @@ -2490,7 +1925,7 @@ public class Manager implements Closeable { .stream() .map(this::resolveSignalServiceAddress) .collect(Collectors.toList()), - createGroupAvatarAttachment(groupInfo.getGroupId()), + groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()), groupInfo.isMember(account.getSelfRecipientId()), Optional.of(groupInfo.messageExpirationTime), Optional.fromNullable(groupInfo.color), @@ -2639,17 +2074,7 @@ public class Manager implements Closeable { } public GroupInfo getGroup(GroupId groupId) { - return getGroup(groupId, false); - } - - public GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { - final var group = account.getGroupStore().getGroup(groupId); - if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); - ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient); - account.getGroupStore().updateGroup(group); - } - return group; + return groupHelper.getGroup(groupId); } public List getIdentities() { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java new file mode 100644 index 00000000..0b9cc950 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -0,0 +1,628 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.AvatarStore; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.api.SendGroupMessageResults; +import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.groups.GroupIdV2; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupLinkState; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.groups.LastGroupAdminException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.asamk.signal.manager.util.AttachmentUtils; +import org.asamk.signal.manager.util.IOUtils; +import org.signal.storageservice.protos.groups.GroupChange; +import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.signal.zkgroup.groups.GroupSecretParams; +import org.signal.zkgroup.profiles.ProfileKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; +import org.whispersystems.signalservice.api.push.exceptions.ConflictException; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class GroupHelper { + + private final static Logger logger = LoggerFactory.getLogger(GroupHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final SendHelper sendHelper; + private final GroupV2Helper groupV2Helper; + private final AvatarStore avatarStore; + private final SignalServiceAddressResolver addressResolver; + private final RecipientResolver recipientResolver; + + public GroupHelper( + final SignalAccount account, + final SignalDependencies dependencies, + final SendHelper sendHelper, + final GroupV2Helper groupV2Helper, + final AvatarStore avatarStore, + final SignalServiceAddressResolver addressResolver, + final RecipientResolver recipientResolver + ) { + this.account = account; + this.dependencies = dependencies; + this.sendHelper = sendHelper; + this.groupV2Helper = groupV2Helper; + this.avatarStore = avatarStore; + this.addressResolver = addressResolver; + this.recipientResolver = recipientResolver; + } + + public GroupInfo getGroup(GroupId groupId) { + return getGroup(groupId, false); + } + + public Optional createGroupAvatarAttachment(GroupIdV1 groupId) throws IOException { + final var streamDetails = avatarStore.retrieveGroupAvatar(groupId); + if (streamDetails == null) { + return Optional.absent(); + } + + return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); + } + + public GroupInfoV2 getOrMigrateGroup( + final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange + ) { + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + + var groupId = GroupUtils.getGroupIdV2(groupSecretParams); + var groupInfo = getGroup(groupId); + final GroupInfoV2 groupInfoV2; + if (groupInfo instanceof GroupInfoV1) { + // Received a v2 group message for a v1 group, we need to locally migrate the group + account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId()); + groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); + logger.info("Locally migrated group {} to group v2, id: {}", + groupInfo.getGroupId().toBase64(), + groupInfoV2.getGroupId().toBase64()); + } else if (groupInfo instanceof GroupInfoV2) { + groupInfoV2 = (GroupInfoV2) groupInfo; + } else { + groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); + } + + if (groupInfoV2.getGroup() == null || groupInfoV2.getGroup().getRevision() < revision) { + DecryptedGroup group = null; + if (signedGroupChange != null + && groupInfoV2.getGroup() != null + && groupInfoV2.getGroup().getRevision() + 1 == revision) { + group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), + signedGroupChange, + groupMasterKey); + } + if (group == null) { + group = groupV2Helper.getDecryptedGroup(groupSecretParams); + } + if (group != null) { + storeProfileKeysFromMembers(group); + final var avatar = group.getAvatar(); + if (avatar != null && !avatar.isEmpty()) { + downloadGroupAvatar(groupId, groupSecretParams, avatar); + } + } + groupInfoV2.setGroup(group, recipientResolver); + account.getGroupStore().updateGroup(groupInfoV2); + } + + return groupInfoV2; + } + + public Pair createGroup( + String name, Set members, File avatarFile + ) throws IOException, AttachmentInvalidException { + final var selfRecipientId = account.getSelfRecipientId(); + if (members != null && members.contains(selfRecipientId)) { + members = new HashSet<>(members); + members.remove(selfRecipientId); + } + + var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name, + members == null ? Set.of() : members, + avatarFile); + + if (gv2Pair == null) { + // Failed to create v2 group, creating v1 group instead + var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); + gv1.addMembers(List.of(selfRecipientId)); + final var result = updateGroupV1(gv1, name, members, avatarFile); + return new Pair<>(gv1.getGroupId(), result); + } + + final var gv2 = gv2Pair.first(); + final var decryptedGroup = gv2Pair.second(); + + gv2.setGroup(decryptedGroup, recipientResolver); + if (avatarFile != null) { + avatarStore.storeGroupAvatar(gv2.getGroupId(), + outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); + } + + account.getGroupStore().updateGroup(gv2); + + final var messageBuilder = getGroupUpdateMessageBuilder(gv2, null); + + final var result = sendGroupMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId)); + return new Pair<>(gv2.getGroupId(), result); + } + + public SendGroupMessageResults updateGroup( + final GroupId groupId, + final String name, + final String description, + final Set members, + final Set removeMembers, + final Set admins, + final Set removeAdmins, + final boolean resetGroupLink, + final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, + final File avatarFile, + final Integer expirationTimer, + final Boolean isAnnouncementGroup + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { + var group = getGroupForUpdating(groupId); + + if (group instanceof GroupInfoV2) { + try { + return updateGroupV2((GroupInfoV2) group, + name, + description, + members, + removeMembers, + admins, + removeAdmins, + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer, + isAnnouncementGroup); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + group = getGroup(groupId, true); + return updateGroupV2((GroupInfoV2) group, + name, + description, + members, + removeMembers, + admins, + removeAdmins, + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer, + isAnnouncementGroup); + } + } + + final var gv1 = (GroupInfoV1) group; + final var result = updateGroupV1(gv1, name, members, avatarFile); + if (expirationTimer != null) { + setExpirationTimer(gv1, expirationTimer); + } + return result; + } + + public Pair joinGroup( + GroupInviteLinkUrl inviteLinkUrl + ) throws IOException, GroupLinkNotActiveException { + final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), + inviteLinkUrl.getPassword()); + final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(), + inviteLinkUrl.getPassword(), + groupJoinInfo); + final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), + groupJoinInfo.getRevision() + 1, + groupChange.toByteArray()); + + if (group.getGroup() == null) { + // Only requested member, can't send update to group members + return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of())); + } + + final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange); + + return new Pair<>(group.getGroupId(), result); + } + + public SendGroupMessageResults quitGroup( + final GroupId groupId, final Set newAdmins + ) throws IOException, LastGroupAdminException, NotAGroupMemberException, GroupNotFoundException { + var group = getGroupForUpdating(groupId); + if (group instanceof GroupInfoV1) { + return quitGroupV1((GroupInfoV1) group); + } + + try { + return quitGroupV2((GroupInfoV2) group, newAdmins); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + group = getGroup(groupId, true); + return quitGroupV2((GroupInfoV2) group, newAdmins); + } + } + + public SendGroupMessageResults sendGroupInfoRequest( + GroupIdV1 groupId, RecipientId recipientId + ) throws IOException { + var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); + + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); + + // Send group info request message to the recipient who sent us a message with this groupId + return sendGroupMessage(messageBuilder, Set.of(recipientId)); + } + + public SendGroupMessageResults sendGroupInfoMessage( + GroupIdV1 groupId, RecipientId recipientId + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { + GroupInfoV1 g; + var group = getGroupForUpdating(groupId); + if (!(group instanceof GroupInfoV1)) { + throw new IOException("Received an invalid group request for a v2 group!"); + } + g = (GroupInfoV1) group; + + if (!g.isMember(recipientId)) { + throw new NotAGroupMemberException(groupId, g.name); + } + + var messageBuilder = getGroupUpdateMessageBuilder(g); + + // Send group message only to the recipient who requested it + return sendGroupMessage(messageBuilder, Set.of(recipientId)); + } + + private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { + final var group = account.getGroupStore().getGroup(groupId); + if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) { + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); + ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), recipientResolver); + account.getGroupStore().updateGroup(group); + } + return group; + } + + private void downloadGroupAvatar(GroupIdV2 groupId, GroupSecretParams groupSecretParams, String cdnKey) { + try { + avatarStore.storeGroupAvatar(groupId, + outputStream -> retrieveGroupV2Avatar(groupSecretParams, cdnKey, outputStream)); + } catch (IOException e) { + logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); + } + } + + private void retrieveGroupV2Avatar( + GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream + ) throws IOException { + var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); + + var tmpFile = IOUtils.createTempFile(); + try (InputStream input = dependencies.getMessageReceiver() + .retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { + var encryptedData = IOUtils.readFully(input); + + var decryptedData = groupOperations.decryptAvatar(encryptedData); + outputStream.write(decryptedData); + } finally { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete received group avatar temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); + } + } + } + + private void storeProfileKeysFromMembers(final DecryptedGroup group) { + for (var member : group.getMembersList()) { + final var uuid = UuidUtil.parseOrThrow(member.getUuid().toByteArray()); + final var recipientId = account.getRecipientStore().resolveRecipient(uuid); + try { + account.getProfileStore() + .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray())); + } catch (InvalidInputException ignored) { + } + } + } + + private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { + var g = getGroup(groupId); + if (g == null) { + throw new GroupNotFoundException(groupId); + } + if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) { + throw new NotAGroupMemberException(groupId, g.getTitle()); + } + return g; + } + + private SendGroupMessageResults updateGroupV1( + final GroupInfoV1 gv1, final String name, final Set members, final File avatarFile + ) throws IOException, AttachmentInvalidException { + updateGroupV1Details(gv1, name, members, avatarFile); + + account.getGroupStore().updateGroup(gv1); + + var messageBuilder = getGroupUpdateMessageBuilder(gv1); + return sendGroupMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + } + + private void updateGroupV1Details( + final GroupInfoV1 g, final String name, final Collection members, final File avatarFile + ) throws IOException { + if (name != null) { + g.name = name; + } + + if (members != null) { + g.addMembers(members); + } + + if (avatarFile != null) { + avatarStore.storeGroupAvatar(g.getGroupId(), + outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); + } + } + + /** + * Change the expiration timer for a group + */ + private void setExpirationTimer( + GroupInfoV1 groupInfoV1, int messageExpirationTimer + ) throws NotAGroupMemberException, GroupNotFoundException, IOException { + groupInfoV1.messageExpirationTime = messageExpirationTimer; + account.getGroupStore().updateGroup(groupInfoV1); + sendExpirationTimerUpdate(groupInfoV1.getGroupId()); + } + + private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + sendHelper.sendAsGroupMessage(messageBuilder, groupId); + } + + private SendGroupMessageResults updateGroupV2( + final GroupInfoV2 group, + final String name, + final String description, + final Set members, + final Set removeMembers, + final Set admins, + final Set removeAdmins, + final boolean resetGroupLink, + final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, + final File avatarFile, + final Integer expirationTimer, + final Boolean isAnnouncementGroup + ) throws IOException { + SendGroupMessageResults result = null; + if (group.isPendingMember(account.getSelfRecipientId())) { + var groupGroupChangePair = groupV2Helper.acceptInvite(group); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (members != null) { + final var newMembers = new HashSet<>(members); + newMembers.removeAll(group.getMembers()); + if (newMembers.size() > 0) { + var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + } + + if (removeMembers != null) { + var existingRemoveMembers = new HashSet<>(removeMembers); + existingRemoveMembers.retainAll(group.getMembers()); + existingRemoveMembers.remove(account.getSelfRecipientId());// self can be removed with sendQuitGroupMessage + if (existingRemoveMembers.size() > 0) { + var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + var pendingRemoveMembers = new HashSet<>(removeMembers); + pendingRemoveMembers.retainAll(group.getPendingMembers()); + if (pendingRemoveMembers.size() > 0) { + var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + } + + if (admins != null) { + final var newAdmins = new HashSet<>(admins); + newAdmins.retainAll(group.getMembers()); + newAdmins.removeAll(group.getAdminMembers()); + if (newAdmins.size() > 0) { + for (var admin : newAdmins) { + var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true); + result = sendUpdateGroupV2Message(group, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + } + } + + if (removeAdmins != null) { + final var existingRemoveAdmins = new HashSet<>(removeAdmins); + existingRemoveAdmins.retainAll(group.getAdminMembers()); + if (existingRemoveAdmins.size() > 0) { + for (var admin : existingRemoveAdmins) { + var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false); + result = sendUpdateGroupV2Message(group, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + } + } + + if (resetGroupLink) { + var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (groupLinkState != null) { + var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (addMemberPermission != null) { + var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (editDetailsPermission != null) { + var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (expirationTimer != null) { + var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (isAnnouncementGroup != null) { + var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (name != null || description != null || avatarFile != null) { + var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile); + if (avatarFile != null) { + avatarStore.storeGroupAvatar(group.getGroupId(), + outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream)); + } + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + return result; + } + + private SendGroupMessageResults quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { + var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) + .withId(groupInfoV1.getGroupId().serialize()) + .build(); + + var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); + groupInfoV1.removeMember(account.getSelfRecipientId()); + account.getGroupStore().updateGroup(groupInfoV1); + return sendGroupMessage(messageBuilder, + groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + } + + private SendGroupMessageResults quitGroupV2( + final GroupInfoV2 groupInfoV2, final Set newAdmins + ) throws LastGroupAdminException, IOException { + final var currentAdmins = groupInfoV2.getAdminMembers(); + newAdmins.removeAll(currentAdmins); + newAdmins.retainAll(groupInfoV2.getMembers()); + if (currentAdmins.contains(account.getSelfRecipientId()) + && currentAdmins.size() == 1 + && groupInfoV2.getMembers().size() > 1 + && newAdmins.size() == 0) { + // Last admin can't leave the group, unless she's also the last member + throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle()); + } + final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); + groupInfoV2.setGroup(groupGroupChangePair.first(), recipientResolver); + account.getGroupStore().updateGroup(groupInfoV2); + + var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); + return sendGroupMessage(messageBuilder, + groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + } + + private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { + var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) + .withId(g.getGroupId().serialize()) + .withName(g.name) + .withMembers(g.getMembers() + .stream() + .map(addressResolver::resolveSignalServiceAddress) + .collect(Collectors.toList())); + + try { + final var attachment = createGroupAvatarAttachment(g.getGroupId()); + if (attachment.isPresent()) { + group.withAvatar(attachment.get()); + } + } catch (IOException e) { + throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); + } + + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.getMessageExpirationTime()); + } + + private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { + var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) + .withRevision(g.getGroup().getRevision()) + .withSignedGroupChange(signedGroupChange); + return SignalServiceDataMessage.newBuilder() + .asGroupMessage(group.build()) + .withExpiration(g.getMessageExpirationTime()); + } + + private SendGroupMessageResults sendUpdateGroupV2Message( + GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange + ) throws IOException { + final var selfRecipientId = account.getSelfRecipientId(); + final var members = group.getMembersIncludingPendingWithout(selfRecipientId); + group.setGroup(newDecryptedGroup, recipientResolver); + members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId)); + account.getGroupStore().updateGroup(group); + + final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); + return sendGroupMessage(messageBuilder, members); + } + + private SendGroupMessageResults sendGroupMessage( + final SignalServiceDataMessage.Builder messageBuilder, final Set members + ) throws IOException { + final var timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); + final var results = sendHelper.sendGroupMessage(messageBuilder.build(), members); + return new SendGroupMessageResults(timestamp, results); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index e161673e..19240cef 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -43,6 +43,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Set; import java.util.UUID; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class GroupV2Helper { @@ -59,8 +60,6 @@ public class GroupV2Helper { private final GroupsV2Api groupsV2Api; - private final GroupAuthorizationProvider groupAuthorizationProvider; - private final SignalServiceAddressResolver addressResolver; public GroupV2Helper( @@ -69,7 +68,6 @@ public class GroupV2Helper { final SelfRecipientIdProvider selfRecipientIdProvider, final GroupsV2Operations groupsV2Operations, final GroupsV2Api groupsV2Api, - final GroupAuthorizationProvider groupAuthorizationProvider, final SignalServiceAddressResolver addressResolver ) { this.profileKeyCredentialProvider = profileKeyCredentialProvider; @@ -77,14 +75,12 @@ public class GroupV2Helper { this.selfRecipientIdProvider = selfRecipientIdProvider; this.groupsV2Operations = groupsV2Operations; this.groupsV2Api = groupsV2Api; - this.groupAuthorizationProvider = groupAuthorizationProvider; this.addressResolver = addressResolver; } public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) { try { - final var groupsV2AuthorizationString = groupAuthorizationProvider.getAuthorizationForToday( - groupSecretParams); + final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString); } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); @@ -99,7 +95,7 @@ public class GroupV2Helper { return groupsV2Api.getGroupJoinInfo(groupSecretParams, Optional.fromNullable(password).transform(GroupLinkPassword::serialize), - groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams)); + getGroupAuthForToday(groupSecretParams)); } public Pair createGroup( @@ -116,7 +112,7 @@ public class GroupV2Helper { final GroupsV2AuthorizationString groupAuthForToday; final DecryptedGroup decryptedGroup; try { - groupAuthForToday = groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams); + groupAuthForToday = getGroupAuthForToday(groupSecretParams); groupsV2Api.putNewGroup(newGroup, groupAuthForToday); decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday); } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { @@ -214,7 +210,7 @@ public class GroupV2Helper { final var avatarBytes = readAvatarBytes(avatarFile); var avatarCdnKey = groupsV2Api.uploadAvatar(avatarBytes, groupSecretParams, - groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams)); + getGroupAuthForToday(groupSecretParams)); change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey)); } @@ -487,7 +483,7 @@ public class GroupV2Helper { } var signedGroupChange = groupsV2Api.patchGroup(changeActions, - groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams), + getGroupAuthForToday(groupSecretParams), Optional.absent()); return new Pair<>(decryptedGroupState, signedGroupChange); @@ -503,7 +499,7 @@ public class GroupV2Helper { final var changeActions = change.setRevision(nextRevision).build(); return groupsV2Api.patchGroup(changeActions, - groupAuthorizationProvider.getAuthorizationForToday(groupSecretParams), + getGroupAuthForToday(groupSecretParams), Optional.fromNullable(password).transform(GroupLinkPassword::serialize)); } @@ -534,4 +530,26 @@ public class GroupV2Helper { return null; } + + private static int currentTimeDays() { + return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()); + } + + private GroupsV2AuthorizationString getGroupAuthForToday( + final GroupSecretParams groupSecretParams + ) throws IOException { + final var today = currentTimeDays(); + // Returns credentials for the next 7 days + final var credentials = groupsV2Api.getCredentials(today); + // TODO cache credentials until they expire + var authCredentialResponse = credentials.get(today); + final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId()) + .getUuid() + .get(); + try { + return groupsV2Api.getGroupsV2AuthorizationString(uuid, today, groupSecretParams, authCredentialResponse); + } catch (VerificationFailedException e) { + throw new IOException(e); + } + } } From 7f64a9812ca5bb10e8f57cacf3d22b904bd200b4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 09:34:06 +0200 Subject: [PATCH 0771/2005] Prevent non-admins from sending to announcement groups Only reactions are allowed --- .../org/asamk/signal/manager/Manager.java | 36 ++++++++++++------- .../GroupSendingNotAllowedException.java | 8 +++++ .../signal/manager/helper/GroupHelper.java | 7 ++-- .../signal/manager/helper/SendHelper.java | 25 ++++++++++--- .../signal/commands/RemoteDeleteCommand.java | 3 +- .../asamk/signal/commands/SendCommand.java | 3 +- .../signal/commands/SendReactionCommand.java | 3 +- .../signal/commands/SendTypingCommand.java | 3 +- .../signal/commands/UpdateGroupCommand.java | 3 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 17 ++++----- 10 files changed, 76 insertions(+), 32 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/groups/GroupSendingNotAllowedException.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 3857d803..13696235 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -31,6 +31,7 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -697,7 +698,7 @@ public class Manager implements Closeable { File avatarFile, Integer expirationTimer, Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { return groupHelper.updateGroup(groupId, name, description, @@ -722,7 +723,7 @@ public class Manager implements Closeable { public SendMessageResults sendMessage( SignalServiceDataMessage.Builder messageBuilder, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var results = new HashMap>(); long timestamp = System.currentTimeMillis(); messageBuilder.withTimestamp(timestamp); @@ -745,7 +746,7 @@ public class Manager implements Closeable { public void sendTypingMessage( SignalServiceTypingMessage.Action action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var timestamp = System.currentTimeMillis(); for (var recipient : recipients) { if (recipient instanceof RecipientIdentifier.Single) { @@ -806,7 +807,7 @@ public class Manager implements Closeable { public SendMessageResults sendMessage( Message message, Set recipients - ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); return sendMessage(messageBuilder, recipients); @@ -836,7 +837,7 @@ public class Manager implements Closeable { public SendMessageResults sendRemoteDeleteMessage( long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); return sendMessage(messageBuilder, recipients); @@ -848,7 +849,7 @@ public class Manager implements Closeable { RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var targetAuthorRecipientId = resolveRecipient(targetAuthor); var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, @@ -864,7 +865,7 @@ public class Manager implements Closeable { try { return sendMessage(messageBuilder, recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } finally { for (var recipient : recipients) { @@ -931,7 +932,7 @@ public class Manager implements Closeable { final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); try { sendMessage(messageBuilder, Set.of(recipient)); - } catch (NotAGroupMemberException | GroupNotFoundException e) { + } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } } @@ -1109,7 +1110,7 @@ public class Manager implements Closeable { public void sendTypingMessage( TypingAction action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { sendTypingMessage(action.toSignalService(), recipients); } @@ -1530,9 +1531,20 @@ public class Manager implements Closeable { } final var recipientId = resolveRecipient(source); - return !group.isMember(recipientId) || ( - group.isAnnouncementGroup() && !group.isAdmin(recipientId) - ); + if (!group.isMember(recipientId)) { + return true; + } + + if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { + return message.getBody().isPresent() + || message.getAttachments().isPresent() + || message.getQuote() + .isPresent() + || message.getPreviews().isPresent() + || message.getMentions().isPresent() + || message.getSticker().isPresent(); + } + return false; } private List handleMessage( diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/GroupSendingNotAllowedException.java b/lib/src/main/java/org/asamk/signal/manager/groups/GroupSendingNotAllowedException.java new file mode 100644 index 00000000..1a2fa432 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/groups/GroupSendingNotAllowedException.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.groups; + +public class GroupSendingNotAllowedException extends Exception { + + public GroupSendingNotAllowedException(GroupId groupId, String groupName) { + super("User is not allowed to send message to group: " + groupName + " (" + groupId.toBase64() + ")"); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 0b9cc950..9ff3134e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -12,6 +12,7 @@ import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -195,7 +196,7 @@ public class GroupHelper { final File avatarFile, final Integer expirationTimer, final Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { var group = getGroupForUpdating(groupId); if (group instanceof GroupInfoV2) { @@ -410,13 +411,13 @@ public class GroupHelper { */ private void setExpirationTimer( GroupInfoV1 groupInfoV1, int messageExpirationTimer - ) throws NotAGroupMemberException, GroupNotFoundException, IOException { + ) throws NotAGroupMemberException, GroupNotFoundException, IOException, GroupSendingNotAllowedException { groupInfoV1.messageExpirationTime = messageExpirationTimer; account.getGroupStore().updateGroup(groupInfoV1); sendExpirationTimerUpdate(groupInfoV1.getGroupId()); } - private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { + private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); sendHelper.sendAsGroupMessage(messageBuilder, groupId); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index b04ff40d..f92d7bde 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.SignalAccount; @@ -86,19 +87,32 @@ public class SendHelper { */ public List sendAsGroupMessage( SignalServiceDataMessage.Builder messageBuilder, GroupId groupId - ) throws IOException, GroupNotFoundException, NotAGroupMemberException { + ) throws IOException, GroupNotFoundException, NotAGroupMemberException, GroupSendingNotAllowedException { final var g = getGroupForSending(groupId); return sendAsGroupMessage(messageBuilder, g); } private List sendAsGroupMessage( final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g - ) throws IOException { + ) throws IOException, GroupSendingNotAllowedException { GroupUtils.setGroupContext(messageBuilder, g); messageBuilder.withExpiration(g.getMessageExpirationTime()); + final var message = messageBuilder.build(); final var recipients = g.getMembersWithout(account.getSelfRecipientId()); - return sendGroupMessage(messageBuilder.build(), recipients); + + if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) { + if (message.getBody().isPresent() + || message.getAttachments().isPresent() + || message.getQuote().isPresent() + || message.getPreviews().isPresent() + || message.getMentions().isPresent() + || message.getSticker().isPresent()) { + throw new GroupSendingNotAllowedException(g.getGroupId(), g.getTitle()); + } + } + + return sendGroupMessage(message, recipients); } /** @@ -181,8 +195,11 @@ public class SendHelper { public void sendGroupTypingMessage( SignalServiceTypingMessage message, GroupId groupId - ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var g = getGroupForSending(groupId); + if (g.isAnnouncementGroup() && !g.isAdmin(account.getSelfRecipientId())) { + throw new GroupSendingNotAllowedException(groupId, g.getTitle()); + } final var messageSender = dependencies.getMessageSender(); final var recipientIdList = new ArrayList<>(g.getMembersWithout(account.getSelfRecipientId())); final var addresses = recipientIdList.stream() diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index f033e0b1..e482dd58 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -13,6 +13,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; @@ -60,7 +61,7 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { final var results = m.sendRemoteDeleteMessage(targetTimestamp, recipientIdentifiers); outputResult(outputWriter, results.getTimestamp()); ErrorUtils.handleSendMessageResults(results.getResults()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index a1e4c296..c29d0268 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -17,6 +17,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; @@ -108,7 +109,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { ErrorUtils.handleSendMessageResults(results.getResults()); } catch (AttachmentInvalidException | IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } } diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index c8e339a1..98e5f5ec 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -13,6 +13,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; @@ -76,7 +77,7 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { recipientIdentifiers); outputResult(outputWriter, results.getTimestamp()); ErrorUtils.handleSendMessageResults(results.getResults()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index 14139885..ace4da85 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -11,6 +11,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; @@ -59,7 +60,7 @@ public class SendTypingCommand implements JsonRpcLocalCommand { m.sendTypingMessage(action, recipientIdentifiers); } catch (IOException | UntrustedIdentityException e) { throw new UserErrorException("Failed to send message: " + e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index fc2cfbc0..a8d556f3 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -17,6 +17,7 @@ import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; @@ -170,7 +171,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { outputResult(outputWriter, timestamp, isNewGroup ? groupId : null); } catch (AttachmentInvalidException e) { throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index c5be7501..315e1d8e 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -10,6 +10,7 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.identities.IdentityInfo; @@ -77,7 +78,7 @@ public class DbusSignalImpl implements Signal { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -104,7 +105,7 @@ public class DbusSignalImpl implements Signal { return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -120,7 +121,7 @@ public class DbusSignalImpl implements Signal { return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -158,7 +159,7 @@ public class DbusSignalImpl implements Signal { return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -176,7 +177,7 @@ public class DbusSignalImpl implements Signal { throw new Error.AttachmentInvalid(e.getMessage()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -200,7 +201,7 @@ public class DbusSignalImpl implements Signal { return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); @@ -225,7 +226,7 @@ public class DbusSignalImpl implements Signal { return results.getTimestamp(); } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } } @@ -337,7 +338,7 @@ public class DbusSignalImpl implements Signal { } } catch (IOException e) { throw new Error.Failure(e.getMessage()); - } catch (GroupNotFoundException | NotAGroupMemberException e) { + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (AttachmentInvalidException e) { throw new Error.AttachmentInvalid(e.getMessage()); From cd3741d236c7cb64052ba0468a48d480769067e7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 10:28:04 +0200 Subject: [PATCH 0772/2005] Rename internal quitGroup method --- lib/src/main/java/org/asamk/signal/manager/Manager.java | 2 +- src/main/java/org/asamk/signal/commands/QuitGroupCommand.java | 2 +- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 13696235..5de65f3e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -665,7 +665,7 @@ public class Manager implements Closeable { return account.getGroupStore().getGroups(); } - public SendGroupMessageResults sendQuitGroupMessage( + public SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { final var newAdmins = getRecipientIds(groupAdmins); diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index c39f298d..03bf232b 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -54,7 +54,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { try { try { - final var results = m.sendQuitGroupMessage(groupId, groupAdmins); + final var results = m.quitGroup(groupId, groupAdmins); final var timestamp = results.getTimestamp(); outputResult(outputWriter, timestamp); handleSendMessageResults(results.getResults()); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 315e1d8e..392b2df0 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -420,7 +420,7 @@ public class DbusSignalImpl implements Signal { public void quitGroup(final byte[] groupId) { var group = getGroupId(groupId); try { - m.sendQuitGroupMessage(group, Set.of()); + m.quitGroup(group, Set.of()); } catch (GroupNotFoundException | NotAGroupMemberException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException | LastGroupAdminException e) { From e532a24cf8cba6aede416b1b21aa72e95519c383 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 10:56:30 +0200 Subject: [PATCH 0773/2005] Move more profile functionality to ProfileHelper --- .../asamk/signal/manager/HandleAction.java | 2 +- .../org/asamk/signal/manager/Manager.java | 249 ++-------------- .../signal/manager/helper/ProfileHelper.java | 265 +++++++++++++++++- 3 files changed, 283 insertions(+), 233 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java b/lib/src/main/java/org/asamk/signal/manager/HandleAction.java index 2396df06..8639806f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/HandleAction.java @@ -170,7 +170,7 @@ class RetrieveProfileAction implements HandleAction { @Override public void execute(Manager m) throws Throwable { - m.getRecipientProfile(recipientId, true); + m.refreshRecipientProfile(recipientId); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 5de65f3e..ac7b571f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -58,14 +58,12 @@ import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.KeyUtils; -import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.ProtocolInvalidMessageException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; -import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; @@ -106,8 +104,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; -import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; -import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; @@ -137,7 +133,6 @@ import java.nio.file.Files; import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Base64; import java.util.Collection; import java.util.Date; import java.util.HashMap; @@ -205,26 +200,29 @@ public class Manager implements Closeable { account.getSignalProtocolStore(), executor, sessionLock); - this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); + this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); + this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, account.getProfileStore()::getProfileKey, this::getRecipientProfile, this::getSenderCertificate); - this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey, + this.profileHelper = new ProfileHelper(account, + dependencies, + avatarStore, + account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, dependencies::getProfileService, dependencies::getMessageReceiver, this::resolveSignalServiceAddress); - final GroupV2Helper groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, + final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, this::getRecipientProfile, account::getSelfRecipientId, dependencies.getGroupsV2Operations(), dependencies.getGroupsV2Api(), this::resolveSignalServiceAddress); - this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); - this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); - this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); this.sendHelper = new SendHelper(account, dependencies, unidentifiedAccessHelper, @@ -246,7 +244,7 @@ public class Manager implements Closeable { return account.getUsername(); } - public SignalServiceAddress getSelfAddress() { + private SignalServiceAddress getSelfAddress() { return account.getSelfAddress(); } @@ -377,45 +375,12 @@ public class Manager implements Closeable { public void setProfile( String givenName, final String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException { - var profile = getRecipientProfile(account.getSelfRecipientId()); - var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); - if (givenName != null) { - builder.withGivenName(givenName); - } - if (familyName != null) { - builder.withFamilyName(familyName); - } - if (about != null) { - builder.withAbout(about); - } - if (aboutEmoji != null) { - builder.withAboutEmoji(aboutEmoji); - } - var newProfile = builder.build(); + profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); - try (final var streamDetails = avatar == null - ? avatarStore.retrieveProfileAvatar(getSelfAddress()) - : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { - dependencies.getAccountManager() - .setVersionedProfile(account.getUuid(), - account.getProfileKey(), - newProfile.getInternalServiceName(), - newProfile.getAbout() == null ? "" : newProfile.getAbout(), - newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), - Optional.absent(), - streamDetails); - } - - if (avatar != null) { - if (avatar.isPresent()) { - avatarStore.storeProfileAvatar(getSelfAddress(), - outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream)); - } else { - avatarStore.deleteProfileAvatar(getSelfAddress()); - } - } - account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile); + sendSyncFetchProfileMessage(); + } + private void sendSyncFetchProfileMessage() throws IOException { sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); } @@ -522,134 +487,12 @@ public class Manager implements Closeable { return record; } - public Profile getRecipientProfile( - RecipientId recipientId - ) { - return getRecipientProfile(recipientId, false); + public Profile getRecipientProfile(RecipientId recipientId) { + return profileHelper.getRecipientProfile(recipientId); } - private final Set pendingProfileRequest = new HashSet<>(); - - Profile getRecipientProfile( - RecipientId recipientId, boolean force - ) { - var profile = account.getProfileStore().getProfile(recipientId); - - var now = System.currentTimeMillis(); - // Profiles are cached for 24h before retrieving them again, unless forced - if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) { - return profile; - } - - synchronized (pendingProfileRequest) { - if (pendingProfileRequest.contains(recipientId)) { - return profile; - } - pendingProfileRequest.add(recipientId); - } - final SignalServiceProfile encryptedProfile; - try { - encryptedProfile = retrieveEncryptedProfile(recipientId); - } finally { - synchronized (pendingProfileRequest) { - pendingProfileRequest.remove(recipientId); - } - } - if (encryptedProfile == null) { - return null; - } - - profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile); - account.getProfileStore().storeProfile(recipientId, profile); - - return profile; - } - - private Profile decryptProfileIfKeyKnown( - final RecipientId recipientId, final SignalServiceProfile encryptedProfile - ) { - var profileKey = account.getProfileStore().getProfileKey(recipientId); - if (profileKey == null) { - return new Profile(System.currentTimeMillis(), - null, - null, - null, - null, - ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null), - ProfileUtils.getCapabilities(encryptedProfile)); - } - - return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile); - } - - private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) { - try { - return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile(); - } catch (IOException e) { - logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); - return null; - } - } - - private ProfileAndCredential retrieveProfileAndCredential( - final RecipientId recipientId, final SignalServiceProfile.RequestType requestType - ) throws IOException { - final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType); - final var profile = profileAndCredential.getProfile(); - - try { - var newIdentity = account.getIdentityKeyStore() - .saveIdentity(recipientId, - new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())), - new Date()); - - if (newIdentity) { - account.getSessionStore().archiveSessions(recipientId); - } - } catch (InvalidKeyException ignored) { - logger.warn("Got invalid identity key in profile for {}", - resolveSignalServiceAddress(recipientId).getIdentifier()); - } - return profileAndCredential; - } - - private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) { - var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId); - if (profileKeyCredential != null) { - return profileKeyCredential; - } - - ProfileAndCredential profileAndCredential; - try { - profileAndCredential = retrieveProfileAndCredential(recipientId, - SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); - } catch (IOException e) { - logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); - return null; - } - - profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull(); - account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential); - - var profileKey = account.getProfileStore().getProfileKey(recipientId); - if (profileKey != null) { - final var profile = decryptProfileAndDownloadAvatar(recipientId, - profileKey, - profileAndCredential.getProfile()); - account.getProfileStore().storeProfile(recipientId, profile); - } - - return profileKeyCredential; - } - - private Profile decryptProfileAndDownloadAvatar( - final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile - ) { - if (encryptedProfile.getAvatar() != null) { - downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey); - } - - return ProfileUtils.decryptProfile(profileKey, encryptedProfile); + public void refreshRecipientProfile(RecipientId recipientId) { + profileHelper.refreshRecipientProfile(recipientId); } private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { @@ -1784,7 +1627,7 @@ public class Manager implements Closeable { if (syncMessage.getFetchType().isPresent()) { switch (syncMessage.getFetchType().get()) { case LOCAL_PROFILE: - getRecipientProfile(account.getSelfRecipientId(), true); + actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); case STORAGE_MANIFEST: // TODO } @@ -1820,20 +1663,6 @@ public class Manager implements Closeable { } } - private void downloadProfileAvatar( - SignalServiceAddress address, String avatarPath, ProfileKey profileKey - ) { - try { - avatarStore.storeProfileAvatar(address, - outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream)); - } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage()); - } - } - public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { return attachmentStore.getAttachmentFile(attachmentId); } @@ -1862,28 +1691,6 @@ public class Manager implements Closeable { } } - private void retrieveProfileAvatar( - String avatarPath, ProfileKey profileKey, OutputStream outputStream - ) throws IOException { - var tmpFile = IOUtils.createTempFile(); - try (var input = dependencies.getMessageReceiver() - .retrieveProfileAvatar(avatarPath, - tmpFile, - profileKey, - ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { - // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ... - IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); - } finally { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } - } - private void retrieveAttachment( final SignalServiceAttachment attachment, final OutputStream outputStream ) throws IOException { @@ -2069,17 +1876,15 @@ public class Manager implements Closeable { public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { final var recipientId = resolveRecipient(recipientIdentifier); - final var recipient = account.getRecipientStore().getRecipient(recipientId); - if (recipient == null) { - return null; + + final var contact = account.getRecipientStore().getContact(recipientId); + if (contact != null && !Util.isEmpty(contact.getName())) { + return contact.getName(); } - if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) { - return recipient.getContact().getName(); - } - - if (recipient.getProfile() != null && recipient.getProfile() != null) { - return recipient.getProfile().getDisplayName(); + final var profile = getRecipientProfile(recipientId); + if (profile != null) { + return profile.getDisplayName(); } return null; @@ -2188,7 +1993,7 @@ public class Manager implements Closeable { } } else { // Retrieve profile to get the current identity key from the server - retrieveEncryptedProfile(recipientId); + refreshRecipientProfile(recipientId); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index c3c74b0b..ac75a573 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -1,7 +1,20 @@ package org.asamk.signal.manager.helper; +import org.asamk.signal.manager.AvatarStore; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.ProfileUtils; +import org.asamk.signal.manager.util.Utils; import org.signal.zkgroup.profiles.ProfileKey; +import org.signal.zkgroup.profiles.ProfileKeyCredential; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; @@ -12,29 +25,43 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.internal.ServiceResponse; +import java.io.File; import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Base64; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; import io.reactivex.rxjava3.core.Single; public final class ProfileHelper { + private final static Logger logger = LoggerFactory.getLogger(ProfileHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final AvatarStore avatarStore; private final ProfileKeyProvider profileKeyProvider; - private final UnidentifiedAccessProvider unidentifiedAccessProvider; - private final ProfileServiceProvider profileServiceProvider; - private final MessageReceiverProvider messageReceiverProvider; - private final SignalServiceAddressResolver addressResolver; public ProfileHelper( + final SignalAccount account, + final SignalDependencies dependencies, + final AvatarStore avatarStore, final ProfileKeyProvider profileKeyProvider, final UnidentifiedAccessProvider unidentifiedAccessProvider, final ProfileServiceProvider profileServiceProvider, final MessageReceiverProvider messageReceiverProvider, final SignalServiceAddressResolver addressResolver ) { + this.account = account; + this.dependencies = dependencies; + this.avatarStore = avatarStore; this.profileKeyProvider = profileKeyProvider; this.unidentifiedAccessProvider = unidentifiedAccessProvider; this.profileServiceProvider = profileServiceProvider; @@ -42,7 +69,193 @@ public final class ProfileHelper { this.addressResolver = addressResolver; } - public ProfileAndCredential retrieveProfileSync( + public Profile getRecipientProfile(RecipientId recipientId) { + return getRecipientProfile(recipientId, false); + } + + public void refreshRecipientProfile(RecipientId recipientId) { + getRecipientProfile(recipientId, true); + } + + public ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) { + var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId); + if (profileKeyCredential != null) { + return profileKeyCredential; + } + + ProfileAndCredential profileAndCredential; + try { + profileAndCredential = retrieveProfileAndCredential(recipientId, + SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); + } catch (IOException e) { + logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); + return null; + } + + profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull(); + account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential); + + var profileKey = account.getProfileStore().getProfileKey(recipientId); + if (profileKey != null) { + final var profile = decryptProfileAndDownloadAvatar(recipientId, + profileKey, + profileAndCredential.getProfile()); + account.getProfileStore().storeProfile(recipientId, profile); + } + + return profileKeyCredential; + } + + /** + * @param givenName if null, the previous givenName will be kept + * @param familyName if null, the previous familyName will be kept + * @param about if null, the previous about text will be kept + * @param aboutEmoji if null, the previous about emoji will be kept + * @param avatar if avatar is null the image from the local avatar store is used (if present), + */ + public void setProfile( + String givenName, final String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException { + var profile = getRecipientProfile(account.getSelfRecipientId()); + var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); + if (givenName != null) { + builder.withGivenName(givenName); + } + if (familyName != null) { + builder.withFamilyName(familyName); + } + if (about != null) { + builder.withAbout(about); + } + if (aboutEmoji != null) { + builder.withAboutEmoji(aboutEmoji); + } + var newProfile = builder.build(); + + try (final var streamDetails = avatar == null + ? avatarStore.retrieveProfileAvatar(account.getSelfAddress()) + : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { + dependencies.getAccountManager() + .setVersionedProfile(account.getUuid(), + account.getProfileKey(), + newProfile.getInternalServiceName(), + newProfile.getAbout() == null ? "" : newProfile.getAbout(), + newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), + Optional.absent(), + streamDetails); + } + + if (avatar != null) { + if (avatar.isPresent()) { + avatarStore.storeProfileAvatar(account.getSelfAddress(), + outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream)); + } else { + avatarStore.deleteProfileAvatar(account.getSelfAddress()); + } + } + account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile); + } + + private final Set pendingProfileRequest = new HashSet<>(); + + private Profile getRecipientProfile(RecipientId recipientId, boolean force) { + var profile = account.getProfileStore().getProfile(recipientId); + + var now = System.currentTimeMillis(); + // Profiles are cached for 24h before retrieving them again, unless forced + if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) { + return profile; + } + + synchronized (pendingProfileRequest) { + if (pendingProfileRequest.contains(recipientId)) { + return profile; + } + pendingProfileRequest.add(recipientId); + } + final SignalServiceProfile encryptedProfile; + try { + encryptedProfile = retrieveEncryptedProfile(recipientId); + } finally { + synchronized (pendingProfileRequest) { + pendingProfileRequest.remove(recipientId); + } + } + if (encryptedProfile == null) { + return null; + } + + profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile); + account.getProfileStore().storeProfile(recipientId, profile); + + return profile; + } + + private Profile decryptProfileIfKeyKnown( + final RecipientId recipientId, final SignalServiceProfile encryptedProfile + ) { + var profileKey = account.getProfileStore().getProfileKey(recipientId); + if (profileKey == null) { + return new Profile(System.currentTimeMillis(), + null, + null, + null, + null, + ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null), + ProfileUtils.getCapabilities(encryptedProfile)); + } + + return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile); + } + + private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) { + try { + return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile(); + } catch (IOException e) { + logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); + return null; + } + } + + private SignalServiceProfile retrieveProfileSync(String username) throws IOException { + return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent()); + } + + private ProfileAndCredential retrieveProfileAndCredential( + final RecipientId recipientId, final SignalServiceProfile.RequestType requestType + ) throws IOException { + final var profileAndCredential = retrieveProfileSync(recipientId, requestType); + final var profile = profileAndCredential.getProfile(); + + try { + var newIdentity = account.getIdentityKeyStore() + .saveIdentity(recipientId, + new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())), + new Date()); + + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } + } catch (InvalidKeyException ignored) { + logger.warn("Got invalid identity key in profile for {}", + addressResolver.resolveSignalServiceAddress(recipientId).getIdentifier()); + } + return profileAndCredential; + } + + private Profile decryptProfileAndDownloadAvatar( + final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile + ) { + if (encryptedProfile.getAvatar() != null) { + downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId), + encryptedProfile.getAvatar(), + profileKey); + } + + return ProfileUtils.decryptProfile(profileKey, encryptedProfile); + } + + private ProfileAndCredential retrieveProfileSync( RecipientId recipientId, SignalServiceProfile.RequestType requestType ) throws IOException { try { @@ -58,11 +271,7 @@ public final class ProfileHelper { } } - public SignalServiceProfile retrieveProfileSync(String username) throws IOException { - return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent()); - } - - public Single retrieveProfile( + private Single retrieveProfile( RecipientId recipientId, SignalServiceProfile.RequestType requestType ) throws IOException { var unidentifiedAccess = getUnidentifiedAccess(recipientId); @@ -106,6 +315,42 @@ public final class ProfileHelper { }); } + private void downloadProfileAvatar( + SignalServiceAddress address, String avatarPath, ProfileKey profileKey + ) { + try { + avatarStore.storeProfileAvatar(address, + outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream)); + } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage()); + } + } + + private void retrieveProfileAvatar( + String avatarPath, ProfileKey profileKey, OutputStream outputStream + ) throws IOException { + var tmpFile = IOUtils.createTempFile(); + try (var input = dependencies.getMessageReceiver() + .retrieveProfileAvatar(avatarPath, + tmpFile, + profileKey, + ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { + // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ... + IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); + } finally { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); + } + } + } + private Optional getUnidentifiedAccess(RecipientId recipientId) { var unidentifiedAccess = unidentifiedAccessProvider.getAccessFor(recipientId); From debbaa81ba9a371c5529bac543a3ef8c10fcc5f5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 12:05:15 +0200 Subject: [PATCH 0774/2005] Extract AttachmentHelper and SyncHelper --- .../org/asamk/signal/manager/Manager.java | 504 ++---------------- .../manager/helper/AttachmentHelper.java | 122 +++++ .../signal/manager/helper/GroupHelper.java | 18 + .../signal/manager/helper/SyncHelper.java | 373 +++++++++++++ 4 files changed, 562 insertions(+), 455 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index ac7b571f..936f625c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -35,11 +35,13 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.helper.AttachmentHelper; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.jobs.Context; import org.asamk.signal.manager.jobs.Job; @@ -55,8 +57,6 @@ import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.StickerPackId; -import org.asamk.signal.manager.util.AttachmentUtils; -import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; @@ -69,7 +69,6 @@ import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.fingerprint.Fingerprint; import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; @@ -82,30 +81,15 @@ import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; -import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; -import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; -import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; -import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; -import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -113,23 +97,17 @@ import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableExcept import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; -import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; import java.io.Closeable; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; @@ -165,6 +143,8 @@ public class Manager implements Closeable { private final ProfileHelper profileHelper; private final PinHelper pinHelper; private final SendHelper sendHelper; + private final SyncHelper syncHelper; + private final AttachmentHelper attachmentHelper; private final GroupHelper groupHelper; private final AvatarStore avatarStore; @@ -204,6 +184,7 @@ public class Manager implements Closeable { this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, account.getProfileStore()::getProfileKey, @@ -233,11 +214,19 @@ public class Manager implements Closeable { this::refreshRegisteredUser); this.groupHelper = new GroupHelper(account, dependencies, + attachmentHelper, sendHelper, groupV2Helper, avatarStore, this::resolveSignalServiceAddress, this::resolveRecipient); + this.syncHelper = new SyncHelper(account, + attachmentHelper, + sendHelper, + groupHelper, + avatarStore, + this::resolveSignalServiceAddress, + this::resolveRecipient); } public String getUsername() { @@ -376,12 +365,7 @@ public class Manager implements Closeable { String givenName, final String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException { profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); - - sendSyncFetchProfileMessage(); - } - - private void sendSyncFetchProfileMessage() throws IOException { - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); + syncHelper.sendSyncFetchProfileMessage(); } public void unregister() throws IOException { @@ -495,15 +479,6 @@ public class Manager implements Closeable { profileHelper.refreshRecipientProfile(recipientId); } - private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { - final var streamDetails = avatarStore.retrieveContactAvatar(address); - if (streamDetails == null) { - return Optional.absent(); - } - - return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); - } - public List getGroups() { return account.getGroupStore().getGroups(); } @@ -516,8 +491,7 @@ public class Manager implements Closeable { } public void deleteGroup(GroupId groupId) throws IOException { - account.getGroupStore().deleteGroup(groupId); - avatarStore.deleteGroupAvatar(groupId); + groupHelper.deleteGroup(groupId); } public Pair createGroup( @@ -660,21 +634,9 @@ public class Manager implements Closeable { final SignalServiceDataMessage.Builder messageBuilder, final Message message ) throws AttachmentInvalidException, IOException { messageBuilder.withBody(message.getMessageText()); - if (message.getAttachments() != null) { - var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(message.getAttachments()); - - // Upload attachments here, so we only upload once even for multiple recipients - var messageSender = dependencies.getMessageSender(); - var attachmentPointers = new ArrayList(attachmentStreams.size()); - for (var attachment : attachmentStreams) { - if (attachment.isStream()) { - attachmentPointers.add(messageSender.uploadAttachment(attachment.asStream())); - } else if (attachment.isPointer()) { - attachmentPointers.add(attachment.asPointer()); - } - } - - messageBuilder.withAttachments(attachmentPointers); + final var attachments = message.getAttachments(); + if (attachments != null) { + messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments)); } } @@ -822,51 +784,7 @@ public class Manager implements Closeable { } public void requestAllSyncData() throws IOException { - requestSyncGroups(); - requestSyncContacts(); - requestSyncBlocked(); - requestSyncConfiguration(); - requestSyncKeys(); - } - - private void requestSyncGroups() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncContacts() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncBlocked() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncConfiguration() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); - } - - private void requestSyncKeys() throws IOException { - var r = SignalServiceProtos.SyncMessage.Request.newBuilder() - .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS) - .build(); - var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - sendHelper.sendSyncMessage(message); + syncHelper.requestAllSyncData(); } private byte[] getSenderCertificate() { @@ -984,7 +902,7 @@ public class Manager implements Closeable { if (groupInfo.getAvatar().isPresent()) { var avatar = groupInfo.getAvatar().get(); - downloadGroupAvatar(groupV1.getGroupId(), avatar); + groupHelper.downloadGroupAvatar(groupV1.getGroupId(), avatar); } if (groupInfo.getName().isPresent()) { @@ -1059,13 +977,31 @@ public class Manager implements Closeable { if (!ignoreAttachments) { if (message.getAttachments().isPresent()) { for (var attachment : message.getAttachments().get()) { - downloadAttachment(attachment); + attachmentHelper.downloadAttachment(attachment); } } if (message.getSharedContacts().isPresent()) { for (var contact : message.getSharedContacts().get()) { if (contact.getAvatar().isPresent()) { - downloadAttachment(contact.getAvatar().get().getAttachment()); + attachmentHelper.downloadAttachment(contact.getAvatar().get().getAttachment()); + } + } + } + if (message.getPreviews().isPresent()) { + final var previews = message.getPreviews().get(); + for (var preview : previews) { + if (preview.getImage().isPresent()) { + attachmentHelper.downloadAttachment(preview.getImage().get()); + } + } + } + if (message.getQuote().isPresent()) { + final var quote = message.getQuote().get(); + + for (var quotedAttachment : quote.getAttachments()) { + final var thumbnail = quotedAttachment.getThumbnail(); + if (thumbnail != null) { + attachmentHelper.downloadAttachment(thumbnail); } } } @@ -1082,24 +1018,6 @@ public class Manager implements Closeable { } this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey); } - if (message.getPreviews().isPresent()) { - final var previews = message.getPreviews().get(); - for (var preview : previews) { - if (preview.getImage().isPresent()) { - downloadAttachment(preview.getImage().get()); - } - } - } - if (message.getQuote().isPresent()) { - final var quote = message.getQuote().get(); - - for (var quotedAttachment : quote.getAttachments()) { - final var thumbnail = quotedAttachment.getThumbnail(); - if (thumbnail != null) { - downloadAttachment(thumbnail); - } - } - } if (message.getSticker().isPresent()) { final var messageSticker = message.getSticker().get(); final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId()); @@ -1441,65 +1359,11 @@ public class Manager implements Closeable { // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); } if (syncMessage.getGroups().isPresent()) { - File tmpFile = null; try { - tmpFile = IOUtils.createTempFile(); final var groupsMessage = syncMessage.getGroups().get(); - try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) { - var s = new DeviceGroupsInputStream(attachmentAsStream); - DeviceGroup g; - while (true) { - try { - g = s.read(); - } catch (IOException e) { - logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage()); - continue; - } - if (g == null) { - break; - } - var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId())); - if (syncGroup != null) { - if (g.getName().isPresent()) { - syncGroup.name = g.getName().get(); - } - syncGroup.addMembers(g.getMembers() - .stream() - .map(this::resolveRecipient) - .collect(Collectors.toSet())); - if (!g.isActive()) { - syncGroup.removeMember(account.getSelfRecipientId()); - } else { - // Add ourself to the member set as it's marked as active - syncGroup.addMembers(List.of(account.getSelfRecipientId())); - } - syncGroup.blocked = g.isBlocked(); - if (g.getColor().isPresent()) { - syncGroup.color = g.getColor().get(); - } - - if (g.getAvatar().isPresent()) { - downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); - } - syncGroup.archived = g.isArchived(); - account.getGroupStore().updateGroup(syncGroup); - } - } - } + attachmentHelper.retrieveAttachment(groupsMessage, syncHelper::handleSyncDeviceGroups); } catch (Exception e) { - logger.warn("Failed to handle received sync groups “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } finally { - if (tmpFile != null) { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received groups temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } + logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage()); } } if (syncMessage.getBlockedList().isPresent()) { @@ -1520,75 +1384,12 @@ public class Manager implements Closeable { } } if (syncMessage.getContacts().isPresent()) { - File tmpFile = null; try { - tmpFile = IOUtils.createTempFile(); final var contactsMessage = syncMessage.getContacts().get(); - try (var attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream() - .asPointer(), tmpFile)) { - var s = new DeviceContactsInputStream(attachmentAsStream); - DeviceContact c; - while (true) { - try { - c = s.read(); - } catch (IOException e) { - logger.warn("Sync contacts contained invalid contact, ignoring: {}", - e.getMessage()); - continue; - } - if (c == null) { - break; - } - if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { - account.setProfileKey(c.getProfileKey().get()); - } - final var recipientId = resolveRecipientTrusted(c.getAddress()); - var contact = account.getContactStore().getContact(recipientId); - final var builder = contact == null - ? Contact.newBuilder() - : Contact.newBuilder(contact); - if (c.getName().isPresent()) { - builder.withName(c.getName().get()); - } - if (c.getColor().isPresent()) { - builder.withColor(c.getColor().get()); - } - if (c.getProfileKey().isPresent()) { - account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get()); - } - if (c.getVerified().isPresent()) { - final var verifiedMessage = c.getVerified().get(); - account.getIdentityKeyStore() - .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()), - verifiedMessage.getIdentityKey(), - TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); - } - if (c.getExpirationTimer().isPresent()) { - builder.withMessageExpirationTime(c.getExpirationTimer().get()); - } - builder.withBlocked(c.isBlocked()); - builder.withArchived(c.isArchived()); - account.getContactStore().storeContact(recipientId, builder.build()); - - if (c.getAvatar().isPresent()) { - downloadContactAvatar(c.getAvatar().get(), c.getAddress()); - } - } - } + attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(), + syncHelper::handleSyncDeviceContacts); } catch (Exception e) { - logger.warn("Failed to handle received sync contacts “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } finally { - if (tmpFile != null) { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received contacts temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } + logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); } } if (syncMessage.getVerified().isPresent()) { @@ -1647,227 +1448,20 @@ public class Manager implements Closeable { return actions; } - private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) { - try { - avatarStore.storeContactAvatar(address, outputStream -> retrieveAttachment(avatar, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage()); - } - } - - private void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { - try { - avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); - } - } - public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { return attachmentStore.getAttachmentFile(attachmentId); } - private void downloadAttachment(final SignalServiceAttachment attachment) { - if (!attachment.isPointer()) { - logger.warn("Invalid state, can't store an attachment stream."); - } - - var pointer = attachment.asPointer(); - if (pointer.getPreview().isPresent()) { - final var preview = pointer.getPreview().get(); - try { - attachmentStore.storeAttachmentPreview(pointer.getRemoteId(), - outputStream -> outputStream.write(preview, 0, preview.length)); - } catch (IOException e) { - logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage()); - } - } - - try { - attachmentStore.storeAttachment(pointer.getRemoteId(), - outputStream -> retrieveAttachmentPointer(pointer, outputStream)); - } catch (IOException e) { - logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage()); - } - } - - private void retrieveAttachment( - final SignalServiceAttachment attachment, final OutputStream outputStream - ) throws IOException { - if (attachment.isPointer()) { - var pointer = attachment.asPointer(); - retrieveAttachmentPointer(pointer, outputStream); - } else { - var stream = attachment.asStream(); - IOUtils.copyStream(stream.getInputStream(), outputStream); - } - } - - private void retrieveAttachmentPointer( - SignalServiceAttachmentPointer pointer, OutputStream outputStream - ) throws IOException { - var tmpFile = IOUtils.createTempFile(); - try (var input = retrieveAttachmentAsStream(pointer, tmpFile)) { - IOUtils.copyStream(input, outputStream); - } catch (MissingConfigurationException | InvalidMessageException e) { - throw new IOException(e); - } finally { - try { - Files.delete(tmpFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}", - tmpFile, - e.getMessage()); - } - } - } - - private InputStream retrieveAttachmentAsStream( - SignalServiceAttachmentPointer pointer, File tmpFile - ) throws IOException, InvalidMessageException, MissingConfigurationException { - return dependencies.getMessageReceiver() - .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); - } - void sendGroups() throws IOException { - var groupsFile = IOUtils.createTempFile(); - - try { - try (OutputStream fos = new FileOutputStream(groupsFile)) { - var out = new DeviceGroupsOutputStream(fos); - for (var record : getGroups()) { - if (record instanceof GroupInfoV1) { - var groupInfo = (GroupInfoV1) record; - out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), - Optional.fromNullable(groupInfo.name), - groupInfo.getMembers() - .stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toList()), - groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()), - groupInfo.isMember(account.getSelfRecipientId()), - Optional.of(groupInfo.messageExpirationTime), - Optional.fromNullable(groupInfo.color), - groupInfo.blocked, - Optional.absent(), - groupInfo.archived)); - } - } - } - - if (groupsFile.exists() && groupsFile.length() > 0) { - try (var groupsFileStream = new FileInputStream(groupsFile)) { - var attachmentStream = SignalServiceAttachment.newStreamBuilder() - .withStream(groupsFileStream) - .withContentType("application/octet-stream") - .withLength(groupsFile.length()) - .build(); - - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); - } - } - } finally { - try { - Files.delete(groupsFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage()); - } - } + syncHelper.sendGroups(); } public void sendContacts() throws IOException { - var contactsFile = IOUtils.createTempFile(); - - try { - try (OutputStream fos = new FileOutputStream(contactsFile)) { - var out = new DeviceContactsOutputStream(fos); - for (var contactPair : account.getContactStore().getContacts()) { - final var recipientId = contactPair.first(); - final var contact = contactPair.second(); - final var address = resolveSignalServiceAddress(recipientId); - - var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId); - VerifiedMessage verifiedMessage = null; - if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(address, - currentIdentity.getIdentityKey(), - currentIdentity.getTrustLevel().toVerifiedState(), - currentIdentity.getDateAdded().getTime()); - } - - var profileKey = account.getProfileStore().getProfileKey(recipientId); - out.write(new DeviceContact(address, - Optional.fromNullable(contact.getName()), - createContactAvatarAttachment(address), - Optional.fromNullable(contact.getColor()), - Optional.fromNullable(verifiedMessage), - Optional.fromNullable(profileKey), - contact.isBlocked(), - Optional.of(contact.getMessageExpirationTime()), - Optional.absent(), - contact.isArchived())); - } - - if (account.getProfileKey() != null) { - // Send our own profile key as well - out.write(new DeviceContact(account.getSelfAddress(), - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.absent(), - Optional.of(account.getProfileKey()), - false, - Optional.absent(), - Optional.absent(), - false)); - } - } - - if (contactsFile.exists() && contactsFile.length() > 0) { - try (var contactsFileStream = new FileInputStream(contactsFile)) { - var attachmentStream = SignalServiceAttachment.newStreamBuilder() - .withStream(contactsFileStream) - .withContentType("application/octet-stream") - .withLength(contactsFile.length()) - .build(); - - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, - true))); - } - } - } finally { - try { - Files.delete(contactsFile.toPath()); - } catch (IOException e) { - logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage()); - } - } + syncHelper.sendContacts(); } void sendBlockedList() throws IOException { - var addresses = new ArrayList(); - for (var record : account.getContactStore().getContacts()) { - if (record.second().isBlocked()) { - addresses.add(resolveSignalServiceAddress(record.first())); - } - } - var groupIds = new ArrayList(); - for (var record : getGroups()) { - if (record.isBlocked()) { - groupIds.add(record.getGroupId().serialize()); - } - } - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); - } - - private void sendVerifiedMessage( - SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel - ) throws IOException { - var verifiedMessage = new VerifiedMessage(destination, - identityKey, - trustLevel.toVerifiedState(), - System.currentTimeMillis()); - sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); + syncHelper.sendBlockedList(); } public List> getContacts() { @@ -1974,7 +1568,7 @@ public class Manager implements Closeable { account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); try { var address = account.getRecipientStore().resolveServiceAddress(recipientId); - sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); + syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); } catch (IOException e) { logger.warn("Failed to send verification sync message: {}", e.getMessage()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java new file mode 100644 index 00000000..88a611b9 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -0,0 +1,122 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.AttachmentStore; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.util.AttachmentUtils; +import org.asamk.signal.manager.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +public class AttachmentHelper { + + private final static Logger logger = LoggerFactory.getLogger(AttachmentHelper.class); + + private final SignalDependencies dependencies; + private final AttachmentStore attachmentStore; + + public AttachmentHelper( + final SignalDependencies dependencies, final AttachmentStore attachmentStore + ) { + this.dependencies = dependencies; + this.attachmentStore = attachmentStore; + } + + public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException { + var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); + + // Upload attachments here, so we only upload once even for multiple recipients + var messageSender = dependencies.getMessageSender(); + var attachmentPointers = new ArrayList(attachmentStreams.size()); + for (var attachment : attachmentStreams) { + if (attachment.isStream()) { + attachmentPointers.add(messageSender.uploadAttachment(attachment.asStream())); + } else if (attachment.isPointer()) { + attachmentPointers.add(attachment.asPointer()); + } + } + return attachmentPointers; + } + + public void downloadAttachment(final SignalServiceAttachment attachment) { + if (!attachment.isPointer()) { + logger.warn("Invalid state, can't store an attachment stream."); + } + + var pointer = attachment.asPointer(); + if (pointer.getPreview().isPresent()) { + final var preview = pointer.getPreview().get(); + try { + attachmentStore.storeAttachmentPreview(pointer.getRemoteId(), + outputStream -> outputStream.write(preview, 0, preview.length)); + } catch (IOException e) { + logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage()); + } + } + + try { + attachmentStore.storeAttachment(pointer.getRemoteId(), + outputStream -> this.retrieveAttachment(pointer, outputStream)); + } catch (IOException e) { + logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage()); + } + } + + void retrieveAttachment(SignalServiceAttachment attachment, OutputStream outputStream) throws IOException { + retrieveAttachment(attachment, input -> IOUtils.copyStream(input, outputStream)); + } + + public void retrieveAttachment( + SignalServiceAttachment attachment, AttachmentHandler consumer + ) throws IOException { + if (attachment.isStream()) { + try (var input = attachment.asStream().getInputStream()) { + consumer.handle(input); + } + return; + } + + var tmpFile = IOUtils.createTempFile(); + try (var input = retrieveAttachmentAsStream(attachment.asPointer(), tmpFile)) { + consumer.handle(input); + } finally { + try { + Files.delete(tmpFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}", + tmpFile, + e.getMessage()); + } + } + } + + private InputStream retrieveAttachmentAsStream( + SignalServiceAttachmentPointer pointer, File tmpFile + ) throws IOException { + try { + return dependencies.getMessageReceiver() + .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); + } catch (MissingConfigurationException | InvalidMessageException e) { + throw new IOException(e); + } + } + + @FunctionalInterface + public interface AttachmentHandler { + + void handle(InputStream inputStream) throws IOException; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 9ff3134e..5566d9d7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -35,6 +35,7 @@ import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; @@ -59,6 +60,7 @@ public class GroupHelper { private final SignalAccount account; private final SignalDependencies dependencies; + private final AttachmentHelper attachmentHelper; private final SendHelper sendHelper; private final GroupV2Helper groupV2Helper; private final AvatarStore avatarStore; @@ -68,6 +70,7 @@ public class GroupHelper { public GroupHelper( final SignalAccount account, final SignalDependencies dependencies, + final AttachmentHelper attachmentHelper, final SendHelper sendHelper, final GroupV2Helper groupV2Helper, final AvatarStore avatarStore, @@ -76,6 +79,7 @@ public class GroupHelper { ) { this.account = account; this.dependencies = dependencies; + this.attachmentHelper = attachmentHelper; this.sendHelper = sendHelper; this.groupV2Helper = groupV2Helper; this.avatarStore = avatarStore; @@ -87,6 +91,15 @@ public class GroupHelper { return getGroup(groupId, false); } + public void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { + try { + avatarStore.storeGroupAvatar(groupId, + outputStream -> attachmentHelper.retrieveAttachment(avatar, outputStream)); + } catch (IOException e) { + logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); + } + } + public Optional createGroupAvatarAttachment(GroupIdV1 groupId) throws IOException { final var streamDetails = avatarStore.retrieveGroupAvatar(groupId); if (streamDetails == null) { @@ -282,6 +295,11 @@ public class GroupHelper { } } + public void deleteGroup(GroupId groupId) throws IOException { + account.getGroupStore().deleteGroup(groupId); + avatarStore.deleteGroupAvatar(groupId); + } + public SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, RecipientId recipientId ) throws IOException { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java new file mode 100644 index 00000000..48dc206e --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -0,0 +1,373 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.AvatarStore; +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.asamk.signal.manager.util.AttachmentUtils; +import org.asamk.signal.manager.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; +import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; +import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; +import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; +import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class SyncHelper { + + private final static Logger logger = LoggerFactory.getLogger(SyncHelper.class); + + private final SignalAccount account; + private final AttachmentHelper attachmentHelper; + private final SendHelper sendHelper; + private final GroupHelper groupHelper; + private final AvatarStore avatarStore; + private final SignalServiceAddressResolver addressResolver; + private final RecipientResolver recipientResolver; + + public SyncHelper( + final SignalAccount account, + final AttachmentHelper attachmentHelper, + final SendHelper sendHelper, + final GroupHelper groupHelper, + final AvatarStore avatarStore, + final SignalServiceAddressResolver addressResolver, + final RecipientResolver recipientResolver + ) { + this.account = account; + this.attachmentHelper = attachmentHelper; + this.sendHelper = sendHelper; + this.groupHelper = groupHelper; + this.avatarStore = avatarStore; + this.addressResolver = addressResolver; + this.recipientResolver = recipientResolver; + } + + public void requestAllSyncData() throws IOException { + requestSyncGroups(); + requestSyncContacts(); + requestSyncBlocked(); + requestSyncConfiguration(); + requestSyncKeys(); + } + + public void sendSyncFetchProfileMessage() throws IOException { + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); + } + + public void sendGroups() throws IOException { + var groupsFile = IOUtils.createTempFile(); + + try { + try (OutputStream fos = new FileOutputStream(groupsFile)) { + var out = new DeviceGroupsOutputStream(fos); + for (var record : account.getGroupStore().getGroups()) { + if (record instanceof GroupInfoV1) { + var groupInfo = (GroupInfoV1) record; + out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), + Optional.fromNullable(groupInfo.name), + groupInfo.getMembers() + .stream() + .map(addressResolver::resolveSignalServiceAddress) + .collect(Collectors.toList()), + groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()), + groupInfo.isMember(account.getSelfRecipientId()), + Optional.of(groupInfo.messageExpirationTime), + Optional.fromNullable(groupInfo.color), + groupInfo.blocked, + Optional.absent(), + groupInfo.archived)); + } + } + } + + if (groupsFile.exists() && groupsFile.length() > 0) { + try (var groupsFileStream = new FileInputStream(groupsFile)) { + var attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(groupsFileStream) + .withContentType("application/octet-stream") + .withLength(groupsFile.length()) + .build(); + + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + } + } + } finally { + try { + Files.delete(groupsFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage()); + } + } + } + + public void sendContacts() throws IOException { + var contactsFile = IOUtils.createTempFile(); + + try { + try (OutputStream fos = new FileOutputStream(contactsFile)) { + var out = new DeviceContactsOutputStream(fos); + for (var contactPair : account.getContactStore().getContacts()) { + final var recipientId = contactPair.first(); + final var contact = contactPair.second(); + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + + var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId); + VerifiedMessage verifiedMessage = null; + if (currentIdentity != null) { + verifiedMessage = new VerifiedMessage(address, + currentIdentity.getIdentityKey(), + currentIdentity.getTrustLevel().toVerifiedState(), + currentIdentity.getDateAdded().getTime()); + } + + var profileKey = account.getProfileStore().getProfileKey(recipientId); + out.write(new DeviceContact(address, + Optional.fromNullable(contact.getName()), + createContactAvatarAttachment(address), + Optional.fromNullable(contact.getColor()), + Optional.fromNullable(verifiedMessage), + Optional.fromNullable(profileKey), + contact.isBlocked(), + Optional.of(contact.getMessageExpirationTime()), + Optional.absent(), + contact.isArchived())); + } + + if (account.getProfileKey() != null) { + // Send our own profile key as well + out.write(new DeviceContact(account.getSelfAddress(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.absent(), + Optional.of(account.getProfileKey()), + false, + Optional.absent(), + Optional.absent(), + false)); + } + } + + if (contactsFile.exists() && contactsFile.length() > 0) { + try (var contactsFileStream = new FileInputStream(contactsFile)) { + var attachmentStream = SignalServiceAttachment.newStreamBuilder() + .withStream(contactsFileStream) + .withContentType("application/octet-stream") + .withLength(contactsFile.length()) + .build(); + + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, + true))); + } + } + } finally { + try { + Files.delete(contactsFile.toPath()); + } catch (IOException e) { + logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage()); + } + } + } + + public void sendBlockedList() throws IOException { + var addresses = new ArrayList(); + for (var record : account.getContactStore().getContacts()) { + if (record.second().isBlocked()) { + addresses.add(addressResolver.resolveSignalServiceAddress(record.first())); + } + } + var groupIds = new ArrayList(); + for (var record : account.getGroupStore().getGroups()) { + if (record.isBlocked()) { + groupIds.add(record.getGroupId().serialize()); + } + } + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); + } + + public void sendVerifiedMessage( + SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel + ) throws IOException { + var verifiedMessage = new VerifiedMessage(destination, + identityKey, + trustLevel.toVerifiedState(), + System.currentTimeMillis()); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); + } + + public void handleSyncDeviceGroups(final InputStream input) { + final var s = new DeviceGroupsInputStream(input); + DeviceGroup g; + while (true) { + try { + g = s.read(); + } catch (IOException e) { + logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage()); + continue; + } + if (g == null) { + break; + } + var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId())); + if (syncGroup != null) { + if (g.getName().isPresent()) { + syncGroup.name = g.getName().get(); + } + syncGroup.addMembers(g.getMembers() + .stream() + .map(recipientResolver::resolveRecipient) + .collect(Collectors.toSet())); + if (!g.isActive()) { + syncGroup.removeMember(account.getSelfRecipientId()); + } else { + // Add ourself to the member set as it's marked as active + syncGroup.addMembers(List.of(account.getSelfRecipientId())); + } + syncGroup.blocked = g.isBlocked(); + if (g.getColor().isPresent()) { + syncGroup.color = g.getColor().get(); + } + + if (g.getAvatar().isPresent()) { + groupHelper.downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); + } + syncGroup.archived = g.isArchived(); + account.getGroupStore().updateGroup(syncGroup); + } + } + } + + public void handleSyncDeviceContacts(final InputStream input) { + final var s = new DeviceContactsInputStream(input); + DeviceContact c; + while (true) { + try { + c = s.read(); + } catch (IOException e) { + logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage()); + continue; + } + if (c == null) { + break; + } + if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { + account.setProfileKey(c.getProfileKey().get()); + } + final var recipientId = account.getRecipientStore().resolveRecipientTrusted(c.getAddress()); + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + if (c.getName().isPresent()) { + builder.withName(c.getName().get()); + } + if (c.getColor().isPresent()) { + builder.withColor(c.getColor().get()); + } + if (c.getProfileKey().isPresent()) { + account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get()); + } + if (c.getVerified().isPresent()) { + final var verifiedMessage = c.getVerified().get(); + account.getIdentityKeyStore() + .setIdentityTrustLevel(account.getRecipientStore() + .resolveRecipientTrusted(verifiedMessage.getDestination()), + verifiedMessage.getIdentityKey(), + TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + } + if (c.getExpirationTimer().isPresent()) { + builder.withMessageExpirationTime(c.getExpirationTimer().get()); + } + builder.withBlocked(c.isBlocked()); + builder.withArchived(c.isArchived()); + account.getContactStore().storeContact(recipientId, builder.build()); + + if (c.getAvatar().isPresent()) { + downloadContactAvatar(c.getAvatar().get(), c.getAddress()); + } + } + } + + private void requestSyncGroups() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() + .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS) + .build(); + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); + } + + private void requestSyncContacts() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() + .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS) + .build(); + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); + } + + private void requestSyncBlocked() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() + .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED) + .build(); + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); + } + + private void requestSyncConfiguration() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() + .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION) + .build(); + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); + } + + private void requestSyncKeys() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() + .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS) + .build(); + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); + } + + private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { + final var streamDetails = avatarStore.retrieveContactAvatar(address); + if (streamDetails == null) { + return Optional.absent(); + } + + return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); + } + + private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) { + try { + avatarStore.storeContactAvatar(address, + outputStream -> attachmentHelper.retrieveAttachment(avatar, outputStream)); + } catch (IOException e) { + logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage()); + } + } +} From 8bc6c0abcbdc70b1049df08712cdeff046f48f5e Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 15:25:02 +0200 Subject: [PATCH 0775/2005] Extract ContactHelper and IncomingMessageHandler --- .../asamk/signal/manager/HandleAction.java | 219 ------ .../org/asamk/signal/manager/JobExecutor.java | 17 + .../org/asamk/signal/manager/Manager.java | 624 ++---------------- .../signal/manager/actions/HandleAction.java | 8 + .../manager/actions/RenewSessionAction.java | 36 + .../actions/RetrieveProfileAction.java | 33 + .../manager/actions/SendGroupInfoAction.java | 39 ++ .../actions/SendGroupInfoRequestAction.java | 39 ++ .../manager/actions/SendReceiptAction.java | 36 + .../actions/SendSyncBlockedListAction.java | 20 + .../actions/SendSyncContactsAction.java | 20 + .../manager/actions/SendSyncGroupsAction.java | 20 + .../manager/helper/AttachmentHelper.java | 5 + .../signal/manager/helper/ContactHelper.java | 41 ++ .../signal/manager/helper/GroupHelper.java | 15 + .../helper/IncomingMessageHandler.java | 492 ++++++++++++++ .../signal/manager/helper/SendHelper.java | 10 + .../asamk/signal/manager/jobs/Context.java | 42 +- .../asamk/signal/commands/BlockCommand.java | 7 + .../asamk/signal/commands/UnblockCommand.java | 7 + .../org/asamk/signal/dbus/DbusSignalImpl.java | 4 + 21 files changed, 954 insertions(+), 780 deletions(-) delete mode 100644 lib/src/main/java/org/asamk/signal/manager/HandleAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/JobExecutor.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/HandleAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/RetrieveProfileAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoRequestAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendSyncBlockedListAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendSyncContactsAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendSyncGroupsAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java diff --git a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java b/lib/src/main/java/org/asamk/signal/manager/HandleAction.java deleted file mode 100644 index 8639806f..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/HandleAction.java +++ /dev/null @@ -1,219 +0,0 @@ -package org.asamk.signal.manager; - -import org.asamk.signal.manager.groups.GroupIdV1; -import org.asamk.signal.manager.storage.recipients.RecipientId; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; - -import java.util.List; -import java.util.Objects; - -interface HandleAction { - - void execute(Manager m) throws Throwable; -} - -class SendReceiptAction implements HandleAction { - - private final SignalServiceAddress address; - private final long timestamp; - - public SendReceiptAction(final SignalServiceAddress address, final long timestamp) { - this.address = address; - this.timestamp = timestamp; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendDeliveryReceipt(address, List.of(timestamp)); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final var that = (SendReceiptAction) o; - return timestamp == that.timestamp && address.equals(that.address); - } - - @Override - public int hashCode() { - return Objects.hash(address, timestamp); - } -} - -class SendSyncContactsAction implements HandleAction { - - private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction(); - - private SendSyncContactsAction() { - } - - public static SendSyncContactsAction create() { - return INSTANCE; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendContacts(); - } -} - -class SendSyncGroupsAction implements HandleAction { - - private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction(); - - private SendSyncGroupsAction() { - } - - public static SendSyncGroupsAction create() { - return INSTANCE; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendGroups(); - } -} - -class SendSyncBlockedListAction implements HandleAction { - - private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction(); - - private SendSyncBlockedListAction() { - } - - public static SendSyncBlockedListAction create() { - return INSTANCE; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendBlockedList(); - } -} - -class SendGroupInfoRequestAction implements HandleAction { - - private final SignalServiceAddress address; - private final GroupIdV1 groupId; - - public SendGroupInfoRequestAction(final SignalServiceAddress address, final GroupIdV1 groupId) { - this.address = address; - this.groupId = groupId; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendGroupInfoRequest(groupId, address); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final var that = (SendGroupInfoRequestAction) o; - - if (!address.equals(that.address)) return false; - return groupId.equals(that.groupId); - } - - @Override - public int hashCode() { - var result = address.hashCode(); - result = 31 * result + groupId.hashCode(); - return result; - } -} - -class SendGroupInfoAction implements HandleAction { - - private final SignalServiceAddress address; - private final GroupIdV1 groupId; - - public SendGroupInfoAction(final SignalServiceAddress address, final GroupIdV1 groupId) { - this.address = address; - this.groupId = groupId; - } - - @Override - public void execute(Manager m) throws Throwable { - m.sendGroupInfoMessage(groupId, address); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final var that = (SendGroupInfoAction) o; - - if (!address.equals(that.address)) return false; - return groupId.equals(that.groupId); - } - - @Override - public int hashCode() { - var result = address.hashCode(); - result = 31 * result + groupId.hashCode(); - return result; - } -} - -class RetrieveProfileAction implements HandleAction { - - private final RecipientId recipientId; - - public RetrieveProfileAction(final RecipientId recipientId) { - this.recipientId = recipientId; - } - - @Override - public void execute(Manager m) throws Throwable { - m.refreshRecipientProfile(recipientId); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final RetrieveProfileAction that = (RetrieveProfileAction) o; - - return recipientId.equals(that.recipientId); - } - - @Override - public int hashCode() { - return recipientId.hashCode(); - } -} - -class RenewSessionAction implements HandleAction { - - private final RecipientId recipientId; - - public RenewSessionAction(final RecipientId recipientId) { - this.recipientId = recipientId; - } - - @Override - public void execute(Manager m) throws Throwable { - m.renewSession(recipientId); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - final RenewSessionAction that = (RenewSessionAction) o; - - return recipientId.equals(that.recipientId); - } - - @Override - public int hashCode() { - return recipientId.hashCode(); - } -} diff --git a/lib/src/main/java/org/asamk/signal/manager/JobExecutor.java b/lib/src/main/java/org/asamk/signal/manager/JobExecutor.java new file mode 100644 index 00000000..b86d7da1 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/JobExecutor.java @@ -0,0 +1,17 @@ +package org.asamk.signal.manager; + +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.jobs.Job; + +public class JobExecutor { + + private final Context context; + + public JobExecutor(final Context context) { + this.context = context; + } + + public void enqueueJob(Job job) { + job.run(context); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 936f625c..0d06dfba 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -16,6 +16,7 @@ */ package org.asamk.signal.manager; +import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -26,29 +27,26 @@ import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; -import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; -import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.helper.AttachmentHelper; +import org.asamk.signal.manager.helper.ContactHelper; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; +import org.asamk.signal.manager.helper.IncomingMessageHandler; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.jobs.Context; -import org.asamk.signal.manager.jobs.Job; -import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; -import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.messageCache.CachedMessage; @@ -60,10 +58,7 @@ import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; -import org.signal.libsignal.metadata.ProtocolInvalidMessageException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; -import org.signal.zkgroup.InvalidInputException; -import org.signal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; @@ -85,10 +80,8 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemo import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; -import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; @@ -109,7 +102,6 @@ import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SignatureException; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; @@ -146,19 +138,10 @@ public class Manager implements Closeable { private final SyncHelper syncHelper; private final AttachmentHelper attachmentHelper; private final GroupHelper groupHelper; + private final ContactHelper contactHelper; + private final IncomingMessageHandler incomingMessageHandler; - private final AvatarStore avatarStore; - private final AttachmentStore attachmentStore; - private final StickerPackStore stickerPackStore; - private final SignalSessionLock sessionLock = new SignalSessionLock() { - private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); - - @Override - public Lock acquire() { - LEGACY_LOCK.lock(); - return LEGACY_LOCK::unlock; - } - }; + private final Context context; Manager( SignalAccount account, @@ -173,6 +156,15 @@ public class Manager implements Closeable { account.getUsername(), account.getPassword(), account.getDeviceId()); + final var sessionLock = new SignalSessionLock() { + private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); + + @Override + public Lock acquire() { + LEGACY_LOCK.lock(); + return LEGACY_LOCK::unlock; + } + }; this.dependencies = new SignalDependencies(account.getSelfAddress(), serviceEnvironmentConfig, userAgent, @@ -180,9 +172,9 @@ public class Manager implements Closeable { account.getSignalProtocolStore(), executor, sessionLock); - this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); - this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); - this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); + final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); @@ -220,6 +212,7 @@ public class Manager implements Closeable { avatarStore, this::resolveSignalServiceAddress, this::resolveRecipient); + this.contactHelper = new ContactHelper(account); this.syncHelper = new SyncHelper(account, attachmentHelper, sendHelper, @@ -227,16 +220,31 @@ public class Manager implements Closeable { avatarStore, this::resolveSignalServiceAddress, this::resolveRecipient); + + this.context = new Context(account, + dependencies.getAccountManager(), + dependencies.getMessageReceiver(), + stickerPackStore, + sendHelper, + groupHelper, + syncHelper, + profileHelper); + var jobExecutor = new JobExecutor(context); + + this.incomingMessageHandler = new IncomingMessageHandler(account, + dependencies, + this::resolveRecipient, + groupHelper, + contactHelper, + attachmentHelper, + syncHelper, + jobExecutor); } public String getUsername() { return account.getUsername(); } - private SignalServiceAddress getSelfAddress() { - return account.getSelfAddress(); - } - public RecipientId getSelfRecipientId() { return account.getSelfRecipientId(); } @@ -326,15 +334,15 @@ public class Manager implements Closeable { } })); - // Note "contactDetails" has no optionals. It only gives us info on users who are registered - var contactDetails = getRegisteredUsers(canonicalizedNumbers.values() + // Note "registeredUsers" has no optionals. It only gives us info on users who are registered + var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values() .stream() .filter(s -> !s.isEmpty()) .collect(Collectors.toSet())); return numbers.stream().collect(Collectors.toMap(n -> n, n -> { final var number = canonicalizedNumbers.get(n); - final var uuid = contactDetails.get(number); + final var uuid = registeredUsers.get(number); return new Pair<>(number.isEmpty() ? null : number, uuid); })); } @@ -475,10 +483,6 @@ public class Manager implements Closeable { return profileHelper.getRecipientProfile(recipientId); } - public void refreshRecipientProfile(RecipientId recipientId) { - profileHelper.refreshRecipientProfile(recipientId); - } - public List getGroups() { return account.getGroupStore().getGroups(); } @@ -578,20 +582,6 @@ public class Manager implements Closeable { } } - SendGroupMessageResults sendGroupInfoMessage( - GroupIdV1 groupId, SignalServiceAddress recipient - ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { - final var recipientId = resolveRecipient(recipient); - return groupHelper.sendGroupInfoMessage(groupId, recipientId); - } - - SendGroupMessageResults sendGroupInfoRequest( - GroupIdV1 groupId, SignalServiceAddress recipient - ) throws IOException { - final var recipientId = resolveRecipient(recipient); - return groupHelper.sendGroupInfoRequest(groupId, recipientId); - } - public void sendReadReceipt( RecipientIdentifier.Single sender, List messageIds ) throws IOException, UntrustedIdentityException { @@ -612,16 +602,6 @@ public class Manager implements Closeable { sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); } - void sendDeliveryReceipt( - SignalServiceAddress remoteAddress, List messageIds - ) throws IOException, UntrustedIdentityException { - var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, - messageIds, - System.currentTimeMillis()); - - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); - } - public SendMessageResults sendMessage( Message message, Set recipients ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { @@ -674,56 +654,38 @@ public class Manager implements Closeable { throw new AssertionError(e); } finally { for (var recipient : recipients) { - final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); - handleEndSession(recipientId); + final var recipientId = resolveRecipient(recipient); + account.getSessionStore().deleteAllSessions(recipientId); } } } - void renewSession(RecipientId recipientId) throws IOException { - account.getSessionStore().archiveSessions(recipientId); - if (!recipientId.equals(getSelfRecipientId())) { - sendHelper.sendNullMessage(recipientId); - } - } - public void setContactName( RecipientIdentifier.Single recipient, String name ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - final var recipientId = resolveRecipient(recipient); - var contact = account.getContactStore().getContact(recipientId); - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - account.getContactStore().storeContact(recipientId, builder.withName(name).build()); + contactHelper.setContactName(resolveRecipient(recipient), name); } public void setContactBlocked( RecipientIdentifier.Single recipient, boolean blocked - ) throws NotMasterDeviceException { + ) throws NotMasterDeviceException, IOException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } - setContactBlocked(resolveRecipient(recipient), blocked); + contactHelper.setContactBlocked(resolveRecipient(recipient), blocked); + // TODO cycle our profile key + syncHelper.sendBlockedList(); } - private void setContactBlocked(RecipientId recipientId, boolean blocked) { - var contact = account.getContactStore().getContact(recipientId); - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + public void setGroupBlocked( + final GroupId groupId, final boolean blocked + ) throws GroupNotFoundException, IOException { + groupHelper.setGroupBlocked(groupId, blocked); // TODO cycle our profile key - account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); - } - - public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { - var group = getGroup(groupId); - if (group == null) { - throw new GroupNotFoundException(groupId); - } - - group.setBlocked(blocked); - // TODO cycle our profile key - account.getGroupStore().updateGroup(group); + syncHelper.sendBlockedList(); } /** @@ -733,7 +695,7 @@ public class Manager implements Closeable { RecipientIdentifier.Single recipient, int messageExpirationTimer ) throws IOException { var recipientId = resolveRecipient(recipient); - setExpirationTimer(recipientId, messageExpirationTimer); + contactHelper.setExpirationTimer(recipientId, messageExpirationTimer); final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); try { sendMessage(messageBuilder, Set.of(recipient)); @@ -742,16 +704,6 @@ public class Manager implements Closeable { } } - private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { - var contact = account.getContactStore().getContact(recipientId); - if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { - return; - } - final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); - account.getContactStore() - .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); - } - /** * Upload the sticker pack from path. * @@ -875,162 +827,6 @@ public class Manager implements Closeable { sendTypingMessage(action.toSignalService(), recipients); } - private void handleEndSession(RecipientId recipientId) { - account.getSessionStore().deleteAllSessions(recipientId); - } - - private List handleSignalServiceDataMessage( - SignalServiceDataMessage message, - boolean isSync, - SignalServiceAddress source, - SignalServiceAddress destination, - boolean ignoreAttachments - ) { - var actions = new ArrayList(); - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - var groupInfo = message.getGroupContext().get().getGroupV1().get(); - var groupId = GroupId.v1(groupInfo.getGroupId()); - var group = getGroup(groupId); - if (group == null || group instanceof GroupInfoV1) { - var groupV1 = (GroupInfoV1) group; - switch (groupInfo.getType()) { - case UPDATE: { - if (groupV1 == null) { - groupV1 = new GroupInfoV1(groupId); - } - - if (groupInfo.getAvatar().isPresent()) { - var avatar = groupInfo.getAvatar().get(); - groupHelper.downloadGroupAvatar(groupV1.getGroupId(), avatar); - } - - if (groupInfo.getName().isPresent()) { - groupV1.name = groupInfo.getName().get(); - } - - if (groupInfo.getMembers().isPresent()) { - groupV1.addMembers(groupInfo.getMembers() - .get() - .stream() - .map(this::resolveRecipient) - .collect(Collectors.toSet())); - } - - account.getGroupStore().updateGroup(groupV1); - break; - } - case DELIVER: - if (groupV1 == null && !isSync) { - actions.add(new SendGroupInfoRequestAction(source, groupId)); - } - break; - case QUIT: { - if (groupV1 != null) { - groupV1.removeMember(resolveRecipient(source)); - account.getGroupStore().updateGroup(groupV1); - } - break; - } - case REQUEST_INFO: - if (groupV1 != null && !isSync) { - actions.add(new SendGroupInfoAction(source, groupV1.getGroupId())); - } - break; - } - } else { - // Received a group v1 message for a v2 group - } - } - if (message.getGroupContext().get().getGroupV2().isPresent()) { - final var groupContext = message.getGroupContext().get().getGroupV2().get(); - final var groupMasterKey = groupContext.getMasterKey(); - - groupHelper.getOrMigrateGroup(groupMasterKey, - groupContext.getRevision(), - groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); - } - } - - final var conversationPartnerAddress = isSync ? destination : source; - if (conversationPartnerAddress != null && message.isEndSession()) { - handleEndSession(resolveRecipient(conversationPartnerAddress)); - } - if (message.isExpirationUpdate() || message.getBody().isPresent()) { - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - var groupInfo = message.getGroupContext().get().getGroupV1().get(); - var group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId())); - if (group != null) { - if (group.messageExpirationTime != message.getExpiresInSeconds()) { - group.messageExpirationTime = message.getExpiresInSeconds(); - account.getGroupStore().updateGroup(group); - } - } - } else if (message.getGroupContext().get().getGroupV2().isPresent()) { - // disappearing message timer already stored in the DecryptedGroup - } - } else if (conversationPartnerAddress != null) { - setExpirationTimer(resolveRecipient(conversationPartnerAddress), message.getExpiresInSeconds()); - } - } - if (!ignoreAttachments) { - if (message.getAttachments().isPresent()) { - for (var attachment : message.getAttachments().get()) { - attachmentHelper.downloadAttachment(attachment); - } - } - if (message.getSharedContacts().isPresent()) { - for (var contact : message.getSharedContacts().get()) { - if (contact.getAvatar().isPresent()) { - attachmentHelper.downloadAttachment(contact.getAvatar().get().getAttachment()); - } - } - } - if (message.getPreviews().isPresent()) { - final var previews = message.getPreviews().get(); - for (var preview : previews) { - if (preview.getImage().isPresent()) { - attachmentHelper.downloadAttachment(preview.getImage().get()); - } - } - } - if (message.getQuote().isPresent()) { - final var quote = message.getQuote().get(); - - for (var quotedAttachment : quote.getAttachments()) { - final var thumbnail = quotedAttachment.getThumbnail(); - if (thumbnail != null) { - attachmentHelper.downloadAttachment(thumbnail); - } - } - } - } - if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { - final ProfileKey profileKey; - try { - profileKey = new ProfileKey(message.getProfileKey().get()); - } catch (InvalidInputException e) { - throw new AssertionError(e); - } - if (source.matches(account.getSelfAddress())) { - this.account.setProfileKey(profileKey); - } - this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey); - } - if (message.getSticker().isPresent()) { - final var messageSticker = message.getSticker().get(); - final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId()); - var sticker = account.getStickerStore().getSticker(stickerPackId); - if (sticker == null) { - sticker = new Sticker(stickerPackId, messageSticker.getPackKey()); - account.getStickerStore().updateSticker(sticker); - } - enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); - } - return actions; - } - private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { Set queuedActions = new HashSet<>(); for (var cachedMessage : account.getMessageCache().getCachedMessages()) { @@ -1070,7 +866,7 @@ public class Manager implements Closeable { cachedMessage.delete(); return null; } - actions = handleMessage(envelope, content, ignoreAttachments); + actions = incomingMessageHandler.handleMessage(envelope, content, ignoreAttachments); } handler.handleMessage(envelope, content, null); cachedMessage.delete(); @@ -1095,8 +891,6 @@ public class Manager implements Closeable { while (!Thread.interrupted()) { SignalServiceEnvelope envelope; - SignalServiceContent content = null; - Exception exception = null; final CachedMessage[] cachedMessage = {null}; account.setLastReceiveTimestamp(System.currentTimeMillis()); logger.debug("Checking for new message from server"); @@ -1137,58 +931,17 @@ public class Manager implements Closeable { continue; } - if (envelope.hasSource()) { - // Store uuid if we don't have it already - // address/uuid in envelope is sent by server - resolveRecipientTrusted(envelope.getSourceAddress()); - } - if (!envelope.isReceipt()) { - try { - content = dependencies.getCipher().decrypt(envelope); - } catch (Exception e) { - exception = e; - } - if (!envelope.hasSource() && content != null) { - // Store uuid if we don't have it already - // address/uuid is validated by unidentified sender certificate - resolveRecipientTrusted(content.getSender()); - } - var actions = handleMessage(envelope, content, ignoreAttachments); - if (exception instanceof ProtocolInvalidMessageException) { - final var sender = resolveRecipient(((ProtocolInvalidMessageException) exception).getSender()); - logger.debug("Received invalid message, queuing renew session action."); - actions.add(new RenewSessionAction(sender)); - } - if (hasCaughtUpWithOldMessages) { - for (var action : actions) { - try { - action.execute(this); - } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - } - logger.warn("Message action failed.", e); - } - } - } else { - queuedActions.addAll(actions); - } - } - final var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content); - if (isMessageBlocked(envelope, content)) { - logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); - } else if (notAllowedToSendToGroup) { - logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", - (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), - envelope.getTimestamp()); - } else { - handler.handleMessage(envelope, content, exception); + final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); + queuedActions.addAll(result.first()); + final var exception = result.second(); + + if (hasCaughtUpWithOldMessages) { + handleQueuedActions(queuedActions); } if (cachedMessage[0] != null) { if (exception instanceof ProtocolUntrustedIdentityException) { final var identifier = ((ProtocolUntrustedIdentityException) exception).getSender(); final var recipientId = resolveRecipient(identifier); - queuedActions.add(new RetrieveProfileAction(recipientId)); if (!envelope.hasSource()) { try { cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); @@ -1205,10 +958,10 @@ public class Manager implements Closeable { handleQueuedActions(queuedActions); } - private void handleQueuedActions(final Set queuedActions) { + private void handleQueuedActions(final Collection queuedActions) { for (var action : queuedActions) { try { - action.execute(this); + action.execute(context); } catch (Throwable e) { if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { Thread.currentThread().interrupt(); @@ -1218,252 +971,19 @@ public class Manager implements Closeable { } } - private boolean isMessageBlocked( - SignalServiceEnvelope envelope, SignalServiceContent content - ) { - SignalServiceAddress source; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - source = envelope.getSourceAddress(); - } else if (content != null) { - source = content.getSender(); - } else { - return false; - } - final var recipientId = resolveRecipient(source); - if (isContactBlocked(recipientId)) { - return true; - } - - if (content != null && content.getDataMessage().isPresent()) { - var message = content.getDataMessage().get(); - if (message.getGroupContext().isPresent()) { - var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); - var group = getGroup(groupId); - if (group != null && group.isBlocked()) { - return true; - } - } - } - return false; - } - public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { final var recipientId = resolveRecipient(recipient); - return isContactBlocked(recipientId); - } - - private boolean isContactBlocked(final RecipientId recipientId) { - var sourceContact = account.getContactStore().getContact(recipientId); - return sourceContact != null && sourceContact.isBlocked(); - } - - private boolean isNotAllowedToSendToGroup( - SignalServiceEnvelope envelope, SignalServiceContent content - ) { - SignalServiceAddress source; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - source = envelope.getSourceAddress(); - } else if (content != null) { - source = content.getSender(); - } else { - return false; - } - - if (content == null || !content.getDataMessage().isPresent()) { - return false; - } - - var message = content.getDataMessage().get(); - if (!message.getGroupContext().isPresent()) { - return false; - } - - if (message.getGroupContext().get().getGroupV1().isPresent()) { - var groupInfo = message.getGroupContext().get().getGroupV1().get(); - if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) { - return false; - } - } - - var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); - var group = getGroup(groupId); - if (group == null) { - return false; - } - - final var recipientId = resolveRecipient(source); - if (!group.isMember(recipientId)) { - return true; - } - - if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { - return message.getBody().isPresent() - || message.getAttachments().isPresent() - || message.getQuote() - .isPresent() - || message.getPreviews().isPresent() - || message.getMentions().isPresent() - || message.getSticker().isPresent(); - } - return false; - } - - private List handleMessage( - SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments - ) { - var actions = new ArrayList(); - if (content != null) { - final SignalServiceAddress sender; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - sender = envelope.getSourceAddress(); - } else { - sender = content.getSender(); - } - - if (content.getDataMessage().isPresent()) { - var message = content.getDataMessage().get(); - - if (content.isNeedsReceipt()) { - actions.add(new SendReceiptAction(sender, message.getTimestamp())); - } - - actions.addAll(handleSignalServiceDataMessage(message, - false, - sender, - account.getSelfAddress(), - ignoreAttachments)); - } - if (content.getSyncMessage().isPresent()) { - account.setMultiDevice(true); - var syncMessage = content.getSyncMessage().get(); - if (syncMessage.getSent().isPresent()) { - var message = syncMessage.getSent().get(); - final var destination = message.getDestination().orNull(); - actions.addAll(handleSignalServiceDataMessage(message.getMessage(), - true, - sender, - destination, - ignoreAttachments)); - } - if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { - var rm = syncMessage.getRequest().get(); - if (rm.isContactsRequest()) { - actions.add(SendSyncContactsAction.create()); - } - if (rm.isGroupsRequest()) { - actions.add(SendSyncGroupsAction.create()); - } - if (rm.isBlockedListRequest()) { - actions.add(SendSyncBlockedListAction.create()); - } - // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); - } - if (syncMessage.getGroups().isPresent()) { - try { - final var groupsMessage = syncMessage.getGroups().get(); - attachmentHelper.retrieveAttachment(groupsMessage, syncHelper::handleSyncDeviceGroups); - } catch (Exception e) { - logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage()); - } - } - if (syncMessage.getBlockedList().isPresent()) { - final var blockedListMessage = syncMessage.getBlockedList().get(); - for (var address : blockedListMessage.getAddresses()) { - setContactBlocked(resolveRecipient(address), true); - } - for (var groupId : blockedListMessage.getGroupIds() - .stream() - .map(GroupId::unknownVersion) - .collect(Collectors.toSet())) { - try { - setGroupBlocked(groupId, true); - } catch (GroupNotFoundException e) { - logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", - groupId.toBase64()); - } - } - } - if (syncMessage.getContacts().isPresent()) { - try { - final var contactsMessage = syncMessage.getContacts().get(); - attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(), - syncHelper::handleSyncDeviceContacts); - } catch (Exception e) { - logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); - } - } - if (syncMessage.getVerified().isPresent()) { - final var verifiedMessage = syncMessage.getVerified().get(); - account.getIdentityKeyStore() - .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()), - verifiedMessage.getIdentityKey(), - TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); - } - if (syncMessage.getStickerPackOperations().isPresent()) { - final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); - for (var m : stickerPackOperationMessages) { - if (!m.getPackId().isPresent()) { - continue; - } - final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); - final var installed = !m.getType().isPresent() - || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; - - var sticker = account.getStickerStore().getSticker(stickerPackId); - if (m.getPackKey().isPresent()) { - if (sticker == null) { - sticker = new Sticker(stickerPackId, m.getPackKey().get()); - } - if (installed) { - enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); - } - } - - if (sticker != null) { - sticker.setInstalled(installed); - account.getStickerStore().updateSticker(sticker); - } - } - } - if (syncMessage.getFetchType().isPresent()) { - switch (syncMessage.getFetchType().get()) { - case LOCAL_PROFILE: - actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); - case STORAGE_MANIFEST: - // TODO - } - } - if (syncMessage.getKeys().isPresent()) { - final var keysMessage = syncMessage.getKeys().get(); - if (keysMessage.getStorageService().isPresent()) { - final var storageKey = keysMessage.getStorageService().get(); - account.setStorageKey(storageKey); - } - } - if (syncMessage.getConfiguration().isPresent()) { - // TODO - } - } - } - return actions; + return contactHelper.isContactBlocked(recipientId); } public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { - return attachmentStore.getAttachmentFile(attachmentId); - } - - void sendGroups() throws IOException { - syncHelper.sendGroups(); + return attachmentHelper.getAttachmentFile(attachmentId); } public void sendContacts() throws IOException { syncHelper.sendContacts(); } - void sendBlockedList() throws IOException { - syncHelper.sendBlockedList(); - } - public List> getContacts() { return account.getContactStore().getContacts(); } @@ -1471,7 +991,7 @@ public class Manager implements Closeable { public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { final var recipientId = resolveRecipient(recipientIdentifier); - final var contact = account.getRecipientStore().getContact(recipientId); + final var contact = account.getContactStore().getContact(recipientId); if (contact != null && !Util.isEmpty(contact.getName())) { return contact.getName(); } @@ -1587,7 +1107,7 @@ public class Manager implements Closeable { } } else { // Retrieve profile to get the current identity key from the server - refreshRecipientProfile(recipientId); + profileHelper.refreshRecipientProfile(recipientId); } } @@ -1660,20 +1180,12 @@ public class Manager implements Closeable { return account.getRecipientStore().resolveRecipientTrusted(address); } - private void enqueueJob(Job job) { - var context = new Context(account, - dependencies.getAccountManager(), - dependencies.getMessageReceiver(), - stickerPackStore); - job.run(context); - } - @Override public void close() throws IOException { close(true); } - void close(boolean closeAccount) throws IOException { + private void close(boolean closeAccount) throws IOException { executor.shutdown(); dependencies.getSignalWebSocket().disconnect(); diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/HandleAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/HandleAction.java new file mode 100644 index 00000000..cfe13bce --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/HandleAction.java @@ -0,0 +1,8 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public interface HandleAction { + + void execute(Context context) throws Throwable; +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java new file mode 100644 index 00000000..07194cd0 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/RenewSessionAction.java @@ -0,0 +1,36 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class RenewSessionAction implements HandleAction { + + private final RecipientId recipientId; + + public RenewSessionAction(final RecipientId recipientId) { + this.recipientId = recipientId; + } + + @Override + public void execute(Context context) throws Throwable { + context.getAccount().getSessionStore().archiveSessions(recipientId); + if (!recipientId.equals(context.getAccount().getSelfRecipientId())) { + context.getSendHelper().sendNullMessage(recipientId); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final RenewSessionAction that = (RenewSessionAction) o; + + return recipientId.equals(that.recipientId); + } + + @Override + public int hashCode() { + return recipientId.hashCode(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveProfileAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveProfileAction.java new file mode 100644 index 00000000..329e7cd2 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveProfileAction.java @@ -0,0 +1,33 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class RetrieveProfileAction implements HandleAction { + + private final RecipientId recipientId; + + public RetrieveProfileAction(final RecipientId recipientId) { + this.recipientId = recipientId; + } + + @Override + public void execute(Context context) throws Throwable { + context.getProfileHelper().refreshRecipientProfile(recipientId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final RetrieveProfileAction that = (RetrieveProfileAction) o; + + return recipientId.equals(that.recipientId); + } + + @Override + public int hashCode() { + return recipientId.hashCode(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoAction.java new file mode 100644 index 00000000..6f66ceeb --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoAction.java @@ -0,0 +1,39 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class SendGroupInfoAction implements HandleAction { + + private final RecipientId recipientId; + private final GroupIdV1 groupId; + + public SendGroupInfoAction(final RecipientId recipientId, final GroupIdV1 groupId) { + this.recipientId = recipientId; + this.groupId = groupId; + } + + @Override + public void execute(Context context) throws Throwable { + context.getGroupHelper().sendGroupInfoMessage(groupId, recipientId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final var that = (SendGroupInfoAction) o; + + if (!recipientId.equals(that.recipientId)) return false; + return groupId.equals(that.groupId); + } + + @Override + public int hashCode() { + var result = recipientId.hashCode(); + result = 31 * result + groupId.hashCode(); + return result; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoRequestAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoRequestAction.java new file mode 100644 index 00000000..4ded0a31 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendGroupInfoRequestAction.java @@ -0,0 +1,39 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.groups.GroupIdV1; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class SendGroupInfoRequestAction implements HandleAction { + + private final RecipientId recipientId; + private final GroupIdV1 groupId; + + public SendGroupInfoRequestAction(final RecipientId recipientId, final GroupIdV1 groupId) { + this.recipientId = recipientId; + this.groupId = groupId; + } + + @Override + public void execute(Context context) throws Throwable { + context.getGroupHelper().sendGroupInfoRequest(groupId, recipientId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final var that = (SendGroupInfoRequestAction) o; + + if (!recipientId.equals(that.recipientId)) return false; + return groupId.equals(that.groupId); + } + + @Override + public int hashCode() { + var result = recipientId.hashCode(); + result = 31 * result + groupId.hashCode(); + return result; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java new file mode 100644 index 00000000..8341304c --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendReceiptAction.java @@ -0,0 +1,36 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +import java.util.List; +import java.util.Objects; + +public class SendReceiptAction implements HandleAction { + + private final RecipientId recipientId; + private final long timestamp; + + public SendReceiptAction(final RecipientId recipientId, final long timestamp) { + this.recipientId = recipientId; + this.timestamp = timestamp; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSendHelper().sendDeliveryReceipt(recipientId, List.of(timestamp)); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final var that = (SendReceiptAction) o; + return timestamp == that.timestamp && recipientId.equals(that.recipientId); + } + + @Override + public int hashCode() { + return Objects.hash(recipientId, timestamp); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncBlockedListAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncBlockedListAction.java new file mode 100644 index 00000000..4aea9e69 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncBlockedListAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class SendSyncBlockedListAction implements HandleAction { + + private static final SendSyncBlockedListAction INSTANCE = new SendSyncBlockedListAction(); + + private SendSyncBlockedListAction() { + } + + public static SendSyncBlockedListAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSyncHelper().sendBlockedList(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncContactsAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncContactsAction.java new file mode 100644 index 00000000..f590e982 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncContactsAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class SendSyncContactsAction implements HandleAction { + + private static final SendSyncContactsAction INSTANCE = new SendSyncContactsAction(); + + private SendSyncContactsAction() { + } + + public static SendSyncContactsAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSyncHelper().sendContacts(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncGroupsAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncGroupsAction.java new file mode 100644 index 00000000..3f18732f --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncGroupsAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class SendSyncGroupsAction implements HandleAction { + + private static final SendSyncGroupsAction INSTANCE = new SendSyncGroupsAction(); + + private SendSyncGroupsAction() { + } + + public static SendSyncGroupsAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSyncHelper().sendGroups(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index 88a611b9..d3931955 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; import java.io.File; @@ -35,6 +36,10 @@ public class AttachmentHelper { this.attachmentStore = attachmentStore; } + public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { + return attachmentStore.getAttachmentFile(attachmentId); + } + public List uploadAttachments(final List attachments) throws AttachmentInvalidException, IOException { var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java new file mode 100644 index 00000000..71b2ded8 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ContactHelper.java @@ -0,0 +1,41 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public class ContactHelper { + + private final SignalAccount account; + + public ContactHelper(final SignalAccount account) { + this.account = account; + } + + public boolean isContactBlocked(final RecipientId recipientId) { + var sourceContact = account.getContactStore().getContact(recipientId); + return sourceContact != null && sourceContact.isBlocked(); + } + + public void setContactName(final RecipientId recipientId, final String name) { + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore().storeContact(recipientId, builder.withName(name).build()); + } + + public void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { + var contact = account.getContactStore().getContact(recipientId); + if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { + return; + } + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore() + .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); + } + + public void setContactBlocked(RecipientId recipientId, boolean blocked) { + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 5566d9d7..3ddd6edd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -91,6 +91,11 @@ public class GroupHelper { return getGroup(groupId, false); } + public boolean isGroupBlocked(final GroupId groupId) { + var group = getGroup(groupId); + return group != null && group.isBlocked(); + } + public void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { try { avatarStore.storeGroupAvatar(groupId, @@ -300,6 +305,16 @@ public class GroupHelper { avatarStore.deleteGroupAvatar(groupId); } + public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { + var group = getGroup(groupId); + if (group == null) { + throw new GroupNotFoundException(groupId); + } + + group.setBlocked(blocked); + account.getGroupStore().updateGroup(group); + } + public SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, RecipientId recipientId ) throws IOException { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java new file mode 100644 index 00000000..369d3205 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -0,0 +1,492 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.JobExecutor; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.actions.HandleAction; +import org.asamk.signal.manager.actions.RenewSessionAction; +import org.asamk.signal.manager.actions.RetrieveProfileAction; +import org.asamk.signal.manager.actions.SendGroupInfoAction; +import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; +import org.asamk.signal.manager.actions.SendReceiptAction; +import org.asamk.signal.manager.actions.SendSyncBlockedListAction; +import org.asamk.signal.manager.actions.SendSyncContactsAction; +import org.asamk.signal.manager.actions.SendSyncGroupsAction; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.asamk.signal.manager.storage.stickers.Sticker; +import org.asamk.signal.manager.storage.stickers.StickerPackId; +import org.signal.libsignal.metadata.ProtocolInvalidMessageException; +import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.profiles.ProfileKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.signalservice.api.messages.SignalServiceContent; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public final class IncomingMessageHandler { + + private final static Logger logger = LoggerFactory.getLogger(IncomingMessageHandler.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final RecipientResolver recipientResolver; + private final GroupHelper groupHelper; + private final ContactHelper contactHelper; + private final AttachmentHelper attachmentHelper; + private final SyncHelper syncHelper; + private final JobExecutor jobExecutor; + + public IncomingMessageHandler( + final SignalAccount account, + final SignalDependencies dependencies, + final RecipientResolver recipientResolver, + final GroupHelper groupHelper, + final ContactHelper contactHelper, + final AttachmentHelper attachmentHelper, + final SyncHelper syncHelper, + final JobExecutor jobExecutor + ) { + this.account = account; + this.dependencies = dependencies; + this.recipientResolver = recipientResolver; + this.groupHelper = groupHelper; + this.contactHelper = contactHelper; + this.attachmentHelper = attachmentHelper; + this.syncHelper = syncHelper; + this.jobExecutor = jobExecutor; + } + + public Pair, Exception> handleEnvelope( + final SignalServiceEnvelope envelope, + final boolean ignoreAttachments, + final Manager.ReceiveMessageHandler handler + ) { + final var actions = new ArrayList(); + if (envelope.hasSource()) { + // Store uuid if we don't have it already + // address/uuid in envelope is sent by server + account.getRecipientStore().resolveRecipientTrusted(envelope.getSourceAddress()); + } + SignalServiceContent content = null; + Exception exception = null; + if (!envelope.isReceipt()) { + try { + content = dependencies.getCipher().decrypt(envelope); + } catch (ProtocolUntrustedIdentityException e) { + final var recipientId = account.getRecipientStore().resolveRecipient(e.getSender()); + actions.add(new RetrieveProfileAction(recipientId)); + } catch (ProtocolInvalidMessageException e) { + final var sender = account.getRecipientStore().resolveRecipient(e.getSender()); + logger.debug("Received invalid message, queuing renew session action."); + actions.add(new RenewSessionAction(sender)); + exception = e; + } catch (Exception e) { + exception = e; + } + + if (!envelope.hasSource() && content != null) { + // Store uuid if we don't have it already + // address/uuid is validated by unidentified sender certificate + account.getRecipientStore().resolveRecipientTrusted(content.getSender()); + } + + actions.addAll(handleMessage(envelope, content, ignoreAttachments)); + } + + if (isMessageBlocked(envelope, content)) { + logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); + } else if (isNotAllowedToSendToGroup(envelope, content)) { + logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", + (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), + envelope.getTimestamp()); + } else { + handler.handleMessage(envelope, content, exception); + } + return new Pair<>(actions, exception); + } + + public List handleMessage( + SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments + ) { + var actions = new ArrayList(); + if (content != null) { + final RecipientId sender; + if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { + sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); + } else { + sender = recipientResolver.resolveRecipient(content.getSender()); + } + + if (content.getDataMessage().isPresent()) { + var message = content.getDataMessage().get(); + + if (content.isNeedsReceipt()) { + actions.add(new SendReceiptAction(sender, message.getTimestamp())); + } + + actions.addAll(handleSignalServiceDataMessage(message, + false, + sender, + account.getSelfRecipientId(), + ignoreAttachments)); + } + if (content.getSyncMessage().isPresent()) { + account.setMultiDevice(true); + var syncMessage = content.getSyncMessage().get(); + if (syncMessage.getSent().isPresent()) { + var message = syncMessage.getSent().get(); + final var destination = message.getDestination().orNull(); + actions.addAll(handleSignalServiceDataMessage(message.getMessage(), + true, + sender, + destination == null ? null : recipientResolver.resolveRecipient(destination), + ignoreAttachments)); + } + if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { + var rm = syncMessage.getRequest().get(); + if (rm.isContactsRequest()) { + actions.add(SendSyncContactsAction.create()); + } + if (rm.isGroupsRequest()) { + actions.add(SendSyncGroupsAction.create()); + } + if (rm.isBlockedListRequest()) { + actions.add(SendSyncBlockedListAction.create()); + } + // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); + } + if (syncMessage.getGroups().isPresent()) { + try { + final var groupsMessage = syncMessage.getGroups().get(); + attachmentHelper.retrieveAttachment(groupsMessage, syncHelper::handleSyncDeviceGroups); + } catch (Exception e) { + logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage()); + } + } + if (syncMessage.getBlockedList().isPresent()) { + final var blockedListMessage = syncMessage.getBlockedList().get(); + for (var address : blockedListMessage.getAddresses()) { + contactHelper.setContactBlocked(recipientResolver.resolveRecipient(address), true); + } + for (var groupId : blockedListMessage.getGroupIds() + .stream() + .map(GroupId::unknownVersion) + .collect(Collectors.toSet())) { + try { + groupHelper.setGroupBlocked(groupId, true); + } catch (GroupNotFoundException e) { + logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", + groupId.toBase64()); + } + } + } + if (syncMessage.getContacts().isPresent()) { + try { + final var contactsMessage = syncMessage.getContacts().get(); + attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(), + syncHelper::handleSyncDeviceContacts); + } catch (Exception e) { + logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); + } + } + if (syncMessage.getVerified().isPresent()) { + final var verifiedMessage = syncMessage.getVerified().get(); + account.getIdentityKeyStore() + .setIdentityTrustLevel(account.getRecipientStore() + .resolveRecipientTrusted(verifiedMessage.getDestination()), + verifiedMessage.getIdentityKey(), + TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + } + if (syncMessage.getStickerPackOperations().isPresent()) { + final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); + for (var m : stickerPackOperationMessages) { + if (!m.getPackId().isPresent()) { + continue; + } + final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); + final var installed = !m.getType().isPresent() + || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; + + var sticker = account.getStickerStore().getSticker(stickerPackId); + if (m.getPackKey().isPresent()) { + if (sticker == null) { + sticker = new Sticker(stickerPackId, m.getPackKey().get()); + } + if (installed) { + jobExecutor.enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); + } + } + + if (sticker != null) { + sticker.setInstalled(installed); + account.getStickerStore().updateSticker(sticker); + } + } + } + if (syncMessage.getFetchType().isPresent()) { + switch (syncMessage.getFetchType().get()) { + case LOCAL_PROFILE: + actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); + case STORAGE_MANIFEST: + // TODO + } + } + if (syncMessage.getKeys().isPresent()) { + final var keysMessage = syncMessage.getKeys().get(); + if (keysMessage.getStorageService().isPresent()) { + final var storageKey = keysMessage.getStorageService().get(); + account.setStorageKey(storageKey); + } + } + if (syncMessage.getConfiguration().isPresent()) { + // TODO + } + } + } + return actions; + } + + private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) { + SignalServiceAddress source; + if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { + source = envelope.getSourceAddress(); + } else if (content != null) { + source = content.getSender(); + } else { + return false; + } + final var recipientId = recipientResolver.resolveRecipient(source); + if (contactHelper.isContactBlocked(recipientId)) { + return true; + } + + if (content != null && content.getDataMessage().isPresent()) { + var message = content.getDataMessage().get(); + if (message.getGroupContext().isPresent()) { + var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + return groupHelper.isGroupBlocked(groupId); + } + } + + return false; + } + + private boolean isNotAllowedToSendToGroup(SignalServiceEnvelope envelope, SignalServiceContent content) { + SignalServiceAddress source; + if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { + source = envelope.getSourceAddress(); + } else if (content != null) { + source = content.getSender(); + } else { + return false; + } + + if (content == null || !content.getDataMessage().isPresent()) { + return false; + } + + var message = content.getDataMessage().get(); + if (!message.getGroupContext().isPresent()) { + return false; + } + + if (message.getGroupContext().get().getGroupV1().isPresent()) { + var groupInfo = message.getGroupContext().get().getGroupV1().get(); + if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) { + return false; + } + } + + var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + var group = groupHelper.getGroup(groupId); + if (group == null) { + return false; + } + + final var recipientId = recipientResolver.resolveRecipient(source); + if (!group.isMember(recipientId)) { + return true; + } + + if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { + return message.getBody().isPresent() + || message.getAttachments().isPresent() + || message.getQuote() + .isPresent() + || message.getPreviews().isPresent() + || message.getMentions().isPresent() + || message.getSticker().isPresent(); + } + return false; + } + + private List handleSignalServiceDataMessage( + SignalServiceDataMessage message, + boolean isSync, + RecipientId source, + RecipientId destination, + boolean ignoreAttachments + ) { + var actions = new ArrayList(); + if (message.getGroupContext().isPresent()) { + if (message.getGroupContext().get().getGroupV1().isPresent()) { + var groupInfo = message.getGroupContext().get().getGroupV1().get(); + var groupId = GroupId.v1(groupInfo.getGroupId()); + var group = groupHelper.getGroup(groupId); + if (group == null || group instanceof GroupInfoV1) { + var groupV1 = (GroupInfoV1) group; + switch (groupInfo.getType()) { + case UPDATE: { + if (groupV1 == null) { + groupV1 = new GroupInfoV1(groupId); + } + + if (groupInfo.getAvatar().isPresent()) { + var avatar = groupInfo.getAvatar().get(); + groupHelper.downloadGroupAvatar(groupV1.getGroupId(), avatar); + } + + if (groupInfo.getName().isPresent()) { + groupV1.name = groupInfo.getName().get(); + } + + if (groupInfo.getMembers().isPresent()) { + groupV1.addMembers(groupInfo.getMembers() + .get() + .stream() + .map(recipientResolver::resolveRecipient) + .collect(Collectors.toSet())); + } + + account.getGroupStore().updateGroup(groupV1); + break; + } + case DELIVER: + if (groupV1 == null && !isSync) { + actions.add(new SendGroupInfoRequestAction(source, groupId)); + } + break; + case QUIT: { + if (groupV1 != null) { + groupV1.removeMember(source); + account.getGroupStore().updateGroup(groupV1); + } + break; + } + case REQUEST_INFO: + if (groupV1 != null && !isSync) { + actions.add(new SendGroupInfoAction(source, groupV1.getGroupId())); + } + break; + } + } else { + // Received a group v1 message for a v2 group + } + } + if (message.getGroupContext().get().getGroupV2().isPresent()) { + final var groupContext = message.getGroupContext().get().getGroupV2().get(); + final var groupMasterKey = groupContext.getMasterKey(); + + groupHelper.getOrMigrateGroup(groupMasterKey, + groupContext.getRevision(), + groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); + } + } + + final var conversationPartnerAddress = isSync ? destination : source; + if (conversationPartnerAddress != null && message.isEndSession()) { + account.getSessionStore().deleteAllSessions(conversationPartnerAddress); + } + if (message.isExpirationUpdate() || message.getBody().isPresent()) { + if (message.getGroupContext().isPresent()) { + if (message.getGroupContext().get().getGroupV1().isPresent()) { + var groupInfo = message.getGroupContext().get().getGroupV1().get(); + var group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId())); + if (group != null) { + if (group.messageExpirationTime != message.getExpiresInSeconds()) { + group.messageExpirationTime = message.getExpiresInSeconds(); + account.getGroupStore().updateGroup(group); + } + } + } else if (message.getGroupContext().get().getGroupV2().isPresent()) { + // disappearing message timer already stored in the DecryptedGroup + } + } else if (conversationPartnerAddress != null) { + contactHelper.setExpirationTimer(conversationPartnerAddress, message.getExpiresInSeconds()); + } + } + if (!ignoreAttachments) { + if (message.getAttachments().isPresent()) { + for (var attachment : message.getAttachments().get()) { + attachmentHelper.downloadAttachment(attachment); + } + } + if (message.getSharedContacts().isPresent()) { + for (var contact : message.getSharedContacts().get()) { + if (contact.getAvatar().isPresent()) { + attachmentHelper.downloadAttachment(contact.getAvatar().get().getAttachment()); + } + } + } + if (message.getPreviews().isPresent()) { + final var previews = message.getPreviews().get(); + for (var preview : previews) { + if (preview.getImage().isPresent()) { + attachmentHelper.downloadAttachment(preview.getImage().get()); + } + } + } + if (message.getQuote().isPresent()) { + final var quote = message.getQuote().get(); + + for (var quotedAttachment : quote.getAttachments()) { + final var thumbnail = quotedAttachment.getThumbnail(); + if (thumbnail != null) { + attachmentHelper.downloadAttachment(thumbnail); + } + } + } + } + if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { + final ProfileKey profileKey; + try { + profileKey = new ProfileKey(message.getProfileKey().get()); + } catch (InvalidInputException e) { + throw new AssertionError(e); + } + if (account.getSelfRecipientId().equals(source)) { + this.account.setProfileKey(profileKey); + } + this.account.getProfileStore().storeProfileKey(source, profileKey); + } + if (message.getSticker().isPresent()) { + final var messageSticker = message.getSticker().get(); + final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId()); + var sticker = account.getStickerStore().getSticker(stickerPackId); + if (sticker == null) { + sticker = new Sticker(stickerPackId, messageSticker.getPackKey()); + account.getStickerStore().updateSticker(sticker); + } + jobExecutor.enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); + } + return actions; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index f92d7bde..058a04f2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -131,6 +131,16 @@ public class SendHelper { return result; } + public void sendDeliveryReceipt( + RecipientId recipientId, List messageIds + ) throws IOException, UntrustedIdentityException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, + messageIds, + System.currentTimeMillis()); + + sendReceiptMessage(receiptMessage, recipientId); + } + public void sendReceiptMessage( final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId ) throws IOException, UntrustedIdentityException { diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java index d34669a4..82c3bf16 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java @@ -1,27 +1,43 @@ package org.asamk.signal.manager.jobs; import org.asamk.signal.manager.StickerPackStore; +import org.asamk.signal.manager.helper.GroupHelper; +import org.asamk.signal.manager.helper.ProfileHelper; +import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.storage.SignalAccount; import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; public class Context { - private SignalAccount account; - private SignalServiceAccountManager accountManager; - private SignalServiceMessageReceiver messageReceiver; - private StickerPackStore stickerPackStore; + private final SignalAccount account; + private final SignalServiceAccountManager accountManager; + private final SignalServiceMessageReceiver messageReceiver; + private final StickerPackStore stickerPackStore; + private final SendHelper sendHelper; + private final GroupHelper groupHelper; + private final SyncHelper syncHelper; + private final ProfileHelper profileHelper; public Context( final SignalAccount account, final SignalServiceAccountManager accountManager, final SignalServiceMessageReceiver messageReceiver, - final StickerPackStore stickerPackStore + final StickerPackStore stickerPackStore, + final SendHelper sendHelper, + final GroupHelper groupHelper, + final SyncHelper syncHelper, + final ProfileHelper profileHelper ) { this.account = account; this.accountManager = accountManager; this.messageReceiver = messageReceiver; this.stickerPackStore = stickerPackStore; + this.sendHelper = sendHelper; + this.groupHelper = groupHelper; + this.syncHelper = syncHelper; + this.profileHelper = profileHelper; } public SignalAccount getAccount() { @@ -39,4 +55,20 @@ public class Context { public StickerPackStore getStickerPackStore() { return stickerPackStore; } + + public SendHelper getSendHelper() { + return sendHelper; + } + + public GroupHelper getGroupHelper() { + return groupHelper; + } + + public SyncHelper getSyncHelper() { + return syncHelper; + } + + public ProfileHelper getProfileHelper() { + return profileHelper; + } } diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 105c2016..77a622b1 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -13,6 +14,8 @@ import org.asamk.signal.util.CommandUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; + public class BlockCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(BlockCommand.class); @@ -39,6 +42,8 @@ public class BlockCommand implements JsonRpcLocalCommand { m.setContactBlocked(contact, true); } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to sync block to linked devices: " + e.getMessage()); } } @@ -49,6 +54,8 @@ public class BlockCommand implements JsonRpcLocalCommand { m.setGroupBlocked(groupId, true); } catch (GroupNotFoundException e) { logger.warn("Group not found {}: {}", groupId.toBase64(), e.getMessage()); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to sync block to linked devices: " + e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index e931a60e..46bd9daa 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -5,6 +5,7 @@ import net.sourceforge.argparse4j.inf.Subparser; import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -13,6 +14,8 @@ import org.asamk.signal.util.CommandUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; + public class UnblockCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(UnblockCommand.class); @@ -38,6 +41,8 @@ public class UnblockCommand implements JsonRpcLocalCommand { m.setContactBlocked(contactNumber, false); } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to sync unblock to linked devices: " + e.getMessage()); } } @@ -47,6 +52,8 @@ public class UnblockCommand implements JsonRpcLocalCommand { m.setGroupBlocked(groupId, false); } catch (GroupNotFoundException e) { logger.warn("Unknown group id: {}", groupId); + } catch (IOException e) { + throw new UnexpectedErrorException("Failed to sync unblock to linked devices: " + e.getMessage()); } } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 392b2df0..6a7cc764 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -253,6 +253,8 @@ public class DbusSignalImpl implements Signal { m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); } } @@ -262,6 +264,8 @@ public class DbusSignalImpl implements Signal { m.setGroupBlocked(getGroupId(groupId), blocked); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); } } From 634437d22dc7120718fdb693b6b8c072aa153e60 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 15:26:25 +0200 Subject: [PATCH 0776/2005] Delete cached failed messages after 30 days --- lib/src/main/java/org/asamk/signal/manager/Manager.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 0d06dfba..bbdfa9e5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -851,6 +851,11 @@ public class Manager implements Closeable { try { content = dependencies.getCipher().decrypt(envelope); } catch (ProtocolUntrustedIdentityException e) { + if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { + // Envelope is more than a month old, cleaning up. + cachedMessage.delete(); + return null; + } if (!envelope.hasSource()) { final var identifier = e.getSender(); final var recipientId = resolveRecipient(identifier); From 85c5caeacaa1b335ff38ed3d8bce9c02b8daca13 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 27 Aug 2021 09:04:14 +0200 Subject: [PATCH 0777/2005] Don't handle blocked or forbidden messages --- .../asamk/signal/manager/helper/IncomingMessageHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 369d3205..b0b42545 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -107,8 +107,6 @@ public final class IncomingMessageHandler { // address/uuid is validated by unidentified sender certificate account.getRecipientStore().resolveRecipientTrusted(content.getSender()); } - - actions.addAll(handleMessage(envelope, content, ignoreAttachments)); } if (isMessageBlocked(envelope, content)) { @@ -118,6 +116,7 @@ public final class IncomingMessageHandler { (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), envelope.getTimestamp()); } else { + actions.addAll(handleMessage(envelope, content, ignoreAttachments)); handler.handleMessage(envelope, content, exception); } return new Pair<>(actions, exception); From 8bcd8d87d219ae0496986cba4bd6b89f3b2ad6f6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 26 Aug 2021 21:23:30 +0200 Subject: [PATCH 0778/2005] Update libsignal-service-java --- lib/build.gradle.kts | 2 +- .../org/asamk/signal/manager/AvatarStore.java | 2 +- .../org/asamk/signal/manager/Manager.java | 229 ++++++++------- .../manager/UntrustedIdentityException.java | 27 ++ .../manager/api/RecipientIdentifier.java | 4 +- .../signal/manager/helper/GroupV2Helper.java | 62 ++-- .../helper/IncomingMessageHandler.java | 266 +++++++++--------- .../signal/manager/helper/ProfileHelper.java | 10 +- .../signal/manager/helper/SendHelper.java | 54 +++- .../signal/manager/helper/SyncHelper.java | 51 +--- .../signal/manager/storage/SignalAccount.java | 16 +- .../asamk/signal/manager/storage/Utils.java | 12 + .../storage/contacts/LegacyContactInfo.java | 6 +- .../manager/storage/groups/GroupInfoV2.java | 9 +- .../manager/storage/groups/GroupStore.java | 14 +- .../storage/identities/IdentityKeyStore.java | 3 +- .../storage/profiles/LegacyProfileStore.java | 6 +- .../profiles/LegacySignalProfileEntry.java | 12 +- .../storage/protocol/LegacyIdentityInfo.java | 10 +- .../protocol/LegacyJsonIdentityKeyStore.java | 16 +- .../protocol/LegacyJsonSessionStore.java | 12 +- .../storage/protocol/LegacySessionInfo.java | 6 +- .../storage/protocol/SignalProtocolStore.java | 12 + .../recipients/LegacyRecipientStore.java | 14 +- .../manager/storage/recipients/Recipient.java | 11 +- .../storage/recipients/RecipientAddress.java | 89 ++++++ .../storage/recipients/RecipientResolver.java | 8 + .../storage/recipients/RecipientStore.java | 99 ++++--- .../storage/sessions/SessionStore.java | 17 +- .../org/asamk/signal/manager/util/Utils.java | 14 +- run_tests.sh | 4 +- .../signal/JsonDbusReceiveMessageHandler.java | 2 +- .../asamk/signal/ReceiveMessageHandler.java | 13 +- .../signal/commands/JoinGroupCommand.java | 9 +- .../signal/commands/ListContactsCommand.java | 3 +- .../signal/commands/ListGroupsCommand.java | 4 +- .../commands/ListIdentitiesCommand.java | 3 +- .../signal/commands/QuitGroupCommand.java | 6 +- .../signal/commands/RemoteDeleteCommand.java | 6 +- .../asamk/signal/commands/SendCommand.java | 21 +- .../signal/commands/SendReactionCommand.java | 6 +- .../signal/commands/SendReceiptCommand.java | 5 +- .../signal/commands/SendTypingCommand.java | 5 +- .../signal/commands/UpdateGroupCommand.java | 6 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 3 + .../org/asamk/signal/json/JsonMention.java | 7 +- .../signal/json/JsonMessageEnvelope.java | 30 +- .../java/org/asamk/signal/json/JsonQuote.java | 3 +- .../org/asamk/signal/json/JsonReaction.java | 4 +- .../signal/json/JsonSyncDataMessage.java | 4 +- .../signal/json/JsonSyncReadMessage.java | 4 +- src/main/java/org/asamk/signal/util/Util.java | 2 +- 52 files changed, 692 insertions(+), 551 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/UntrustedIdentityException.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index dcb99cee..316ce564 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -14,7 +14,7 @@ repositories { } dependencies { - api("com.github.turasa:signal-service-java:2.15.3_unofficial_25") + api("com.github.turasa:signal-service-java:2.15.3_unofficial_26") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.slf4j:slf4j-api:1.7.30") diff --git a/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java b/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java index 8a1e6172..12a6525e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/AvatarStore.java @@ -82,7 +82,7 @@ public class AvatarStore { } private String getLegacyIdentifier(final SignalServiceAddress address) { - return address.getNumber().or(() -> address.getUuid().get().toString()); + return address.getNumber().or(() -> address.getUuid().toString()); } private File getProfileAvatarFile(SignalServiceAddress address) { diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index bbdfa9e5..9e38853b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -73,7 +73,6 @@ import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; @@ -83,6 +82,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -200,7 +200,7 @@ public class Manager implements Closeable { dependencies, unidentifiedAccessHelper, this::resolveSignalServiceAddress, - this::resolveRecipient, + account.getRecipientStore(), this::handleIdentityFailure, this::getGroup, this::refreshRegisteredUser); @@ -211,15 +211,14 @@ public class Manager implements Closeable { groupV2Helper, avatarStore, this::resolveSignalServiceAddress, - this::resolveRecipient); + account.getRecipientStore()); this.contactHelper = new ContactHelper(account); this.syncHelper = new SyncHelper(account, attachmentHelper, sendHelper, groupHelper, avatarStore, - this::resolveSignalServiceAddress, - this::resolveRecipient); + this::resolveSignalServiceAddress); this.context = new Context(account, dependencies.getAccountManager(), @@ -233,7 +232,8 @@ public class Manager implements Closeable { this.incomingMessageHandler = new IncomingMessageHandler(account, dependencies, - this::resolveRecipient, + account.getRecipientStore(), + this::resolveSignalServiceAddress, groupHelper, contactHelper, attachmentHelper, @@ -328,7 +328,7 @@ public class Manager implements Closeable { public Map> areUsersRegistered(Set numbers) throws IOException { Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { try { - return canonicalizePhoneNumber(n); + return PhoneNumberFormatter.formatNumber(n, account.getUsername()); } catch (InvalidNumberException e) { return ""; } @@ -490,7 +490,7 @@ public class Manager implements Closeable { public SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - final var newAdmins = getRecipientIds(groupAdmins); + final var newAdmins = resolveRecipients(groupAdmins); return groupHelper.quitGroup(groupId, newAdmins); } @@ -501,7 +501,7 @@ public class Manager implements Closeable { public Pair createGroup( String name, Set members, File avatarFile ) throws IOException, AttachmentInvalidException { - return groupHelper.createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); + return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile); } public SendGroupMessageResults updateGroup( @@ -523,10 +523,10 @@ public class Manager implements Closeable { return groupHelper.updateGroup(groupId, name, description, - members == null ? null : getRecipientIds(members), - removeMembers == null ? null : getRecipientIds(removeMembers), - admins == null ? null : getRecipientIds(admins), - removeAdmins == null ? null : getRecipientIds(removeAdmins), + members == null ? null : resolveRecipients(members), + removeMembers == null ? null : resolveRecipients(removeMembers), + admins == null ? null : resolveRecipients(admins), + removeAdmins == null ? null : resolveRecipients(removeAdmins), resetGroupLink, groupLinkState, addMemberPermission, @@ -662,7 +662,7 @@ public class Manager implements Closeable { public void setContactName( RecipientIdentifier.Single recipient, String name - ) throws NotMasterDeviceException { + ) throws NotMasterDeviceException, UnregisteredUserException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } @@ -755,53 +755,28 @@ public class Manager implements Closeable { return certificate; } - private Set getRecipientIds(Collection recipients) { - final var signalServiceAddresses = new HashSet(recipients.size()); - final var addressesMissingUuid = new HashSet(); - - for (var number : recipients) { - final var resolvedAddress = resolveSignalServiceAddress(resolveRecipient(number)); - if (resolvedAddress.getUuid().isPresent()) { - signalServiceAddresses.add(resolvedAddress); - } else { - addressesMissingUuid.add(resolvedAddress); - } - } - - final var numbersMissingUuid = addressesMissingUuid.stream() - .map(a -> a.getNumber().get()) - .collect(Collectors.toSet()); - Map registeredUsers; - try { - registeredUsers = getRegisteredUsers(numbersMissingUuid); - } catch (IOException e) { - logger.warn("Failed to resolve uuids from server, ignoring: {}", e.getMessage()); - registeredUsers = Map.of(); - } - - for (var address : addressesMissingUuid) { - final var number = address.getNumber().get(); - if (registeredUsers.containsKey(number)) { - final var newAddress = resolveSignalServiceAddress(resolveRecipientTrusted(new SignalServiceAddress( - registeredUsers.get(number), - number))); - signalServiceAddresses.add(newAddress); - } else { - signalServiceAddresses.add(address); - } - } - - return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet()); - } - private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException { final var address = resolveSignalServiceAddress(recipientId); if (!address.getNumber().isPresent()) { return recipientId; } final var number = address.getNumber().get(); - final var uuidMap = getRegisteredUsers(Set.of(number)); - return resolveRecipientTrusted(new SignalServiceAddress(uuidMap.getOrDefault(number, null), number)); + final var uuid = getRegisteredUser(number); + return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); + } + + private UUID getRegisteredUser(final String number) throws IOException { + final Map uuidMap; + try { + uuidMap = getRegisteredUsers(Set.of(number)); + } catch (NumberFormatException e) { + throw new UnregisteredUserException(number, e); + } + final var uuid = uuidMap.get(number); + if (uuid == null) { + throw new UnregisteredUserException(number, null); + } + return uuid; } private Map getRegisteredUsers(final Set numbers) throws IOException { @@ -856,9 +831,9 @@ public class Manager implements Closeable { cachedMessage.delete(); return null; } - if (!envelope.hasSource()) { + if (!envelope.hasSourceUuid()) { final var identifier = e.getSender(); - final var recipientId = resolveRecipient(identifier); + final var recipientId = account.getRecipientStore().resolveRecipient(identifier); try { account.getMessageCache().replaceSender(cachedMessage, recipientId); } catch (IOException ioException) { @@ -901,8 +876,8 @@ public class Manager implements Closeable { logger.debug("Checking for new message from server"); try { var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> { - final var recipientId = envelope1.hasSource() - ? resolveRecipient(envelope1.getSourceIdentifier()) + final var recipientId = envelope1.hasSourceUuid() + ? resolveRecipient(envelope1.getSourceAddress()) : null; // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); @@ -944,10 +919,10 @@ public class Manager implements Closeable { handleQueuedActions(queuedActions); } if (cachedMessage[0] != null) { - if (exception instanceof ProtocolUntrustedIdentityException) { - final var identifier = ((ProtocolUntrustedIdentityException) exception).getSender(); - final var recipientId = resolveRecipient(identifier); - if (!envelope.hasSource()) { + if (exception instanceof UntrustedIdentityException) { + final var address = ((UntrustedIdentityException) exception).getSender(); + final var recipientId = resolveRecipient(address); + if (!envelope.hasSourceUuid()) { try { cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); } catch (IOException ioException) { @@ -977,7 +952,12 @@ public class Manager implements Closeable { } public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { - final var recipientId = resolveRecipient(recipient); + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } return contactHelper.isContactBlocked(recipientId); } @@ -994,7 +974,12 @@ public class Manager implements Closeable { } public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { - final var recipientId = resolveRecipient(recipientIdentifier); + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipientIdentifier); + } catch (UnregisteredUserException e) { + return null; + } final var contact = account.getContactStore().getContact(recipientId); if (contact != null && !Util.isEmpty(contact.getName())) { @@ -1018,7 +1003,12 @@ public class Manager implements Closeable { } public List getIdentities(RecipientIdentifier.Single recipient) { - final var identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); + IdentityInfo identity; + try { + identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); + } catch (UnregisteredUserException e) { + identity = null; + } return identity == null ? List.of() : List.of(identity); } @@ -1029,7 +1019,12 @@ public class Manager implements Closeable { * @param fingerprint Fingerprint */ public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { - var recipientId = resolveRecipient(recipient); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } return trustIdentity(recipientId, identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), TrustLevel.TRUSTED_VERIFIED); @@ -1042,8 +1037,13 @@ public class Manager implements Closeable { * @param safetyNumber Safety number */ public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { - var recipientId = resolveRecipient(recipient); - var address = account.getRecipientStore().resolveServiceAddress(recipientId); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), TrustLevel.TRUSTED_VERIFIED); @@ -1056,8 +1056,13 @@ public class Manager implements Closeable { * @param safetyNumber Scannable safety number */ public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { - var recipientId = resolveRecipient(recipient); - var address = account.getRecipientStore().resolveServiceAddress(recipientId); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> { final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); try { @@ -1074,7 +1079,12 @@ public class Manager implements Closeable { * @param recipient username of the identity */ public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { - var recipientId = resolveRecipient(recipient); + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); } @@ -1092,7 +1102,7 @@ public class Manager implements Closeable { account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); try { - var address = account.getRecipientStore().resolveServiceAddress(recipientId); + var address = resolveSignalServiceAddress(recipientId); syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); } catch (IOException e) { logger.warn("Failed to send verification sync message: {}", e.getMessage()); @@ -1136,48 +1146,61 @@ public class Manager implements Closeable { theirIdentityKey); } - @Deprecated - public SignalServiceAddress resolveSignalServiceAddress(String identifier) { - var address = Utils.getSignalServiceAddressFromIdentifier(identifier); - - return resolveSignalServiceAddress(address); - } - - @Deprecated public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { if (address.matches(account.getSelfAddress())) { return account.getSelfAddress(); } - return account.getRecipientStore().resolveServiceAddress(address); + return resolveSignalServiceAddress(resolveRecipient(address)); + } + + public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { + return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); } public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { - return account.getRecipientStore().resolveServiceAddress(recipientId); - } - - private String canonicalizePhoneNumber(final String number) throws InvalidNumberException { - return PhoneNumberFormatter.formatNumber(number, account.getUsername()); - } - - private RecipientId resolveRecipient(final String identifier) { - var address = Utils.getSignalServiceAddressFromIdentifier(identifier); - - return resolveRecipient(address); - } - - private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) { - final SignalServiceAddress address; - if (recipient instanceof RecipientIdentifier.Uuid) { - address = new SignalServiceAddress(((RecipientIdentifier.Uuid) recipient).uuid, null); - } else { - address = new SignalServiceAddress(null, ((RecipientIdentifier.Number) recipient).number); + final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); + if (address.getUuid().isPresent()) { + return address.toSignalServiceAddress(); } - return resolveRecipient(address); + // Address in recipient store doesn't have a uuid, this shouldn't happen + // Try to retrieve the uuid from the server + final var number = address.getNumber().get(); + try { + return resolveSignalServiceAddress(getRegisteredUser(number)); + } catch (IOException e) { + logger.warn("Failed to get uuid for e164 number: {}", number, e); + // Return SignalServiceAddress with unknown UUID + return address.toSignalServiceAddress(); + } } - public RecipientId resolveRecipient(SignalServiceAddress address) { + private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { + final var recipientIds = new HashSet(recipients.size()); + for (var number : recipients) { + final var recipientId = resolveRecipient(number); + recipientIds.add(recipientId); + } + return recipientIds; + } + + private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { + if (recipient instanceof RecipientIdentifier.Uuid) { + return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); + } else { + final var number = ((RecipientIdentifier.Number) recipient).number; + return account.getRecipientStore().resolveRecipient(number, () -> { + try { + return getRegisteredUser(number); + } catch (IOException e) { + return null; + } + }); + } + } + + private RecipientId resolveRecipient(SignalServiceAddress address) { return account.getRecipientStore().resolveRecipient(address); } diff --git a/lib/src/main/java/org/asamk/signal/manager/UntrustedIdentityException.java b/lib/src/main/java/org/asamk/signal/manager/UntrustedIdentityException.java new file mode 100644 index 00000000..3b90b9e4 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/UntrustedIdentityException.java @@ -0,0 +1,27 @@ +package org.asamk.signal.manager; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +public class UntrustedIdentityException extends Exception { + + private final SignalServiceAddress sender; + private final Integer senderDevice; + + public UntrustedIdentityException(final SignalServiceAddress sender) { + this(sender, null); + } + + public UntrustedIdentityException(final SignalServiceAddress sender, final Integer senderDevice) { + super("Untrusted identity: " + sender.getIdentifier()); + this.sender = sender; + this.senderDevice = senderDevice; + } + + public SignalServiceAddress getSender() { + return sender; + } + + public Integer getSenderDevice() { + return senderDevice; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java index cbcf1724..4a66cbb3 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -32,9 +32,7 @@ public abstract class RecipientIdentifier { } public static Single fromAddress(SignalServiceAddress address) { - return address.getUuid().isPresent() - ? new Uuid(address.getUuid().get()) - : new Number(address.getNumber().get()); + return new Uuid(address.getUuid()); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 19240cef..3187fca1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -150,11 +150,9 @@ public class GroupV2Helper { if (!areMembersValid(members)) return null; - var self = new GroupCandidate(addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) - .getUuid() - .orNull(), Optional.fromNullable(profileKeyCredential)); + var self = new GroupCandidate(getSelfUuid(), Optional.fromNullable(profileKeyCredential)); var candidates = members.stream() - .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(), + .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid(), Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member)))) .collect(Collectors.toSet()); @@ -169,18 +167,6 @@ public class GroupV2Helper { } private boolean areMembersValid(final Set members) { - final var noUuidCapability = members.stream() - .map(addressResolver::resolveSignalServiceAddress) - .filter(address -> !address.getUuid().isPresent()) - .map(SignalServiceAddress::getNumber) - .map(Optional::get) - .collect(Collectors.toSet()); - if (noUuidCapability.size() > 0) { - logger.warn("Cannot create a V2 group as some members don't have a UUID: {}", - String.join(", ", noUuidCapability)); - return false; - } - final var noGv2Capability = members.stream() .map(profileProvider::getProfile) .filter(profile -> profile != null && !profile.getCapabilities().contains(Profile.Capability.gv2)) @@ -214,11 +200,8 @@ public class GroupV2Helper { change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey)); } - final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId()) - .getUuid(); - if (uuid.isPresent()) { - change.setSourceUuid(UuidUtil.toByteString(uuid.get())); - } + final var uuid = getSelfUuid(); + change.setSourceUuid(UuidUtil.toByteString(uuid)); return commitChange(groupInfoV2, change); } @@ -233,13 +216,11 @@ public class GroupV2Helper { } var candidates = newMembers.stream() - .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid().get(), + .map(member -> new GroupCandidate(addressResolver.resolveSignalServiceAddress(member).getUuid(), Optional.fromNullable(profileKeyCredentialProvider.getProfileKeyCredential(member)))) .collect(Collectors.toSet()); - final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) - .getUuid() - .get(); + final var uuid = getSelfUuid(); final var change = groupOperations.createModifyGroupMembershipChange(candidates, uuid); change.setSourceUuid(UuidUtil.toByteString(uuid)); @@ -251,9 +232,7 @@ public class GroupV2Helper { GroupInfoV2 groupInfoV2, Set membersToMakeAdmin ) throws IOException { var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList(); - final var selfUuid = addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) - .getUuid() - .get(); + final var selfUuid = getSelfUuid(); var selfPendingMember = DecryptedGroupUtil.findPendingByUuid(pendingMembersList, selfUuid); if (selfPendingMember.isPresent()) { @@ -263,7 +242,6 @@ public class GroupV2Helper { final var adminUuids = membersToMakeAdmin.stream() .map(addressResolver::resolveSignalServiceAddress) .map(SignalServiceAddress::getUuid) - .map(Optional::get) .collect(Collectors.toList()); final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); return commitChange(groupInfoV2, groupOperations.createLeaveAndPromoteMembersToAdmin(selfUuid, adminUuids)); @@ -275,8 +253,6 @@ public class GroupV2Helper { final var memberUuids = members.stream() .map(addressResolver::resolveSignalServiceAddress) .map(SignalServiceAddress::getUuid) - .filter(Optional::isPresent) - .map(Optional::get) .collect(Collectors.toSet()); return ejectMembers(groupInfoV2, memberUuids); } @@ -288,8 +264,6 @@ public class GroupV2Helper { final var memberUuids = members.stream() .map(addressResolver::resolveSignalServiceAddress) .map(SignalServiceAddress::getUuid) - .filter(Optional::isPresent) - .map(Optional::get) .map(uuid -> DecryptedGroupUtil.findPendingByUuid(pendingMembersList, uuid)) .filter(Optional::isPresent) .map(Optional::get) @@ -360,8 +334,7 @@ public class GroupV2Helper { : groupOperations.createGroupJoinDirect(profileKeyCredential); change.setSourceUuid(UuidUtil.toByteString(addressResolver.resolveSignalServiceAddress(selfRecipientId) - .getUuid() - .get())); + .getUuid())); return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword); } @@ -378,9 +351,7 @@ public class GroupV2Helper { final var change = groupOperations.createAcceptInviteChange(profileKeyCredential); final var uuid = addressResolver.resolveSignalServiceAddress(selfRecipientId).getUuid(); - if (uuid.isPresent()) { - change.setSourceUuid(UuidUtil.toByteString(uuid.get())); - } + change.setSourceUuid(UuidUtil.toByteString(uuid)); return commitChange(groupInfoV2, change); } @@ -391,7 +362,7 @@ public class GroupV2Helper { final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); final var address = addressResolver.resolveSignalServiceAddress(recipientId); final var newRole = admin ? Member.Role.ADMINISTRATOR : Member.Role.DEFAULT; - final var change = groupOperations.createChangeMemberRole(address.getUuid().get(), newRole); + final var change = groupOperations.createChangeMemberRole(address.getUuid(), newRole); return commitChange(groupInfoV2, change); } @@ -473,10 +444,7 @@ public class GroupV2Helper { final DecryptedGroup decryptedGroupState; try { - decryptedChange = groupOperations.decryptChange(changeActions, - addressResolver.resolveSignalServiceAddress(selfRecipientIdProvider.getSelfRecipientId()) - .getUuid() - .get()); + decryptedChange = groupOperations.decryptChange(changeActions, getSelfUuid()); decryptedGroupState = DecryptedGroupUtil.apply(previousGroupState, decryptedChange); } catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) { throw new IOException(e); @@ -543,13 +511,15 @@ public class GroupV2Helper { final var credentials = groupsV2Api.getCredentials(today); // TODO cache credentials until they expire var authCredentialResponse = credentials.get(today); - final var uuid = addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId()) - .getUuid() - .get(); + final var uuid = getSelfUuid(); try { return groupsV2Api.getGroupsV2AuthorizationString(uuid, today, groupSecretParams, authCredentialResponse); } catch (VerificationFailedException e) { throw new IOException(e); } } + + private UUID getSelfUuid() { + return addressResolver.resolveSignalServiceAddress(this.selfRecipientIdProvider.getSelfRecipientId()).getUuid(); + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index b0b42545..f28b3638 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -4,6 +4,7 @@ import org.asamk.signal.manager.JobExecutor; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.actions.RenewSessionAction; import org.asamk.signal.manager.actions.RetrieveProfileAction; @@ -34,6 +35,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; +import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; @@ -48,6 +50,7 @@ public final class IncomingMessageHandler { private final SignalAccount account; private final SignalDependencies dependencies; private final RecipientResolver recipientResolver; + private final SignalServiceAddressResolver addressResolver; private final GroupHelper groupHelper; private final ContactHelper contactHelper; private final AttachmentHelper attachmentHelper; @@ -58,6 +61,7 @@ public final class IncomingMessageHandler { final SignalAccount account, final SignalDependencies dependencies, final RecipientResolver recipientResolver, + final SignalServiceAddressResolver addressResolver, final GroupHelper groupHelper, final ContactHelper contactHelper, final AttachmentHelper attachmentHelper, @@ -67,6 +71,7 @@ public final class IncomingMessageHandler { this.account = account; this.dependencies = dependencies; this.recipientResolver = recipientResolver; + this.addressResolver = addressResolver; this.groupHelper = groupHelper; this.contactHelper = contactHelper; this.attachmentHelper = attachmentHelper; @@ -80,7 +85,7 @@ public final class IncomingMessageHandler { final Manager.ReceiveMessageHandler handler ) { final var actions = new ArrayList(); - if (envelope.hasSource()) { + if (envelope.hasSourceUuid()) { // Store uuid if we don't have it already // address/uuid in envelope is sent by server account.getRecipientStore().resolveRecipientTrusted(envelope.getSourceAddress()); @@ -93,6 +98,8 @@ public final class IncomingMessageHandler { } catch (ProtocolUntrustedIdentityException e) { final var recipientId = account.getRecipientStore().resolveRecipient(e.getSender()); actions.add(new RetrieveProfileAction(recipientId)); + exception = new UntrustedIdentityException(addressResolver.resolveSignalServiceAddress(recipientId), + e.getSenderDevice()); } catch (ProtocolInvalidMessageException e) { final var sender = account.getRecipientStore().resolveRecipient(e.getSender()); logger.debug("Received invalid message, queuing renew session action."); @@ -102,7 +109,7 @@ public final class IncomingMessageHandler { exception = e; } - if (!envelope.hasSource() && content != null) { + if (!envelope.hasSourceUuid() && content != null) { // Store uuid if we don't have it already // address/uuid is validated by unidentified sender certificate account.getRecipientStore().resolveRecipientTrusted(content.getSender()); @@ -113,7 +120,7 @@ public final class IncomingMessageHandler { logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); } else if (isNotAllowedToSendToGroup(envelope, content)) { logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", - (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), + (envelope.hasSourceUuid() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), envelope.getTimestamp()); } else { actions.addAll(handleMessage(envelope, content, ignoreAttachments)); @@ -126,146 +133,153 @@ public final class IncomingMessageHandler { SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments ) { var actions = new ArrayList(); - if (content != null) { - final RecipientId sender; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); - } else { - sender = recipientResolver.resolveRecipient(content.getSender()); + if (content == null) { + return actions; + } + + final RecipientId sender; + if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { + sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); + } else { + sender = recipientResolver.resolveRecipient(content.getSender()); + } + + if (content.getDataMessage().isPresent()) { + var message = content.getDataMessage().get(); + + if (content.isNeedsReceipt()) { + actions.add(new SendReceiptAction(sender, message.getTimestamp())); } - if (content.getDataMessage().isPresent()) { - var message = content.getDataMessage().get(); + actions.addAll(handleSignalServiceDataMessage(message, + false, + sender, + account.getSelfRecipientId(), + ignoreAttachments)); + } - if (content.isNeedsReceipt()) { - actions.add(new SendReceiptAction(sender, message.getTimestamp())); - } + if (content.getSyncMessage().isPresent()) { + var syncMessage = content.getSyncMessage().get(); + actions.addAll(handleSyncMessage(syncMessage, sender, ignoreAttachments)); + } - actions.addAll(handleSignalServiceDataMessage(message, - false, - sender, - account.getSelfRecipientId(), - ignoreAttachments)); + return actions; + } + + private List handleSyncMessage( + final SignalServiceSyncMessage syncMessage, final RecipientId sender, final boolean ignoreAttachments + ) { + var actions = new ArrayList(); + account.setMultiDevice(true); + if (syncMessage.getSent().isPresent()) { + var message = syncMessage.getSent().get(); + final var destination = message.getDestination().orNull(); + actions.addAll(handleSignalServiceDataMessage(message.getMessage(), + true, + sender, + destination == null ? null : recipientResolver.resolveRecipient(destination), + ignoreAttachments)); + } + if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { + var rm = syncMessage.getRequest().get(); + if (rm.isContactsRequest()) { + actions.add(SendSyncContactsAction.create()); } - if (content.getSyncMessage().isPresent()) { - account.setMultiDevice(true); - var syncMessage = content.getSyncMessage().get(); - if (syncMessage.getSent().isPresent()) { - var message = syncMessage.getSent().get(); - final var destination = message.getDestination().orNull(); - actions.addAll(handleSignalServiceDataMessage(message.getMessage(), - true, - sender, - destination == null ? null : recipientResolver.resolveRecipient(destination), - ignoreAttachments)); + if (rm.isGroupsRequest()) { + actions.add(SendSyncGroupsAction.create()); + } + if (rm.isBlockedListRequest()) { + actions.add(SendSyncBlockedListAction.create()); + } + // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); + } + if (syncMessage.getGroups().isPresent()) { + logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring."); + } + if (syncMessage.getBlockedList().isPresent()) { + final var blockedListMessage = syncMessage.getBlockedList().get(); + for (var address : blockedListMessage.getAddresses()) { + contactHelper.setContactBlocked(recipientResolver.resolveRecipient(address), true); + } + for (var groupId : blockedListMessage.getGroupIds() + .stream() + .map(GroupId::unknownVersion) + .collect(Collectors.toSet())) { + try { + groupHelper.setGroupBlocked(groupId, true); + } catch (GroupNotFoundException e) { + logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", + groupId.toBase64()); } - if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { - var rm = syncMessage.getRequest().get(); - if (rm.isContactsRequest()) { - actions.add(SendSyncContactsAction.create()); - } - if (rm.isGroupsRequest()) { - actions.add(SendSyncGroupsAction.create()); - } - if (rm.isBlockedListRequest()) { - actions.add(SendSyncBlockedListAction.create()); - } - // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); + } + } + if (syncMessage.getContacts().isPresent()) { + try { + final var contactsMessage = syncMessage.getContacts().get(); + attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(), + syncHelper::handleSyncDeviceContacts); + } catch (Exception e) { + logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); + } + } + if (syncMessage.getVerified().isPresent()) { + final var verifiedMessage = syncMessage.getVerified().get(); + account.getIdentityKeyStore() + .setIdentityTrustLevel(account.getRecipientStore() + .resolveRecipientTrusted(verifiedMessage.getDestination()), + verifiedMessage.getIdentityKey(), + TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); + } + if (syncMessage.getStickerPackOperations().isPresent()) { + final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); + for (var m : stickerPackOperationMessages) { + if (!m.getPackId().isPresent()) { + continue; } - if (syncMessage.getGroups().isPresent()) { - try { - final var groupsMessage = syncMessage.getGroups().get(); - attachmentHelper.retrieveAttachment(groupsMessage, syncHelper::handleSyncDeviceGroups); - } catch (Exception e) { - logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage()); - } - } - if (syncMessage.getBlockedList().isPresent()) { - final var blockedListMessage = syncMessage.getBlockedList().get(); - for (var address : blockedListMessage.getAddresses()) { - contactHelper.setContactBlocked(recipientResolver.resolveRecipient(address), true); - } - for (var groupId : blockedListMessage.getGroupIds() - .stream() - .map(GroupId::unknownVersion) - .collect(Collectors.toSet())) { - try { - groupHelper.setGroupBlocked(groupId, true); - } catch (GroupNotFoundException e) { - logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", - groupId.toBase64()); - } - } - } - if (syncMessage.getContacts().isPresent()) { - try { - final var contactsMessage = syncMessage.getContacts().get(); - attachmentHelper.retrieveAttachment(contactsMessage.getContactsStream(), - syncHelper::handleSyncDeviceContacts); - } catch (Exception e) { - logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); - } - } - if (syncMessage.getVerified().isPresent()) { - final var verifiedMessage = syncMessage.getVerified().get(); - account.getIdentityKeyStore() - .setIdentityTrustLevel(account.getRecipientStore() - .resolveRecipientTrusted(verifiedMessage.getDestination()), - verifiedMessage.getIdentityKey(), - TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); - } - if (syncMessage.getStickerPackOperations().isPresent()) { - final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); - for (var m : stickerPackOperationMessages) { - if (!m.getPackId().isPresent()) { - continue; - } - final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); - final var installed = !m.getType().isPresent() - || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; + final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); + final var installed = !m.getType().isPresent() + || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; - var sticker = account.getStickerStore().getSticker(stickerPackId); - if (m.getPackKey().isPresent()) { - if (sticker == null) { - sticker = new Sticker(stickerPackId, m.getPackKey().get()); - } - if (installed) { - jobExecutor.enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); - } - } + var sticker = account.getStickerStore().getSticker(stickerPackId); + if (m.getPackKey().isPresent()) { + if (sticker == null) { + sticker = new Sticker(stickerPackId, m.getPackKey().get()); + } + if (installed) { + jobExecutor.enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); + } + } - if (sticker != null) { - sticker.setInstalled(installed); - account.getStickerStore().updateSticker(sticker); - } - } + if (sticker != null) { + sticker.setInstalled(installed); + account.getStickerStore().updateSticker(sticker); } - if (syncMessage.getFetchType().isPresent()) { - switch (syncMessage.getFetchType().get()) { - case LOCAL_PROFILE: - actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); - case STORAGE_MANIFEST: - // TODO - } - } - if (syncMessage.getKeys().isPresent()) { - final var keysMessage = syncMessage.getKeys().get(); - if (keysMessage.getStorageService().isPresent()) { - final var storageKey = keysMessage.getStorageService().get(); - account.setStorageKey(storageKey); - } - } - if (syncMessage.getConfiguration().isPresent()) { + } + } + if (syncMessage.getFetchType().isPresent()) { + switch (syncMessage.getFetchType().get()) { + case LOCAL_PROFILE: + actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); + case STORAGE_MANIFEST: // TODO - } } } + if (syncMessage.getKeys().isPresent()) { + final var keysMessage = syncMessage.getKeys().get(); + if (keysMessage.getStorageService().isPresent()) { + final var storageKey = keysMessage.getStorageService().get(); + account.setStorageKey(storageKey); + } + } + if (syncMessage.getConfiguration().isPresent()) { + // TODO + } return actions; } private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) { SignalServiceAddress source; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { + if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { source = envelope.getSourceAddress(); } else if (content != null) { source = content.getSender(); @@ -290,7 +304,7 @@ public final class IncomingMessageHandler { private boolean isNotAllowedToSendToGroup(SignalServiceEnvelope envelope, SignalServiceContent content) { SignalServiceAddress source; - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { + if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { source = envelope.getSourceAddress(); } else if (content != null) { source = content.getSender(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index ac75a573..52154798 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -273,7 +273,7 @@ public final class ProfileHelper { private Single retrieveProfile( RecipientId recipientId, SignalServiceProfile.RequestType requestType - ) throws IOException { + ) { var unidentifiedAccess = getUnidentifiedAccess(recipientId); var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId)); @@ -286,7 +286,7 @@ public final class ProfileHelper { Optional profileKey, Optional unidentifiedAccess, SignalServiceProfile.RequestType requestType - ) throws IOException { + ) { var profileService = profileServiceProvider.getProfileService(); Single> responseSingle; @@ -294,11 +294,7 @@ public final class ProfileHelper { responseSingle = profileService.getProfile(address, profileKey, unidentifiedAccess, requestType); } catch (NoClassDefFoundError e) { // Native zkgroup lib not available for ProfileKey - if (!address.getNumber().isPresent()) { - throw new NotFoundException("Can't request profile without number"); - } - var addressWithoutUuid = new SignalServiceAddress(Optional.absent(), address.getNumber()); - responseSingle = profileService.getProfile(addressWithoutUuid, profileKey, unidentifiedAccess, requestType); + responseSingle = profileService.getProfile(address, Optional.absent(), unidentifiedAccess, requestType); } return responseSingle.map(pair -> { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 058a04f2..da901a3d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -1,6 +1,7 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; @@ -13,8 +14,8 @@ import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.ContentHint; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; @@ -43,6 +44,20 @@ public class SendHelper { private final GroupProvider groupProvider; private final RecipientRegistrationRefresher recipientRegistrationRefresher; + private final SignalServiceMessageSender.IndividualSendEvents sendEvents = new SignalServiceMessageSender.IndividualSendEvents() { + @Override + public void onMessageEncrypted() { + } + + @Override + public void onMessageSent() { + } + + @Override + public void onSyncMessageSent() { + } + }; + public SendHelper( final SignalAccount account, final SignalDependencies dependencies, @@ -145,9 +160,12 @@ public class SendHelper { final SignalServiceReceiptMessage receiptMessage, final RecipientId recipientId ) throws IOException, UntrustedIdentityException { final var messageSender = dependencies.getMessageSender(); - messageSender.sendReceipt(addressResolver.resolveSignalServiceAddress(recipientId), - unidentifiedAccessHelper.getAccessFor(recipientId), - receiptMessage); + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + try { + messageSender.sendReceipt(address, unidentifiedAccessHelper.getAccessFor(recipientId), receiptMessage); + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { + throw new UntrustedIdentityException(address); + } } public SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException { @@ -162,7 +180,7 @@ public class SendHelper { final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); return messageSender.sendNullMessage(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId)); } - } catch (UntrustedIdentityException e) { + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); } } @@ -183,7 +201,7 @@ public class SendHelper { var messageSender = dependencies.getMessageSender(); try { return messageSender.sendSyncMessage(message, unidentifiedAccessHelper.getAccessForSync()); - } catch (UntrustedIdentityException e) { + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { var address = addressResolver.resolveSignalServiceAddress(account.getSelfRecipientId()); return SendMessageResult.identityFailure(address, e.getIdentityKey()); } @@ -195,11 +213,15 @@ public class SendHelper { var messageSender = dependencies.getMessageSender(); final var address = addressResolver.resolveSignalServiceAddress(recipientId); try { - messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); - } catch (UnregisteredUserException e) { - final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); - final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); - messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message); + try { + messageSender.sendTyping(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); + } catch (UnregisteredUserException e) { + final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); + final var newAddress = addressResolver.resolveSignalServiceAddress(newRecipientId); + messageSender.sendTyping(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId), message); + } + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { + throw new UntrustedIdentityException(address); } } @@ -247,7 +269,7 @@ public class SendHelper { message, sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), () -> false); - } catch (UntrustedIdentityException e) { + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return List.of(); } } @@ -263,15 +285,17 @@ public class SendHelper { return messageSender.sendDataMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId), ContentHint.DEFAULT, - message); + message, + sendEvents); } catch (UnregisteredUserException e) { final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); return messageSender.sendDataMessage(addressResolver.resolveSignalServiceAddress(newRecipientId), unidentifiedAccessHelper.getAccessFor(newRecipientId), ContentHint.DEFAULT, - message); + message, + sendEvents); } - } catch (UntrustedIdentityException e) { + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 48dc206e..461706f0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -2,11 +2,9 @@ package org.asamk.signal.manager.helper; import org.asamk.signal.manager.AvatarStore; import org.asamk.signal.manager.TrustLevel; -import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.recipients.Contact; -import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.IOUtils; import org.slf4j.Logger; @@ -21,7 +19,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; @@ -36,7 +33,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.util.ArrayList; -import java.util.List; import java.util.stream.Collectors; public class SyncHelper { @@ -49,7 +45,6 @@ public class SyncHelper { private final GroupHelper groupHelper; private final AvatarStore avatarStore; private final SignalServiceAddressResolver addressResolver; - private final RecipientResolver recipientResolver; public SyncHelper( final SignalAccount account, @@ -57,8 +52,7 @@ public class SyncHelper { final SendHelper sendHelper, final GroupHelper groupHelper, final AvatarStore avatarStore, - final SignalServiceAddressResolver addressResolver, - final RecipientResolver recipientResolver + final SignalServiceAddressResolver addressResolver ) { this.account = account; this.attachmentHelper = attachmentHelper; @@ -66,7 +60,6 @@ public class SyncHelper { this.groupHelper = groupHelper; this.avatarStore = avatarStore; this.addressResolver = addressResolver; - this.recipientResolver = recipientResolver; } public void requestAllSyncData() throws IOException { @@ -222,48 +215,6 @@ public class SyncHelper { sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } - public void handleSyncDeviceGroups(final InputStream input) { - final var s = new DeviceGroupsInputStream(input); - DeviceGroup g; - while (true) { - try { - g = s.read(); - } catch (IOException e) { - logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage()); - continue; - } - if (g == null) { - break; - } - var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId())); - if (syncGroup != null) { - if (g.getName().isPresent()) { - syncGroup.name = g.getName().get(); - } - syncGroup.addMembers(g.getMembers() - .stream() - .map(recipientResolver::resolveRecipient) - .collect(Collectors.toSet())); - if (!g.isActive()) { - syncGroup.removeMember(account.getSelfRecipientId()); - } else { - // Add ourself to the member set as it's marked as active - syncGroup.addMembers(List.of(account.getSelfRecipientId())); - } - syncGroup.blocked = g.isBlocked(); - if (g.getColor().isPresent()) { - syncGroup.color = g.getColor().get(); - } - - if (g.getAvatar().isPresent()) { - groupHelper.downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); - } - syncGroup.archived = g.isArchived(); - account.getGroupStore().updateGroup(syncGroup); - } - } - } - public void handleSyncDeviceContacts(final InputStream input) { final var s = new DeviceContactsInputStream(input); DeviceContact c; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 477e02dc..c972c2c7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -152,7 +152,7 @@ public class SignalAccount implements Closeable { account.initStores(dataPath, identityKey, registrationId, trustNewIdentity); account.groupStore = new GroupStore(getGroupCachePath(dataPath, username), - account.recipientStore::resolveRecipient, + account.recipientStore, account::saveGroupStore); account.stickerStore = new StickerStore(account::saveStickerStore); @@ -174,9 +174,9 @@ public class SignalAccount implements Closeable { preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username)); signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username)); - sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient); + sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore); identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username), - recipientStore::resolveRecipient, + recipientStore, identityKey, registrationId, trustNewIdentity); @@ -254,7 +254,7 @@ public class SignalAccount implements Closeable { account.initStores(dataPath, identityKey, registrationId, trustNewIdentity); account.groupStore = new GroupStore(getGroupCachePath(dataPath, username), - account.recipientStore::resolveRecipient, + account.recipientStore, account::saveGroupStore); account.stickerStore = new StickerStore(account::saveStickerStore); @@ -453,12 +453,10 @@ public class SignalAccount implements Closeable { groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class); groupStore = GroupStore.fromStorage(groupStoreStorage, getGroupCachePath(dataPath, username), - recipientStore::resolveRecipient, + recipientStore, this::saveGroupStore); } else { - groupStore = new GroupStore(getGroupCachePath(dataPath, username), - recipientStore::resolveRecipient, - this::saveGroupStore); + groupStore = new GroupStore(getGroupCachePath(dataPath, username), recipientStore, this::saveGroupStore); } if (rootNode.hasNonNull("stickerStore")) { @@ -572,7 +570,7 @@ public class SignalAccount implements Closeable { var profileStoreNode = rootNode.get("profileStore"); final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class); for (var profileEntry : legacyProfileStore.getProfileEntries()) { - var recipientId = recipientStore.resolveRecipient(profileEntry.getServiceAddress()); + var recipientId = recipientStore.resolveRecipient(profileEntry.getAddress()); recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential()); recipientStore.storeProfileKey(recipientId, profileEntry.getProfileKey()); final var profile = profileEntry.getProfile(); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java b/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java index e542f4e3..e4b639a2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/Utils.java @@ -9,7 +9,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; + import java.io.InvalidObjectException; +import java.util.Optional; public class Utils { @@ -37,4 +41,12 @@ public class Utils { return node; } + + public static RecipientAddress getRecipientAddressFromIdentifier(final String identifier) { + if (UuidUtil.isUuid(identifier)) { + return new RecipientAddress(UuidUtil.parseOrThrow(identifier)); + } else { + return new RecipientAddress(Optional.empty(), Optional.of(identifier)); + } + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java index a90f87e0..6c6bd7ea 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/contacts/LegacyContactInfo.java @@ -3,7 +3,7 @@ package org.asamk.signal.manager.storage.contacts; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import java.util.UUID; @@ -42,7 +42,7 @@ public class LegacyContactInfo { } @JsonIgnore - public SignalServiceAddress getAddress() { - return new SignalServiceAddress(uuid, number); + public RecipientAddress getAddress() { + return new RecipientAddress(uuid, number); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index 59cfedb5..f86dcb04 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -9,7 +9,6 @@ import org.signal.storageservice.protos.groups.Member; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.EnabledState; import org.signal.zkgroup.groups.GroupMasterKey; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.Set; @@ -89,7 +88,7 @@ public class GroupInfoV2 extends GroupInfo { } return group.getMembersList() .stream() - .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) + .map(m -> UuidUtil.parseOrThrow(m.getUuid().toByteArray())) .map(recipientResolver::resolveRecipient) .collect(Collectors.toSet()); } @@ -101,7 +100,7 @@ public class GroupInfoV2 extends GroupInfo { } return group.getPendingMembersList() .stream() - .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) + .map(m -> UuidUtil.parseOrThrow(m.getUuid().toByteArray())) .map(recipientResolver::resolveRecipient) .collect(Collectors.toSet()); } @@ -113,7 +112,7 @@ public class GroupInfoV2 extends GroupInfo { } return group.getRequestingMembersList() .stream() - .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) + .map(m -> UuidUtil.parseOrThrow(m.getUuid().toByteArray())) .map(recipientResolver::resolveRecipient) .collect(Collectors.toSet()); } @@ -126,7 +125,7 @@ public class GroupInfoV2 extends GroupInfo { return group.getMembersList() .stream() .filter(m -> m.getRole() == Member.Role.ADMINISTRATOR) - .map(m -> new SignalServiceAddress(UuidUtil.parseOrThrow(m.getUuid().toByteArray()), null)) + .map(m -> UuidUtil.parseOrThrow(m.getUuid().toByteArray())) .map(recipientResolver::resolveRecipient) .collect(Collectors.toSet()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java index 86459c2a..4adc413a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -14,6 +14,7 @@ import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.IOUtils; @@ -22,7 +23,6 @@ import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.groups.GroupMasterKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.util.Hex; @@ -78,7 +78,7 @@ public class GroupStore { final var g1 = (Storage.GroupV1) g; final var members = g1.members.stream().map(m -> { if (m.recipientId == null) { - return recipientResolver.resolveRecipient(new SignalServiceAddress(UuidUtil.parseOrNull(m.uuid), + return recipientResolver.resolveRecipient(new RecipientAddress(UuidUtil.parseOrNull(m.uuid), m.number)); } @@ -343,17 +343,17 @@ public class GroupStore { } } - private static final class JsonSignalServiceAddress { + private static final class JsonRecipientAddress { public String uuid; public String number; // For deserialization - public JsonSignalServiceAddress() { + public JsonRecipientAddress() { } - JsonSignalServiceAddress(final String uuid, final String number) { + JsonRecipientAddress(final String uuid, final String number) { this.uuid = uuid; this.number = number; } @@ -370,7 +370,7 @@ public class GroupStore { if (address.recipientId != null) { jgen.writeNumber(address.recipientId); } else if (address.uuid != null) { - jgen.writeObject(new JsonSignalServiceAddress(address.uuid, address.number)); + jgen.writeObject(new JsonRecipientAddress(address.uuid, address.number)); } else { jgen.writeString(address.number); } @@ -393,7 +393,7 @@ public class GroupStore { } else if (n.isNumber()) { addresses.add(new Member(n.numberValue().longValue(), null, null)); } else { - var address = jsonParser.getCodec().treeToValue(n, JsonSignalServiceAddress.class); + var address = jsonParser.getCodec().treeToValue(n, JsonRecipientAddress.class); addresses.add(new Member(null, address.uuid, address.number)); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java index 0cbdf347..f24e77b1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java @@ -6,7 +6,6 @@ import org.asamk.signal.manager.TrustLevel; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.IOUtils; -import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; @@ -177,7 +176,7 @@ public class IdentityKeyStore implements org.whispersystems.libsignal.state.Iden * @param identifier can be either a serialized uuid or a e164 phone number */ private RecipientId resolveRecipient(String identifier) { - return resolver.resolveRecipient(Utils.getSignalServiceAddressFromIdentifier(identifier)); + return resolver.resolveRecipient(identifier); } private File getIdentityFile(final RecipientId recipientId) { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java index 8e1d5c88..1c6369f0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacyProfileStore.java @@ -8,10 +8,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; @@ -45,7 +45,7 @@ public class LegacyProfileStore { for (var entry : node) { var name = entry.hasNonNull("name") ? entry.get("name").asText() : null; var uuid = entry.hasNonNull("uuid") ? UuidUtil.parseOrNull(entry.get("uuid").asText()) : null; - final var serviceAddress = new SignalServiceAddress(uuid, name); + final var address = new RecipientAddress(uuid, name); ProfileKey profileKey = null; try { profileKey = new ProfileKey(Base64.getDecoder().decode(entry.get("profileKey").asText())); @@ -61,7 +61,7 @@ public class LegacyProfileStore { } var lastUpdateTimestamp = entry.get("lastUpdateTimestamp").asLong(); var profile = jsonProcessor.treeToValue(entry.get("profile"), SignalProfile.class); - profileEntries.add(new LegacySignalProfileEntry(serviceAddress, + profileEntries.add(new LegacySignalProfileEntry(address, profileKey, lastUpdateTimestamp, profile, diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java index 1e2f7ec8..03b11bcb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/LegacySignalProfileEntry.java @@ -1,12 +1,12 @@ package org.asamk.signal.manager.storage.profiles; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; public class LegacySignalProfileEntry { - private final SignalServiceAddress serviceAddress; + private final RecipientAddress address; private final ProfileKey profileKey; @@ -17,21 +17,21 @@ public class LegacySignalProfileEntry { private final ProfileKeyCredential profileKeyCredential; public LegacySignalProfileEntry( - final SignalServiceAddress serviceAddress, + final RecipientAddress address, final ProfileKey profileKey, final long lastUpdateTimestamp, final SignalProfile profile, final ProfileKeyCredential profileKeyCredential ) { - this.serviceAddress = serviceAddress; + this.address = address; this.profileKey = profileKey; this.lastUpdateTimestamp = lastUpdateTimestamp; this.profile = profile; this.profileKeyCredential = profileKeyCredential; } - public SignalServiceAddress getServiceAddress() { - return serviceAddress; + public RecipientAddress getAddress() { + return address; } public ProfileKey getProfileKey() { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java index eb66b3e5..2fa19f42 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyIdentityInfo.java @@ -1,30 +1,30 @@ package org.asamk.signal.manager.storage.protocol; import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Date; public class LegacyIdentityInfo { - SignalServiceAddress address; + RecipientAddress address; IdentityKey identityKey; TrustLevel trustLevel; Date added; - LegacyIdentityInfo(SignalServiceAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) { + LegacyIdentityInfo(RecipientAddress address, IdentityKey identityKey, TrustLevel trustLevel, Date added) { this.address = address; this.identityKey = identityKey; this.trustLevel = trustLevel; this.added = added; } - public SignalServiceAddress getAddress() { + public RecipientAddress getAddress() { return address; } - public void setAddress(final SignalServiceAddress address) { + public void setAddress(final RecipientAddress address) { this.address = address; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java index 781b6f96..5e11ab7a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonIdentityKeyStore.java @@ -6,13 +6,13 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import org.asamk.signal.manager.TrustLevel; -import org.asamk.signal.manager.util.Utils; +import org.asamk.signal.manager.storage.Utils; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; @@ -55,11 +55,11 @@ public class LegacyJsonIdentityKeyStore { return localRegistrationId; } - private LegacyIdentityInfo getIdentity(SignalServiceAddress serviceAddress) { + private LegacyIdentityInfo getIdentity(RecipientAddress address) { long maxDate = 0; LegacyIdentityInfo maxIdentity = null; for (var id : this.identities) { - if (!id.address.matches(serviceAddress)) { + if (!id.getAddress().matches(address)) { continue; } @@ -98,16 +98,16 @@ public class LegacyJsonIdentityKeyStore { var uuid = trustedKey.hasNonNull("uuid") ? UuidUtil.parseOrNull(trustedKey.get("uuid").asText()) : null; - final var serviceAddress = uuid == null - ? Utils.getSignalServiceAddressFromIdentifier(trustedKeyName) - : new SignalServiceAddress(uuid, trustedKeyName); + final var address = uuid == null + ? Utils.getRecipientAddressFromIdentifier(trustedKeyName) + : new RecipientAddress(uuid, trustedKeyName); try { var id = new IdentityKey(Base64.getDecoder().decode(trustedKey.get("identityKey").asText()), 0); var trustLevel = trustedKey.hasNonNull("trustLevel") ? TrustLevel.fromInt(trustedKey.get( "trustLevel").asInt()) : TrustLevel.TRUSTED_UNVERIFIED; var added = trustedKey.hasNonNull("addedTimestamp") ? new Date(trustedKey.get("addedTimestamp") .asLong()) : new Date(); - identities.add(new LegacyIdentityInfo(serviceAddress, id, trustLevel, added)); + identities.add(new LegacyIdentityInfo(address, id, trustLevel, added)); } catch (InvalidKeyException e) { logger.warn("Error while decoding key for {}: {}", trustedKeyName, e.getMessage()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java index 5f301aeb..9ee0cf87 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacyJsonSessionStore.java @@ -5,8 +5,8 @@ import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; -import org.asamk.signal.manager.util.Utils; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.asamk.signal.manager.storage.Utils; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; @@ -45,12 +45,12 @@ public class LegacyJsonSessionStore { } var uuid = session.hasNonNull("uuid") ? UuidUtil.parseOrNull(session.get("uuid").asText()) : null; - final var serviceAddress = uuid == null - ? Utils.getSignalServiceAddressFromIdentifier(sessionName) - : new SignalServiceAddress(uuid, sessionName); + final var address = uuid == null + ? Utils.getRecipientAddressFromIdentifier(sessionName) + : new RecipientAddress(uuid, sessionName); final var deviceId = session.get("deviceId").asInt(); final var record = Base64.getDecoder().decode(session.get("record").asText()); - var sessionInfo = new LegacySessionInfo(serviceAddress, deviceId, record); + var sessionInfo = new LegacySessionInfo(address, deviceId, record); sessions.add(sessionInfo); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java index a19bbd86..2cb984fb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/LegacySessionInfo.java @@ -1,16 +1,16 @@ package org.asamk.signal.manager.storage.protocol; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; public class LegacySessionInfo { - public SignalServiceAddress address; + public RecipientAddress address; public int deviceId; public byte[] sessionRecord; - LegacySessionInfo(final SignalServiceAddress address, final int deviceId, final byte[] sessionRecord) { + LegacySessionInfo(final RecipientAddress address, final int deviceId, final byte[] sessionRecord) { this.address = address; this.deviceId = deviceId; this.sessionRecord = sessionRecord; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java index 84923423..77eb764a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java @@ -129,6 +129,11 @@ public class SignalProtocolStore implements SignalServiceDataStore { sessionStore.archiveSession(address); } + @Override + public Set getAllAddressesWithActiveSessions(final List addressNames) { + return sessionStore.getAllAddressesWithActiveSessions(addressNames); + } + @Override public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { return signedPreKeyStore.loadSignedPreKey(signedPreKeyId); @@ -189,4 +194,11 @@ public class SignalProtocolStore implements SignalServiceDataStore { public boolean isMultiDevice() { return isMultiDevice.get(); } + + @Override + public Transaction beginTransaction() { + return () -> { + // No-op transaction should be safe, as it's only a performance improvement + }; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java index 49317157..2aeb349f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore.java @@ -7,7 +7,6 @@ import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.IOException; @@ -18,28 +17,27 @@ public class LegacyRecipientStore { @JsonProperty("recipientStore") @JsonDeserialize(using = RecipientStoreDeserializer.class) - private final List addresses = new ArrayList<>(); + private final List addresses = new ArrayList<>(); - public List getAddresses() { + public List getAddresses() { return addresses; } - public static class RecipientStoreDeserializer extends JsonDeserializer> { + public static class RecipientStoreDeserializer extends JsonDeserializer> { @Override - public List deserialize( + public List deserialize( JsonParser jsonParser, DeserializationContext deserializationContext ) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); - var addresses = new ArrayList(); + var addresses = new ArrayList(); if (node.isArray()) { for (var recipient : node) { var recipientName = recipient.get("name").asText(); var uuid = UuidUtil.parseOrThrow(recipient.get("uuid").asText()); - final var serviceAddress = new SignalServiceAddress(uuid, recipientName); - addresses.add(serviceAddress); + addresses.add(new RecipientAddress(uuid, recipientName)); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java index 3ccf8210..2d2950dc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java @@ -2,13 +2,12 @@ package org.asamk.signal.manager.storage.recipients; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; public class Recipient { private final RecipientId recipientId; - private final SignalServiceAddress address; + private final RecipientAddress address; private final Contact contact; @@ -20,7 +19,7 @@ public class Recipient { public Recipient( final RecipientId recipientId, - final SignalServiceAddress address, + final RecipientAddress address, final Contact contact, final ProfileKey profileKey, final ProfileKeyCredential profileKeyCredential, @@ -62,7 +61,7 @@ public class Recipient { return recipientId; } - public SignalServiceAddress getAddress() { + public RecipientAddress getAddress() { return address; } @@ -85,7 +84,7 @@ public class Recipient { public static final class Builder { private RecipientId recipientId; - private SignalServiceAddress address; + private RecipientAddress address; private Contact contact; private ProfileKey profileKey; private ProfileKeyCredential profileKeyCredential; @@ -99,7 +98,7 @@ public class Recipient { return this; } - public Builder withAddress(final SignalServiceAddress val) { + public Builder withAddress(final RecipientAddress val) { address = val; return this; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java new file mode 100644 index 00000000..29e964b0 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java @@ -0,0 +1,89 @@ +package org.asamk.signal.manager.storage.recipients; + +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.util.Optional; +import java.util.UUID; + +public class RecipientAddress { + + private final Optional uuid; + private final Optional e164; + + /** + * Construct a RecipientAddress. + * + * @param uuid The UUID of the user, if available. + * @param e164 The phone number of the user, if available. + */ + public RecipientAddress(Optional uuid, Optional e164) { + if (!uuid.isPresent() && !e164.isPresent()) { + throw new AssertionError("Must have either a UUID or E164 number!"); + } + + this.uuid = uuid; + this.e164 = e164; + } + + public RecipientAddress(UUID uuid, String e164) { + this(Optional.ofNullable(uuid), Optional.ofNullable(e164)); + } + + public RecipientAddress(SignalServiceAddress address) { + this.uuid = Optional.of(address.getUuid()); + this.e164 = Optional.ofNullable(address.getNumber().orNull()); + } + + public RecipientAddress(UUID uuid) { + this.uuid = Optional.of(uuid); + this.e164 = Optional.empty(); + } + + public Optional getNumber() { + return e164; + } + + public Optional getUuid() { + return uuid; + } + + public String getIdentifier() { + if (uuid.isPresent()) { + return uuid.get().toString(); + } else if (e164.isPresent()) { + return e164.get(); + } else { + throw new AssertionError("Given the checks in the constructor, this should not be possible."); + } + } + + public boolean matches(RecipientAddress other) { + return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) || ( + e164.isPresent() && other.e164.isPresent() && e164.get().equals(other.e164.get()) + ); + } + + public SignalServiceAddress toSignalServiceAddress() { + return new SignalServiceAddress(uuid.orElse(UuidUtil.UNKNOWN_UUID), + org.whispersystems.libsignal.util.guava.Optional.fromNullable(e164.orElse(null))); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final RecipientAddress that = (RecipientAddress) o; + + if (!uuid.equals(that.uuid)) return false; + return e164.equals(that.e164); + } + + @Override + public int hashCode() { + int result = uuid.hashCode(); + result = 31 * result + e164.hashCode(); + return result; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientResolver.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientResolver.java index c6d06d23..a76e5b50 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientResolver.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientResolver.java @@ -2,7 +2,15 @@ package org.asamk.signal.manager.storage.recipients; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import java.util.UUID; + public interface RecipientResolver { + RecipientId resolveRecipient(String identifier); + + RecipientId resolveRecipient(RecipientAddress address); + RecipientId resolveRecipient(SignalServiceAddress address); + + RecipientId resolveRecipient(UUID uuid); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index c8a11340..86164d58 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -12,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.ByteArrayInputStream; @@ -30,9 +31,10 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.function.Supplier; import java.util.stream.Collectors; -public class RecipientStore implements ContactsStore, ProfileStore { +public class RecipientStore implements RecipientResolver, ContactsStore, ProfileStore { private final static Logger logger = LoggerFactory.getLogger(RecipientStore.class); @@ -51,9 +53,8 @@ public class RecipientStore implements ContactsStore, ProfileStore { final var storage = objectMapper.readValue(inputStream, Storage.class); final var recipients = storage.recipients.stream().map(r -> { final var recipientId = new RecipientId(r.id); - final var address = new SignalServiceAddress(org.whispersystems.libsignal.util.guava.Optional.fromNullable( - r.uuid).transform(UuidUtil::parseOrThrow), - org.whispersystems.libsignal.util.guava.Optional.fromNullable(r.number)); + final var address = new RecipientAddress(Optional.ofNullable(r.uuid).map(UuidUtil::parseOrThrow), + Optional.ofNullable(r.number)); Contact contact = null; if (r.contact != null) { @@ -119,7 +120,7 @@ public class RecipientStore implements ContactsStore, ProfileStore { this.lastId = lastId; } - public SignalServiceAddress resolveServiceAddress(RecipientId recipientId) { + public RecipientAddress resolveRecipientAddress(RecipientId recipientId) { synchronized (recipients) { return getRecipient(recipientId).getAddress(); } @@ -134,24 +135,52 @@ public class RecipientStore implements ContactsStore, ProfileStore { } } - @Deprecated - public SignalServiceAddress resolveServiceAddress(SignalServiceAddress address) { - return resolveServiceAddress(resolveRecipient(address, false)); - } - + @Override public RecipientId resolveRecipient(UUID uuid) { - return resolveRecipient(new SignalServiceAddress(uuid, null), false); + return resolveRecipient(new RecipientAddress(uuid), false); } - public RecipientId resolveRecipient(String number) { - return resolveRecipient(new SignalServiceAddress(null, number), false); + @Override + public RecipientId resolveRecipient(final String identifier) { + return resolveRecipient(Utils.getRecipientAddressFromIdentifier(identifier), false); } - public RecipientId resolveRecipientTrusted(SignalServiceAddress address) { + public RecipientId resolveRecipient( + final String number, Supplier uuidSupplier + ) throws UnregisteredUserException { + final Optional byNumber; + synchronized (recipients) { + byNumber = findByNumberLocked(number); + } + if (byNumber.isEmpty() || byNumber.get().getAddress().getUuid().isEmpty()) { + final var uuid = uuidSupplier.get(); + if (uuid == null) { + throw new UnregisteredUserException(number, null); + } + + return resolveRecipient(new RecipientAddress(uuid, number), false); + } + return byNumber.get().getRecipientId(); + } + + public RecipientId resolveRecipient(RecipientAddress address) { + return resolveRecipient(address, false); + } + + @Override + public RecipientId resolveRecipient(final SignalServiceAddress address) { + return resolveRecipient(new RecipientAddress(address), false); + } + + public RecipientId resolveRecipientTrusted(RecipientAddress address) { return resolveRecipient(address, true); } - public List resolveRecipientsTrusted(List addresses) { + public RecipientId resolveRecipientTrusted(SignalServiceAddress address) { + return resolveRecipient(new RecipientAddress(address), true); + } + + public List resolveRecipientsTrusted(List addresses) { final List recipientIds; final List> toBeMerged = new ArrayList<>(); synchronized (recipients) { @@ -169,10 +198,6 @@ public class RecipientStore implements ContactsStore, ProfileStore { return recipientIds; } - public RecipientId resolveRecipient(SignalServiceAddress address) { - return resolveRecipient(address, false); - } - @Override public void storeContact(final RecipientId recipientId, final Contact contact) { synchronized (recipients) { @@ -262,7 +287,7 @@ public class RecipientStore implements ContactsStore, ProfileStore { * @param isHighTrust true, if the number/uuid connection was obtained from a trusted source. * Has no effect, if the address contains only a number or a uuid. */ - private RecipientId resolveRecipient(SignalServiceAddress address, boolean isHighTrust) { + private RecipientId resolveRecipient(RecipientAddress address, boolean isHighTrust) { final Pair> pair; synchronized (recipients) { pair = resolveRecipientLocked(address, isHighTrust); @@ -278,30 +303,26 @@ public class RecipientStore implements ContactsStore, ProfileStore { } private Pair> resolveRecipientLocked( - SignalServiceAddress address, boolean isHighTrust + RecipientAddress address, boolean isHighTrust ) { - final var byNumber = !address.getNumber().isPresent() + final var byNumber = address.getNumber().isEmpty() ? Optional.empty() - : findByNameLocked(address.getNumber().get()); - final var byUuid = !address.getUuid().isPresent() + : findByNumberLocked(address.getNumber().get()); + final var byUuid = address.getUuid().isEmpty() || address.getUuid().get().equals(UuidUtil.UNKNOWN_UUID) ? Optional.empty() : findByUuidLocked(address.getUuid().get()); if (byNumber.isEmpty() && byUuid.isEmpty()) { logger.debug("Got new recipient, both uuid and number are unknown"); - if (isHighTrust || !address.getUuid().isPresent() || !address.getNumber().isPresent()) { + if (isHighTrust || address.getUuid().isEmpty() || address.getNumber().isEmpty()) { return new Pair<>(addNewRecipientLocked(address), Optional.empty()); } - return new Pair<>(addNewRecipientLocked(new SignalServiceAddress(address.getUuid().get(), null)), - Optional.empty()); + return new Pair<>(addNewRecipientLocked(new RecipientAddress(address.getUuid().get())), Optional.empty()); } - if (!isHighTrust - || !address.getUuid().isPresent() - || !address.getNumber().isPresent() - || byNumber.equals(byUuid)) { + if (!isHighTrust || address.getUuid().isEmpty() || address.getNumber().isEmpty() || byNumber.equals(byUuid)) { return new Pair<>(byUuid.or(() -> byNumber).map(Recipient::getRecipientId).get(), Optional.empty()); } @@ -317,7 +338,7 @@ public class RecipientStore implements ContactsStore, ProfileStore { "Got recipient existing with number, but different uuid, so stripping its number and adding new recipient"); updateRecipientAddressLocked(byNumber.get().getRecipientId(), - new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null)); + new RecipientAddress(byNumber.get().getAddress().getUuid().get())); return new Pair<>(addNewRecipientLocked(address), Optional.empty()); } @@ -331,7 +352,7 @@ public class RecipientStore implements ContactsStore, ProfileStore { "Got separate recipients for high trust number and uuid, recipient for number has different uuid, so stripping its number"); updateRecipientAddressLocked(byNumber.get().getRecipientId(), - new SignalServiceAddress(byNumber.get().getAddress().getUuid().get(), null)); + new RecipientAddress(byNumber.get().getAddress().getUuid().get())); updateRecipientAddressLocked(byUuid.get().getRecipientId(), address); return new Pair<>(byUuid.get().getRecipientId(), Optional.empty()); } @@ -342,14 +363,14 @@ public class RecipientStore implements ContactsStore, ProfileStore { return new Pair<>(byUuid.get().getRecipientId(), byNumber.map(Recipient::getRecipientId)); } - private RecipientId addNewRecipientLocked(final SignalServiceAddress serviceAddress) { + private RecipientId addNewRecipientLocked(final RecipientAddress address) { final var nextRecipientId = nextIdLocked(); - storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, serviceAddress, null, null, null, null)); + storeRecipientLocked(nextRecipientId, new Recipient(nextRecipientId, address, null, null, null, null)); return nextRecipientId; } private void updateRecipientAddressLocked( - final RecipientId recipientId, final SignalServiceAddress address + final RecipientId recipientId, final RecipientAddress address ) { final var recipient = recipients.get(recipientId); storeRecipientLocked(recipientId, Recipient.newBuilder(recipient).withAddress(address).build()); @@ -380,7 +401,7 @@ public class RecipientStore implements ContactsStore, ProfileStore { saveLocked(); } - private Optional findByNameLocked(final String number) { + private Optional findByNumberLocked(final String number) { return recipients.entrySet() .stream() .filter(entry -> entry.getValue().getAddress().getNumber().isPresent() && number.equals(entry.getValue() @@ -431,8 +452,8 @@ public class RecipientStore implements ContactsStore, ProfileStore { .map(Enum::name) .collect(Collectors.toSet())); return new Storage.Recipient(pair.getKey().getId(), - recipient.getAddress().getNumber().orNull(), - recipient.getAddress().getUuid().transform(UUID::toString).orNull(), + recipient.getAddress().getNumber().orElse(null), + recipient.getAddress().getUuid().map(UUID::toString).orElse(null), recipient.getProfileKey() == null ? null : base64.encodeToString(recipient.getProfileKey().serialize()), diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index ef0a055b..5738408d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -3,7 +3,6 @@ package org.asamk.signal.manager.storage.sessions; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.util.IOUtils; -import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.NoSessionException; @@ -23,6 +22,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -150,6 +150,19 @@ public class SessionStore implements SignalServiceSessionStore { } } + @Override + public Set getAllAddressesWithActiveSessions(final List addressNames) { + final var recipientIdToNameMap = addressNames.stream() + .collect(Collectors.toMap(this::resolveRecipient, name -> name)); + synchronized (cachedSessions) { + return recipientIdToNameMap.keySet() + .stream() + .flatMap(recipientId -> getKeysLocked(recipientId).stream()) + .map(key -> new SignalProtocolAddress(recipientIdToNameMap.get(key.recipientId), key.getDeviceId())) + .collect(Collectors.toSet()); + } + } + public void archiveAllSessions() { synchronized (cachedSessions) { final var keys = getKeysLocked(); @@ -198,7 +211,7 @@ public class SessionStore implements SignalServiceSessionStore { * @param identifier can be either a serialized uuid or a e164 phone number */ private RecipientId resolveRecipient(String identifier) { - return resolver.resolveRecipient(Utils.getSignalServiceAddressFromIdentifier(identifier)); + return resolver.resolveRecipient(identifier); } private Key getKey(final SignalProtocolAddress address) { diff --git a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java index a2466311..3530d6ad 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/Utils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/Utils.java @@ -48,11 +48,11 @@ public class Utils { byte[] ownId; byte[] theirId; - if (isUuidCapable && ownAddress.getUuid().isPresent() && theirAddress.getUuid().isPresent()) { + if (isUuidCapable) { // Version 2: UUID user version = 2; - ownId = UuidUtil.toByteArray(ownAddress.getUuid().get()); - theirId = UuidUtil.toByteArray(theirAddress.getUuid().get()); + ownId = UuidUtil.toByteArray(ownAddress.getUuid()); + theirId = UuidUtil.toByteArray(theirAddress.getUuid()); } else { // Version 1: E164 user version = 1; @@ -69,12 +69,4 @@ public class Utils { theirId, theirIdentityKey); } - - public static SignalServiceAddress getSignalServiceAddressFromIdentifier(final String identifier) { - if (UuidUtil.isUuid(identifier)) { - return new SignalServiceAddress(UuidUtil.parseOrNull(identifier), null); - } else { - return new SignalServiceAddress(null, identifier); - } - } } diff --git a/run_tests.sh b/run_tests.sh index 4ee46845..5978eed9 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -136,8 +136,8 @@ run_main -u "$NUMBER_2" listGroups -d run_main -u "$NUMBER_2" --output=json listGroups -d run_main -u "$NUMBER_1" receive run_main -u "$NUMBER_1" updateGroup -g "$GROUP_ID" -m "$NUMBER_2" -run_main -u "$NUMBER_1" block "$GROUP_ID" -run_main -u "$NUMBER_1" unblock "$GROUP_ID" +run_main -u "$NUMBER_1" --verbose block -g "$GROUP_ID" +run_main -u "$NUMBER_1" --verbose unblock -g "$GROUP_ID" ## Identities run_main -u "$NUMBER_1" listIdentities diff --git a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java index 5ed6af00..9433d209 100644 --- a/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/JsonDbusReceiveMessageHandler.java @@ -45,7 +45,7 @@ public class JsonDbusReceiveMessageHandler extends JsonReceiveMessageHandler { e.printStackTrace(); } } else if (content != null) { - final var sender = !envelope.isUnidentifiedSender() && envelope.hasSource() + final var sender = !envelope.isUnidentifiedSender() && envelope.hasSourceUuid() ? envelope.getSourceAddress() : content.getSender(); if (content.getReceiptMessage().isPresent()) { diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 0d8f312f..96603b76 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -1,12 +1,12 @@ package org.asamk.signal; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.Util; -import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.slf4j.helpers.MessageFormatter; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceContent; @@ -38,12 +38,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { @Override public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) { - if (envelope.hasSource()) { + if (envelope.hasSourceUuid()) { var source = envelope.getSourceAddress(); writer.println("Envelope from: {} (device: {})", formatContact(source), envelope.getSourceDevice()); - if (source.getRelay().isPresent()) { - writer.println("Relayed by: {}", source.getRelay().get()); - } } else { writer.println("Envelope from: unknown source"); } @@ -56,8 +53,8 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { writer.println("Got receipt."); } else if (envelope.isSignalMessage() || envelope.isPreKeySignalMessage() || envelope.isUnidentifiedSender()) { if (exception != null) { - if (exception instanceof ProtocolUntrustedIdentityException) { - var e = (ProtocolUntrustedIdentityException) exception; + if (exception instanceof UntrustedIdentityException) { + var e = (UntrustedIdentityException) exception; writer.println( "The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message."); final var recipientName = getLegacyIdentifier(m.resolveSignalServiceAddress(e.getSender())); @@ -630,7 +627,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { private void printMention( PlainTextWriter writer, SignalServiceDataMessage.Mention mention ) { - final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null)); + final var address = m.resolveSignalServiceAddress(mention.getUuid()); writer.println("- {}: {} (length: {})", formatContact(address), mention.getStart(), mention.getLength()); } diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index 8d651592..8c1b9fb2 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -74,9 +74,14 @@ public class JoinGroupCommand implements JsonRpcLocalCommand { } catch (GroupPatchNotAcceptedException e) { throw new UserErrorException("Failed to join group, maybe already a member"); } catch (IOException e) { - throw new IOErrorException("Failed to send message: " + e.getMessage()); + throw new IOErrorException("Failed to send message: " + + e.getMessage() + + " (" + + e.getClass().getSimpleName() + + ")"); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (GroupLinkNotActiveException e) { throw new UserErrorException("Group link is not valid: " + e.getMessage()); } diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index b39f9ec9..5e609a48 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -8,7 +8,6 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.manager.Manager; -import java.util.UUID; import java.util.stream.Collectors; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -47,7 +46,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand { final var address = m.resolveSignalServiceAddress(contactPair.first()); final var contact = contactPair.second(); return new JsonContact(address.getNumber().orNull(), - address.getUuid().transform(UUID::toString).orNull(), + address.getUuid().toString(), contact.getName(), contact.isBlocked(), contact.getMessageExpirationTime()); diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index a6a9a2f1..b53577be 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -16,7 +16,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Set; -import java.util.UUID; import java.util.stream.Collectors; public class ListGroupsCommand implements JsonRpcLocalCommand { @@ -46,8 +45,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { private static Set resolveJsonMembers(Manager m, Set addresses) { return addresses.stream() .map(m::resolveSignalServiceAddress) - .map(address -> new JsonGroupMember(address.getNumber().orNull(), - address.getUuid().transform(UUID::toString).orNull())) + .map(address -> new JsonGroupMember(address.getNumber().orNull(), address.getUuid().toString())) .collect(Collectors.toSet()); } diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index c859996e..02cd1d9f 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -18,7 +18,6 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress; import java.util.Base64; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; public class ListIdentitiesCommand implements JsonRpcLocalCommand { @@ -72,7 +71,7 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(address, id.getIdentityKey())); var scannableSafetyNumber = m.computeSafetyNumberForScanning(address, id.getIdentityKey()); return new JsonIdentity(address.getNumber().orNull(), - address.getUuid().transform(UUID::toString).orNull(), + address.getUuid().toString(), Hex.toString(id.getFingerprint()), safetyNumber, scannableSafetyNumber == null diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 03bf232b..c64d19cc 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -66,7 +66,11 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { m.deleteGroup(groupId); } } catch (IOException e) { - throw new IOErrorException("Failed to send message: " + e.getMessage()); + throw new IOErrorException("Failed to send message: " + + e.getMessage() + + " (" + + e.getClass().getSimpleName() + + ")"); } catch (GroupNotFoundException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } catch (LastGroupAdminException e) { diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index e482dd58..6e1e92f7 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -64,7 +64,8 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } @@ -104,7 +105,8 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } catch (Signal.Error.GroupNotFound e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index c29d0268..7ab445fc 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -85,7 +85,8 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { m.sendEndSessionMessage(singleRecipients); return; } catch (IOException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } @@ -108,7 +109,8 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { outputResult(outputWriter, results.getTimestamp()); ErrorUtils.handleSendMessageResults(results.getResults()); } catch (AttachmentInvalidException | IOException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } @@ -141,9 +143,11 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { signal.sendEndSessionMessage(recipients); return; } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage()); + throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } @@ -182,7 +186,8 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { outputResult(outputWriter, timestamp); return; } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage()); + throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (DBusExecutionException e) { throw new UnexpectedErrorException("Failed to send note to self message: " + e.getMessage()); } @@ -194,9 +199,11 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { } catch (UnknownObject e) { throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage()); + throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 98e5f5ec..11a16b2e 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -80,7 +80,8 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } @@ -127,7 +128,8 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } catch (Signal.Error.GroupNotFound e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } diff --git a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java index 70e2f015..0d5772ec 100644 --- a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java @@ -7,8 +7,8 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.util.CommandUtil; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import java.io.IOException; @@ -51,7 +51,8 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { throw new UserErrorException("Unknown receipt type: " + type); } } catch (IOException | UntrustedIdentityException e) { - throw new UserErrorException("Failed to send message: " + e.getMessage()); + throw new UserErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } } diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index ace4da85..3a965e47 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -8,13 +8,13 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; -import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import java.io.IOException; import java.util.HashSet; @@ -59,7 +59,8 @@ public class SendTypingCommand implements JsonRpcLocalCommand { try { m.sendTypingMessage(action, recipientIdentifiers); } catch (IOException | UntrustedIdentityException e) { - throw new UserErrorException("Failed to send message: " + e.getMessage()); + throw new UserErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index a8d556f3..6df70ac2 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -174,7 +174,8 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } catch (IOException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } @@ -210,7 +211,8 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } catch (Signal.Error.AttachmentInvalid e) { throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() + .getSimpleName() + ")"); } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 6a7cc764..0bb0c435 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -21,6 +21,7 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.io.File; @@ -244,6 +245,8 @@ public class DbusSignalImpl implements Signal { m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); + } catch (UnregisteredUserException e) { + throw new Error.Failure("Contact is not registered."); } } diff --git a/src/main/java/org/asamk/signal/json/JsonMention.java b/src/main/java/org/asamk/signal/json/JsonMention.java index f0c66d00..b24768b7 100644 --- a/src/main/java/org/asamk/signal/json/JsonMention.java +++ b/src/main/java/org/asamk/signal/json/JsonMention.java @@ -4,9 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; -import org.whispersystems.signalservice.api.push.SignalServiceAddress; - -import java.util.UUID; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -29,10 +26,10 @@ public class JsonMention { final int length; JsonMention(SignalServiceDataMessage.Mention mention, Manager m) { - final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid(), null)); + final var address = m.resolveSignalServiceAddress(mention.getUuid()); this.name = getLegacyIdentifier(address); this.number = address.getNumber().orNull(); - this.uuid = address.getUuid().transform(UUID::toString).orNull(); + this.uuid = address.getUuid().toString(); this.start = mention.getStart(); this.length = mention.getLength(); } diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index 814952aa..7b884b0e 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -5,14 +5,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.Signal; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.RecipientIdentifier; -import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.util.InvalidNumberException; import java.util.List; -import java.util.UUID; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -34,10 +33,6 @@ public class JsonMessageEnvelope { @JsonProperty final Integer sourceDevice; - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - final String relay; - @JsonProperty final long timestamp; @@ -64,34 +59,30 @@ public class JsonMessageEnvelope { public JsonMessageEnvelope( SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception, Manager m ) { - if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { - var source = envelope.getSourceAddress(); + if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { + var source = m.resolveSignalServiceAddress(envelope.getSourceAddress()); this.source = getLegacyIdentifier(source); this.sourceNumber = source.getNumber().orNull(); - this.sourceUuid = source.getUuid().transform(UUID::toString).orNull(); + this.sourceUuid = source.getUuid().toString(); this.sourceDevice = envelope.getSourceDevice(); - this.relay = source.getRelay().orNull(); } else if (envelope.isUnidentifiedSender() && content != null) { - final var source = content.getSender(); + final var source = m.resolveSignalServiceAddress(content.getSender()); this.source = getLegacyIdentifier(source); this.sourceNumber = source.getNumber().orNull(); - this.sourceUuid = source.getUuid().transform(UUID::toString).orNull(); + this.sourceUuid = source.getUuid().toString(); this.sourceDevice = content.getSenderDevice(); - this.relay = null; - } else if (exception instanceof ProtocolUntrustedIdentityException) { - var e = (ProtocolUntrustedIdentityException) exception; + } else if (exception instanceof UntrustedIdentityException) { + var e = (UntrustedIdentityException) exception; final var source = m.resolveSignalServiceAddress(e.getSender()); this.source = getLegacyIdentifier(source); this.sourceNumber = source.getNumber().orNull(); - this.sourceUuid = source.getUuid().transform(UUID::toString).orNull(); + this.sourceUuid = source.getUuid().toString(); this.sourceDevice = e.getSenderDevice(); - this.relay = null; } else { this.source = null; this.sourceNumber = null; this.sourceUuid = null; this.sourceDevice = null; - this.relay = null; } String name; try { @@ -129,7 +120,6 @@ public class JsonMessageEnvelope { sourceUuid = null; sourceName = null; sourceDevice = null; - relay = null; timestamp = messageReceived.getTimestamp(); receiptMessage = null; dataMessage = new JsonDataMessage(messageReceived); @@ -144,7 +134,6 @@ public class JsonMessageEnvelope { sourceUuid = null; sourceName = null; sourceDevice = null; - relay = null; timestamp = receiptReceived.getTimestamp(); receiptMessage = JsonReceiptMessage.deliveryReceipt(timestamp, List.of(timestamp)); dataMessage = null; @@ -159,7 +148,6 @@ public class JsonMessageEnvelope { sourceUuid = null; sourceName = null; sourceDevice = null; - relay = null; timestamp = messageReceived.getTimestamp(); receiptMessage = null; dataMessage = null; diff --git a/src/main/java/org/asamk/signal/json/JsonQuote.java b/src/main/java/org/asamk/signal/json/JsonQuote.java index ecd31c1a..73af895a 100644 --- a/src/main/java/org/asamk/signal/json/JsonQuote.java +++ b/src/main/java/org/asamk/signal/json/JsonQuote.java @@ -8,7 +8,6 @@ import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import java.util.ArrayList; import java.util.List; -import java.util.UUID; import java.util.stream.Collectors; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -43,7 +42,7 @@ public class JsonQuote { final var address = m.resolveSignalServiceAddress(quote.getAuthor()); this.author = getLegacyIdentifier(address); this.authorNumber = address.getNumber().orNull(); - this.authorUuid = address.getUuid().transform(UUID::toString).orNull(); + this.authorUuid = address.getUuid().toString(); this.text = quote.getText(); if (quote.getMentions() != null && quote.getMentions().size() > 0) { diff --git a/src/main/java/org/asamk/signal/json/JsonReaction.java b/src/main/java/org/asamk/signal/json/JsonReaction.java index ecea15fe..cc80ee84 100644 --- a/src/main/java/org/asamk/signal/json/JsonReaction.java +++ b/src/main/java/org/asamk/signal/json/JsonReaction.java @@ -5,8 +5,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.Reaction; -import java.util.UUID; - import static org.asamk.signal.util.Util.getLegacyIdentifier; public class JsonReaction { @@ -35,7 +33,7 @@ public class JsonReaction { final var address = m.resolveSignalServiceAddress(reaction.getTargetAuthor()); this.targetAuthor = getLegacyIdentifier(address); this.targetAuthorNumber = address.getNumber().orNull(); - this.targetAuthorUuid = address.getUuid().transform(UUID::toString).orNull(); + this.targetAuthorUuid = address.getUuid().toString(); this.targetSentTimestamp = reaction.getTargetSentTimestamp(); this.isRemove = reaction.isRemove(); } diff --git a/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java index 28c9d936..e2c92bac 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncDataMessage.java @@ -6,8 +6,6 @@ import org.asamk.Signal; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; -import java.util.UUID; - import static org.asamk.signal.util.Util.getLegacyIdentifier; class JsonSyncDataMessage extends JsonDataMessage { @@ -29,7 +27,7 @@ class JsonSyncDataMessage extends JsonDataMessage { final var address = transcriptMessage.getDestination().get(); this.destination = getLegacyIdentifier(address); this.destinationNumber = address.getNumber().orNull(); - this.destinationUuid = address.getUuid().transform(UUID::toString).orNull(); + this.destinationUuid = address.getUuid().toString(); } else { this.destination = null; this.destinationNumber = null; diff --git a/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java b/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java index df307b45..042ed7e4 100644 --- a/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java +++ b/src/main/java/org/asamk/signal/json/JsonSyncReadMessage.java @@ -4,8 +4,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; -import java.util.UUID; - import static org.asamk.signal.util.Util.getLegacyIdentifier; class JsonSyncReadMessage { @@ -27,7 +25,7 @@ class JsonSyncReadMessage { final var sender = readMessage.getSender(); this.sender = getLegacyIdentifier(sender); this.senderNumber = sender.getNumber().orNull(); - this.senderUuid = sender.getUuid().transform(UUID::toString).orNull(); + this.senderUuid = sender.getUuid().toString(); this.timestamp = readMessage.getTimestamp(); } } diff --git a/src/main/java/org/asamk/signal/util/Util.java b/src/main/java/org/asamk/signal/util/Util.java index 01a79dd1..b4954bf3 100644 --- a/src/main/java/org/asamk/signal/util/Util.java +++ b/src/main/java/org/asamk/signal/util/Util.java @@ -60,7 +60,7 @@ public class Util { } public static String getLegacyIdentifier(final SignalServiceAddress address) { - return address.getNumber().or(() -> address.getUuid().get().toString()); + return address.getNumber().or(() -> address.getUuid().toString()); } public static ObjectMapper createJsonObjectMapper() { From 5743cf4455d98ac16231866c43c877c10c016a89 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 30 Aug 2021 13:33:54 +0200 Subject: [PATCH 0779/2005] Improve dbus register error message if called with invalid number --- .../java/org/asamk/signal/manager/storage/SignalAccount.java | 3 ++- src/main/java/org/asamk/signal/BaseConfig.java | 2 +- .../java/org/asamk/signal/dbus/DbusSignalControlImpl.java | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index c972c2c7..4e240887 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -21,6 +21,7 @@ import org.asamk.signal.manager.storage.protocol.SignalProtocolStore; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore; import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientStore; import org.asamk.signal.manager.storage.sessions.SessionStore; @@ -789,7 +790,7 @@ public class SignalAccount implements Closeable { } public RecipientId getSelfRecipientId() { - return recipientStore.resolveRecipientTrusted(getSelfAddress()); + return recipientStore.resolveRecipientTrusted(new RecipientAddress(uuid, username)); } public String getEncryptedDeviceName() { diff --git a/src/main/java/org/asamk/signal/BaseConfig.java b/src/main/java/org/asamk/signal/BaseConfig.java index bb8db7d2..04c1ac8a 100644 --- a/src/main/java/org/asamk/signal/BaseConfig.java +++ b/src/main/java/org/asamk/signal/BaseConfig.java @@ -5,7 +5,7 @@ public class BaseConfig { public final static String PROJECT_NAME = BaseConfig.class.getPackage().getImplementationTitle(); public final static String PROJECT_VERSION = BaseConfig.class.getPackage().getImplementationVersion(); - final static String USER_AGENT_SIGNAL_ANDROID = "Signal-Android/5.12.4"; + final static String USER_AGENT_SIGNAL_ANDROID = "Signal-Android/5.22.3"; final static String USER_AGENT_SIGNAL_CLI = PROJECT_NAME == null ? "signal-cli" : PROJECT_NAME + "/" + PROJECT_VERSION; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java index 35f530b0..6ec8d964 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -13,6 +13,7 @@ import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.KeyBackupServicePinException; import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException; import org.whispersystems.signalservice.api.push.exceptions.CaptchaRequiredException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.IOException; import java.net.URI; @@ -99,6 +100,10 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { public void registerWithCaptcha( final String number, final boolean voiceVerification, final String captcha ) throws Error.Failure, Error.InvalidNumber { + if (!PhoneNumberFormatter.isValidNumber(number, null)) { + throw new SignalControl.Error.InvalidNumber( + "Invalid username (phone number), make sure you include the country code."); + } try (final RegistrationManager registrationManager = c.getNewRegistrationManager(number)) { registrationManager.register(voiceVerification, captcha); } catch (CaptchaRequiredException e) { From 32150b1aaa32888c5179d664a9a497a13d3f4bfa Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 30 Aug 2021 13:39:27 +0200 Subject: [PATCH 0780/2005] Move all message decryption to IncomingMessageHandler --- .../org/asamk/signal/manager/Manager.java | 47 +++++++-------- .../helper/IncomingMessageHandler.java | 59 +++++++++++++++---- 2 files changed, 68 insertions(+), 38 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 9e38853b..87b89913 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -58,7 +58,6 @@ import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; -import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; @@ -818,37 +817,33 @@ public class Manager implements Closeable { ) { var envelope = cachedMessage.loadEnvelope(); if (envelope == null) { + cachedMessage.delete(); return null; } - SignalServiceContent content = null; - List actions = null; - if (!envelope.isReceipt()) { - try { - content = dependencies.getCipher().decrypt(envelope); - } catch (ProtocolUntrustedIdentityException e) { - if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { - // Envelope is more than a month old, cleaning up. - cachedMessage.delete(); - return null; - } - if (!envelope.hasSourceUuid()) { - final var identifier = e.getSender(); - final var recipientId = account.getRecipientStore().resolveRecipient(identifier); - try { - account.getMessageCache().replaceSender(cachedMessage, recipientId); - } catch (IOException ioException) { - logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage()); - } - } - return null; - } catch (Exception er) { - // All other errors are not recoverable, so delete the cached message + + final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler); + final var actions = result.first(); + final var exception = result.second(); + + if (exception instanceof UntrustedIdentityException) { + if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { + // Envelope is more than a month old, cleaning up. cachedMessage.delete(); return null; } - actions = incomingMessageHandler.handleMessage(envelope, content, ignoreAttachments); + if (!envelope.hasSourceUuid()) { + final var identifier = ((UntrustedIdentityException) exception).getSender(); + final var recipientId = account.getRecipientStore().resolveRecipient(identifier); + try { + account.getMessageCache().replaceSender(cachedMessage, recipientId); + } catch (IOException ioException) { + logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage()); + } + } + return null; } - handler.handleMessage(envelope, content, null); + + // If successful and for all other errors that are not recoverable, delete the cached message cachedMessage.delete(); return actions; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index f28b3638..57b71ee1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -79,6 +79,28 @@ public final class IncomingMessageHandler { this.jobExecutor = jobExecutor; } + public Pair, Exception> handleRetryEnvelope( + final SignalServiceEnvelope envelope, + final boolean ignoreAttachments, + final Manager.ReceiveMessageHandler handler + ) { + SignalServiceContent content = null; + if (!envelope.isReceipt()) { + try { + content = dependencies.getCipher().decrypt(envelope); + } catch (ProtocolUntrustedIdentityException e) { + final var recipientId = account.getRecipientStore().resolveRecipient(e.getSender()); + final var exception = new UntrustedIdentityException(addressResolver.resolveSignalServiceAddress( + recipientId), e.getSenderDevice()); + return new Pair<>(List.of(), exception); + } catch (Exception e) { + return new Pair<>(List.of(), e); + } + } + final var actions = checkAndHandleMessage(envelope, content, ignoreAttachments, handler, null); + return new Pair<>(actions, null); + } + public Pair, Exception> handleEnvelope( final SignalServiceEnvelope envelope, final boolean ignoreAttachments, @@ -108,35 +130,48 @@ public final class IncomingMessageHandler { } catch (Exception e) { exception = e; } - - if (!envelope.hasSourceUuid() && content != null) { - // Store uuid if we don't have it already - // address/uuid is validated by unidentified sender certificate - account.getRecipientStore().resolveRecipientTrusted(content.getSender()); - } } + actions.addAll(checkAndHandleMessage(envelope, content, ignoreAttachments, handler, exception)); + return new Pair<>(actions, exception); + } + + private List checkAndHandleMessage( + final SignalServiceEnvelope envelope, + final SignalServiceContent content, + final boolean ignoreAttachments, + final Manager.ReceiveMessageHandler handler, + final Exception exception + ) { + if (!envelope.hasSourceUuid() && content != null) { + // Store uuid if we don't have it already + // address/uuid is validated by unidentified sender certificate + account.getRecipientStore().resolveRecipientTrusted(content.getSender()); + } if (isMessageBlocked(envelope, content)) { logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); + return List.of(); } else if (isNotAllowedToSendToGroup(envelope, content)) { logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", (envelope.hasSourceUuid() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), envelope.getTimestamp()); + return List.of(); } else { - actions.addAll(handleMessage(envelope, content, ignoreAttachments)); + List actions; + if (content != null) { + actions = handleMessage(envelope, content, ignoreAttachments); + } else { + actions = List.of(); + } handler.handleMessage(envelope, content, exception); + return actions; } - return new Pair<>(actions, exception); } public List handleMessage( SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments ) { var actions = new ArrayList(); - if (content == null) { - return actions; - } - final RecipientId sender; if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); From 7a3522dc010fd15601b040547807c617276ec6c2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 30 Aug 2021 13:55:25 +0200 Subject: [PATCH 0781/2005] Prevent endless loop when receiving contact sync message --- .../java/org/asamk/signal/manager/helper/SyncHelper.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 461706f0..3cc76b28 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -222,8 +222,13 @@ public class SyncHelper { try { c = s.read(); } catch (IOException e) { - logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage()); - continue; + if (e.getMessage() != null && e.getMessage().contains("Missing contact address!")) { + logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage()); + continue; + } else { + logger.warn("Failed to read sync contacts", e); + break; + } } if (c == null) { break; From 1f0c2d5c782d2d2663dc8b4147b1fbb8df53bc97 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 30 Aug 2021 14:12:39 +0200 Subject: [PATCH 0782/2005] Remove registration lock pin before deleting account --- lib/src/main/java/org/asamk/signal/manager/Manager.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 87b89913..6d91ba11 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -385,6 +385,13 @@ public class Manager implements Closeable { } public void deleteAccount() throws IOException { + try { + pinHelper.removeRegistrationLockPin(); + } catch (UnauthenticatedResponseException e) { + logger.warn("Failed to remove registration lock pin"); + } + account.setRegistrationLockPin(null, null); + dependencies.getAccountManager().deleteAccount(); account.setRegistered(false); From 626406a43c169105c3485749249ffbfd715e3441 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 30 Aug 2021 15:07:12 +0200 Subject: [PATCH 0783/2005] Create libsignal dependencies only when required --- .../org/asamk/signal/manager/Manager.java | 12 +- .../signal/manager/SignalDependencies.java | 202 +++++++++++------- .../helper/MessageReceiverProvider.java | 8 - .../signal/manager/helper/ProfileHelper.java | 10 +- .../helper/ProfileServiceProvider.java | 8 - .../asamk/signal/manager/jobs/Context.java | 20 +- .../manager/jobs/RetrieveStickerPackJob.java | 8 +- 7 files changed, 138 insertions(+), 130 deletions(-) delete mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/MessageReceiverProvider.java delete mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/ProfileServiceProvider.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 6d91ba11..c40fa7cd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -164,8 +164,7 @@ public class Manager implements Closeable { return LEGACY_LOCK::unlock; } }; - this.dependencies = new SignalDependencies(account.getSelfAddress(), - serviceEnvironmentConfig, + this.dependencies = new SignalDependencies(serviceEnvironmentConfig, userAgent, credentialsProvider, account.getSignalProtocolStore(), @@ -186,8 +185,6 @@ public class Manager implements Closeable { avatarStore, account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, - dependencies::getProfileService, - dependencies::getMessageReceiver, this::resolveSignalServiceAddress); final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, this::getRecipientProfile, @@ -220,8 +217,7 @@ public class Manager implements Closeable { this::resolveSignalServiceAddress); this.context = new Context(account, - dependencies.getAccountManager(), - dependencies.getMessageReceiver(), + dependencies, stickerPackStore, sendHelper, groupHelper, @@ -1149,10 +1145,6 @@ public class Manager implements Closeable { } public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { - if (address.matches(account.getSelfAddress())) { - return account.getSelfAddress(); - } - return resolveSignalServiceAddress(resolveRecipient(address)); } diff --git a/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java index fef8351f..970a6741 100644 --- a/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java @@ -3,7 +3,6 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.signal.libsignal.metadata.certificate.CertificateValidator; -import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.KeyBackupService; import org.whispersystems.signalservice.api.SignalServiceAccountManager; @@ -18,32 +17,41 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.services.ProfileService; -import org.whispersystems.signalservice.api.util.SleepTimer; import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.websocket.WebSocketFactory; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.websocket.WebSocketConnection; import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; import static org.asamk.signal.manager.config.ServiceConfig.capabilities; public class SignalDependencies { - private final SignalServiceAccountManager accountManager; - private final GroupsV2Api groupsV2Api; - private final GroupsV2Operations groupsV2Operations; + private final Object LOCK = new Object(); - private final SignalWebSocket signalWebSocket; - private final SignalServiceMessageReceiver messageReceiver; - private final SignalServiceMessageSender messageSender; + private final ServiceEnvironmentConfig serviceEnvironmentConfig; + private final String userAgent; + private final DynamicCredentialsProvider credentialsProvider; + private final SignalServiceDataStore dataStore; + private final ExecutorService executor; + private final SignalSessionLock sessionLock; - private final KeyBackupService keyBackupService; - private final ProfileService profileService; - private final SignalServiceCipher cipher; + private SignalServiceAccountManager accountManager; + private GroupsV2Api groupsV2Api; + private GroupsV2Operations groupsV2Operations; + private ClientZkOperations clientZkOperations; + + private SignalWebSocket signalWebSocket; + private SignalServiceMessageReceiver messageReceiver; + private SignalServiceMessageSender messageSender; + + private KeyBackupService keyBackupService; + private ProfileService profileService; + private SignalServiceCipher cipher; public SignalDependencies( - final SignalServiceAddress selfAddress, final ServiceEnvironmentConfig serviceEnvironmentConfig, final String userAgent, final DynamicCredentialsProvider credentialsProvider, @@ -51,100 +59,134 @@ public class SignalDependencies { final ExecutorService executor, final SignalSessionLock sessionLock ) { - this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create( - serviceEnvironmentConfig.getSignalServiceConfiguration())) : null; - final SleepTimer timer = new UptimeSleepTimer(); - this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(), - credentialsProvider, - userAgent, - groupsV2Operations, - ServiceConfig.AUTOMATIC_NETWORK_RETRY); - this.groupsV2Api = accountManager.getGroupsV2Api(); - this.keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(), - serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(), - serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(), - serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(), - 10); - final ClientZkProfileOperations clientZkProfileOperations = capabilities.isGv2() ? ClientZkOperations.create( - serviceEnvironmentConfig.getSignalServiceConfiguration()).getProfileOperations() : null; - this.messageReceiver = new SignalServiceMessageReceiver(serviceEnvironmentConfig.getSignalServiceConfiguration(), - credentialsProvider, - userAgent, - clientZkProfileOperations, - ServiceConfig.AUTOMATIC_NETWORK_RETRY); - - final var healthMonitor = new SignalWebSocketHealthMonitor(timer); - final WebSocketFactory webSocketFactory = new WebSocketFactory() { - @Override - public WebSocketConnection createWebSocket() { - return new WebSocketConnection("normal", - serviceEnvironmentConfig.getSignalServiceConfiguration(), - Optional.of(credentialsProvider), - userAgent, - healthMonitor); - } - - @Override - public WebSocketConnection createUnidentifiedWebSocket() { - return new WebSocketConnection("unidentified", - serviceEnvironmentConfig.getSignalServiceConfiguration(), - Optional.absent(), - userAgent, - healthMonitor); - } - }; - this.signalWebSocket = new SignalWebSocket(webSocketFactory); - healthMonitor.monitor(signalWebSocket); - this.profileService = new ProfileService(clientZkProfileOperations, messageReceiver, signalWebSocket); - - final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.getUnidentifiedSenderTrustRoot()); - this.cipher = new SignalServiceCipher(selfAddress, dataStore, sessionLock, certificateValidator); - this.messageSender = new SignalServiceMessageSender(serviceEnvironmentConfig.getSignalServiceConfiguration(), - credentialsProvider, - dataStore, - sessionLock, - userAgent, - signalWebSocket, - Optional.absent(), - clientZkProfileOperations, - executor, - ServiceConfig.MAX_ENVELOPE_SIZE, - ServiceConfig.AUTOMATIC_NETWORK_RETRY); + this.serviceEnvironmentConfig = serviceEnvironmentConfig; + this.userAgent = userAgent; + this.credentialsProvider = credentialsProvider; + this.dataStore = dataStore; + this.executor = executor; + this.sessionLock = sessionLock; } public SignalServiceAccountManager getAccountManager() { - return accountManager; + return getOrCreate(() -> accountManager, + () -> accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(), + credentialsProvider, + userAgent, + getGroupsV2Operations(), + ServiceConfig.AUTOMATIC_NETWORK_RETRY)); } public GroupsV2Api getGroupsV2Api() { - return groupsV2Api; + return getOrCreate(() -> groupsV2Api, () -> groupsV2Api = getAccountManager().getGroupsV2Api()); } public GroupsV2Operations getGroupsV2Operations() { - return groupsV2Operations; + return getOrCreate(() -> groupsV2Operations, + () -> groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create( + serviceEnvironmentConfig.getSignalServiceConfiguration())) : null); + } + + private ClientZkOperations getClientZkOperations() { + return getOrCreate(() -> clientZkOperations, + () -> clientZkOperations = capabilities.isGv2() + ? ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()) + : null); } public SignalWebSocket getSignalWebSocket() { - return signalWebSocket; + return getOrCreate(() -> signalWebSocket, () -> { + final var timer = new UptimeSleepTimer(); + final var healthMonitor = new SignalWebSocketHealthMonitor(timer); + final var webSocketFactory = new WebSocketFactory() { + @Override + public WebSocketConnection createWebSocket() { + return new WebSocketConnection("normal", + serviceEnvironmentConfig.getSignalServiceConfiguration(), + Optional.of(credentialsProvider), + userAgent, + healthMonitor); + } + + @Override + public WebSocketConnection createUnidentifiedWebSocket() { + return new WebSocketConnection("unidentified", + serviceEnvironmentConfig.getSignalServiceConfiguration(), + Optional.absent(), + userAgent, + healthMonitor); + } + }; + signalWebSocket = new SignalWebSocket(webSocketFactory); + healthMonitor.monitor(signalWebSocket); + }); } public SignalServiceMessageReceiver getMessageReceiver() { - return messageReceiver; + return getOrCreate(() -> messageReceiver, + () -> messageReceiver = new SignalServiceMessageReceiver(serviceEnvironmentConfig.getSignalServiceConfiguration(), + credentialsProvider, + userAgent, + getClientZkOperations().getProfileOperations(), + ServiceConfig.AUTOMATIC_NETWORK_RETRY)); } public SignalServiceMessageSender getMessageSender() { - return messageSender; + return getOrCreate(() -> messageSender, + () -> messageSender = new SignalServiceMessageSender(serviceEnvironmentConfig.getSignalServiceConfiguration(), + credentialsProvider, + dataStore, + sessionLock, + userAgent, + getSignalWebSocket(), + Optional.absent(), + getClientZkOperations().getProfileOperations(), + executor, + ServiceConfig.MAX_ENVELOPE_SIZE, + ServiceConfig.AUTOMATIC_NETWORK_RETRY)); } public KeyBackupService getKeyBackupService() { - return keyBackupService; + return getOrCreate(() -> keyBackupService, + () -> keyBackupService = getAccountManager().getKeyBackupService(ServiceConfig.getIasKeyStore(), + serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(), + serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(), + serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(), + 10)); } public ProfileService getProfileService() { - return profileService; + return getOrCreate(() -> profileService, + () -> profileService = new ProfileService(getClientZkOperations().getProfileOperations(), + getMessageReceiver(), + getSignalWebSocket())); } public SignalServiceCipher getCipher() { - return cipher; + return getOrCreate(() -> cipher, () -> { + final var certificateValidator = new CertificateValidator(serviceEnvironmentConfig.getUnidentifiedSenderTrustRoot()); + final var address = new SignalServiceAddress(credentialsProvider.getUuid(), credentialsProvider.getE164()); + cipher = new SignalServiceCipher(address, dataStore, sessionLock, certificateValidator); + }); + } + + private T getOrCreate(Supplier supplier, Callable creator) { + var value = supplier.get(); + if (value != null) { + return value; + } + + synchronized (LOCK) { + value = supplier.get(); + if (value != null) { + return value; + } + creator.call(); + return supplier.get(); + } + } + + private interface Callable { + + void call(); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/MessageReceiverProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/MessageReceiverProvider.java deleted file mode 100644 index 9a18a5e4..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/helper/MessageReceiverProvider.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.asamk.signal.manager.helper; - -import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; - -public interface MessageReceiverProvider { - - SignalServiceMessageReceiver getMessageReceiver(); -} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 52154798..d4f8ae5d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -45,8 +45,6 @@ public final class ProfileHelper { private final AvatarStore avatarStore; private final ProfileKeyProvider profileKeyProvider; private final UnidentifiedAccessProvider unidentifiedAccessProvider; - private final ProfileServiceProvider profileServiceProvider; - private final MessageReceiverProvider messageReceiverProvider; private final SignalServiceAddressResolver addressResolver; public ProfileHelper( @@ -55,8 +53,6 @@ public final class ProfileHelper { final AvatarStore avatarStore, final ProfileKeyProvider profileKeyProvider, final UnidentifiedAccessProvider unidentifiedAccessProvider, - final ProfileServiceProvider profileServiceProvider, - final MessageReceiverProvider messageReceiverProvider, final SignalServiceAddressResolver addressResolver ) { this.account = account; @@ -64,8 +60,6 @@ public final class ProfileHelper { this.avatarStore = avatarStore; this.profileKeyProvider = profileKeyProvider; this.unidentifiedAccessProvider = unidentifiedAccessProvider; - this.profileServiceProvider = profileServiceProvider; - this.messageReceiverProvider = messageReceiverProvider; this.addressResolver = addressResolver; } @@ -218,7 +212,7 @@ public final class ProfileHelper { } private SignalServiceProfile retrieveProfileSync(String username) throws IOException { - return messageReceiverProvider.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent()); + return dependencies.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent()); } private ProfileAndCredential retrieveProfileAndCredential( @@ -287,7 +281,7 @@ public final class ProfileHelper { Optional unidentifiedAccess, SignalServiceProfile.RequestType requestType ) { - var profileService = profileServiceProvider.getProfileService(); + var profileService = dependencies.getProfileService(); Single> responseSingle; try { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileServiceProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileServiceProvider.java deleted file mode 100644 index 4fffb15c..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileServiceProvider.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.asamk.signal.manager.helper; - -import org.whispersystems.signalservice.api.services.ProfileService; - -public interface ProfileServiceProvider { - - ProfileService getProfileService(); -} diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java index 82c3bf16..142c148a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java @@ -1,19 +1,17 @@ package org.asamk.signal.manager.jobs; +import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.StickerPackStore; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.storage.SignalAccount; -import org.whispersystems.signalservice.api.SignalServiceAccountManager; -import org.whispersystems.signalservice.api.SignalServiceMessageReceiver; public class Context { private final SignalAccount account; - private final SignalServiceAccountManager accountManager; - private final SignalServiceMessageReceiver messageReceiver; + private final SignalDependencies dependencies; private final StickerPackStore stickerPackStore; private final SendHelper sendHelper; private final GroupHelper groupHelper; @@ -22,8 +20,7 @@ public class Context { public Context( final SignalAccount account, - final SignalServiceAccountManager accountManager, - final SignalServiceMessageReceiver messageReceiver, + final SignalDependencies dependencies, final StickerPackStore stickerPackStore, final SendHelper sendHelper, final GroupHelper groupHelper, @@ -31,8 +28,7 @@ public class Context { final ProfileHelper profileHelper ) { this.account = account; - this.accountManager = accountManager; - this.messageReceiver = messageReceiver; + this.dependencies = dependencies; this.stickerPackStore = stickerPackStore; this.sendHelper = sendHelper; this.groupHelper = groupHelper; @@ -44,12 +40,8 @@ public class Context { return account; } - public SignalServiceAccountManager getAccountManager() { - return accountManager; - } - - public SignalServiceMessageReceiver getMessageReceiver() { - return messageReceiver; + public SignalDependencies getDependencies() { + return dependencies; } public StickerPackStore getStickerPackStore() { diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java b/lib/src/main/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java index 20042451..c27bcafc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/RetrieveStickerPackJob.java @@ -32,7 +32,9 @@ public class RetrieveStickerPackJob implements Job { } logger.debug("Retrieving sticker pack {}.", Hex.toStringCondensed(packId.serialize())); try { - final var manifest = context.getMessageReceiver().retrieveStickerManifest(packId.serialize(), packKey); + final var manifest = context.getDependencies() + .getMessageReceiver() + .retrieveStickerManifest(packId.serialize(), packKey); final var stickerIds = new HashSet(); if (manifest.getCover().isPresent()) { @@ -43,7 +45,9 @@ public class RetrieveStickerPackJob implements Job { } for (var id : stickerIds) { - final var inputStream = context.getMessageReceiver().retrieveSticker(packId.serialize(), packKey, id); + final var inputStream = context.getDependencies() + .getMessageReceiver() + .retrieveSticker(packId.serialize(), packKey, id); context.getStickerPackStore().storeSticker(packId, id, o -> IOUtils.copyStream(inputStream, o)); } From 0d0978011dac492ac1df3d2440ea0689b1e7f3e3 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 30 Aug 2021 21:07:24 +0200 Subject: [PATCH 0784/2005] Fix handling incoming contacts sync message --- .../org/asamk/signal/manager/helper/AttachmentHelper.java | 6 +++--- .../java/org/asamk/signal/manager/helper/SyncHelper.java | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java index d3931955..449a575e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/AttachmentHelper.java @@ -88,9 +88,9 @@ public class AttachmentHelper { SignalServiceAttachment attachment, AttachmentHandler consumer ) throws IOException { if (attachment.isStream()) { - try (var input = attachment.asStream().getInputStream()) { - consumer.handle(input); - } + var input = attachment.asStream().getInputStream(); + // don't close input stream here, it might be reused later (e.g. with contact sync messages ...) + consumer.handle(input); return; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 3cc76b28..bcdf6ab1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -215,7 +215,7 @@ public class SyncHelper { sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } - public void handleSyncDeviceContacts(final InputStream input) { + public void handleSyncDeviceContacts(final InputStream input) throws IOException { final var s = new DeviceContactsInputStream(input); DeviceContact c; while (true) { @@ -226,8 +226,7 @@ public class SyncHelper { logger.warn("Sync contacts contained invalid contact, ignoring: {}", e.getMessage()); continue; } else { - logger.warn("Failed to read sync contacts", e); - break; + throw e; } } if (c == null) { From e83bfb9e037d5a55d24b6f6f83efe27edb8dd27e Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 1 Sep 2021 20:02:20 +0200 Subject: [PATCH 0785/2005] Print more information for call messages --- .../java/org/asamk/signal/ReceiveMessageHandler.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 96603b76..4a516197 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -235,6 +235,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final var deviceId = callMessage.getDestinationDeviceId().get(); writer.println("Destination device id: {}", deviceId); } + if (callMessage.getGroupId().isPresent()) { + final var groupId = GroupId.unknownVersion(callMessage.getGroupId().get()); + writer.println("Destination group id: {}", groupId); + } + if (callMessage.getTimestamp().isPresent()) { + writer.println("Timestamp: {}", DateUtils.formatTimestamp(callMessage.getTimestamp().get())); + } if (callMessage.getAnswerMessage().isPresent()) { var answerMessage = callMessage.getAnswerMessage().get(); writer.println("Answer message: {}, sdp: {})", answerMessage.getId(), answerMessage.getSdp()); @@ -260,7 +267,9 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } if (callMessage.getOpaqueMessage().isPresent()) { final var opaqueMessage = callMessage.getOpaqueMessage().get(); - writer.println("Opaque message: size {}", opaqueMessage.getOpaque().length); + writer.println("Opaque message: size {}, urgency: {}", + opaqueMessage.getOpaque().length, + opaqueMessage.getUrgency().name()); } } From b9031024078d00e89e7a3462665972dbdf52951b Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Sep 2021 20:12:59 +0200 Subject: [PATCH 0786/2005] Update libsignal-service-java --- graalvm-config-dir/reflect-config.json | 1 - lib/build.gradle.kts | 2 +- .../signal/manager/RegistrationManager.java | 77 +++++++++++++------ .../signal/manager/config/SandboxConfig.java | 2 +- .../signal/manager/helper/SendHelper.java | 11 +++ 5 files changed, 66 insertions(+), 27 deletions(-) diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index aef740aa..2db7538b 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -2272,7 +2272,6 @@ "fields":[ {"name":"bitField0_"}, {"name":"e164_"}, - {"name":"relay_"}, {"name":"uuid_"} ] }, diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 316ce564..dc6c910e 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -14,7 +14,7 @@ repositories { } dependencies { - api("com.github.turasa:signal-service-java:2.15.3_unofficial_26") + api("com.github.turasa:signal-service-java:2.15.3_unofficial_27") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.slf4j:slf4j-api:1.7.30") diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 95d43fd6..2be3f719 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -35,7 +35,9 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.ServiceResponse; import org.whispersystems.signalservice.internal.push.LockedException; +import org.whispersystems.signalservice.internal.push.RequestVerificationCodeResponse; import org.whispersystems.signalservice.internal.push.VerifyAccountResponse; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; @@ -115,13 +117,19 @@ public class RegistrationManager implements Closeable { } public void register(boolean voiceVerification, String captcha) throws IOException { + final ServiceResponse response; if (voiceVerification) { - accountManager.requestVoiceVerificationCode(getDefaultLocale(), + response = accountManager.requestVoiceVerificationCode(getDefaultLocale(), Optional.fromNullable(captcha), + Optional.absent(), Optional.absent()); } else { - accountManager.requestSmsVerificationCode(false, Optional.fromNullable(captcha), Optional.absent()); + response = accountManager.requestSmsVerificationCode(false, + Optional.fromNullable(captcha), + Optional.absent(), + Optional.absent()); } + handleResponseException(response); } private Locale getDefaultLocale() { @@ -143,7 +151,7 @@ public class RegistrationManager implements Closeable { VerifyAccountResponse response; MasterKey masterKey; try { - response = verifyAccountWithCode(verificationCode, null, null); + response = verifyAccountWithCode(verificationCode, null); masterKey = null; pin = null; @@ -154,17 +162,16 @@ public class RegistrationManager implements Closeable { var registrationLockData = pinHelper.getRegistrationLockData(pin, e); if (registrationLockData == null) { - response = verifyAccountWithCode(verificationCode, pin, null); - masterKey = null; - } else { - var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); - try { - response = verifyAccountWithCode(verificationCode, null, registrationLock); - } catch (LockedException _e) { - throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); - } - masterKey = registrationLockData.getMasterKey(); + throw e; } + + var registrationLock = registrationLockData.getMasterKey().deriveRegistrationLock(); + try { + response = verifyAccountWithCode(verificationCode, registrationLock); + } catch (LockedException _e) { + throw new AssertionError("KBS Pin appeared to matched but reg lock still failed!"); + } + masterKey = registrationLockData.getMasterKey(); } // TODO response.isStorageCapable() @@ -192,18 +199,29 @@ public class RegistrationManager implements Closeable { } private VerifyAccountResponse verifyAccountWithCode( - final String verificationCode, final String legacyPin, final String registrationLock + final String verificationCode, final String registrationLock ) throws IOException { - return accountManager.verifyAccountWithCode(verificationCode, - null, - account.getLocalRegistrationId(), - true, - legacyPin, - registrationLock, - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - ServiceConfig.capabilities, - account.isDiscoverableByPhoneNumber()); + final ServiceResponse response; + if (registrationLock == null) { + response = accountManager.verifyAccount(verificationCode, + account.getLocalRegistrationId(), + true, + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), + ServiceConfig.capabilities, + account.isDiscoverableByPhoneNumber()); + } else { + response = accountManager.verifyAccountWithRegistrationLockPin(verificationCode, + account.getLocalRegistrationId(), + true, + registrationLock, + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), + ServiceConfig.capabilities, + account.isDiscoverableByPhoneNumber()); + } + handleResponseException(response); + return response.getResult().get(); } @Override @@ -213,4 +231,15 @@ public class RegistrationManager implements Closeable { account = null; } } + + private void handleResponseException(final ServiceResponse response) throws IOException { + final var throwableOptional = response.getExecutionError().or(response.getApplicationError()); + if (throwableOptional.isPresent()) { + if (throwableOptional.get() instanceof IOException) { + throw (IOException) throwableOptional.get(); + } else { + throw new IOException(throwableOptional.get()); + } + } + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java index 12d87cf5..bedec52c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java @@ -29,7 +29,7 @@ class SandboxConfig { private final static String KEY_BACKUP_ENCLAVE_NAME = "823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9"; private final static byte[] KEY_BACKUP_SERVICE_ID = Hex.decode( - "51a56084c0b21c6b8f62b1bc792ec9bedac4c7c3964bb08ddcab868158c09982"); + "16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29"); private final static String KEY_BACKUP_MRENCLAVE = "a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87"; private final static String URL = "https://chat.staging.signal.org"; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index da901a3d..dd07fc55 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -58,6 +58,16 @@ public class SendHelper { } }; + private final SignalServiceMessageSender.LegacyGroupEvents legacyGroupEvents = new SignalServiceMessageSender.LegacyGroupEvents() { + @Override + public void onMessageSent() { + } + + @Override + public void onSyncMessageSent() { + } + }; + public SendHelper( final SignalAccount account, final SignalDependencies dependencies, @@ -267,6 +277,7 @@ public class SendHelper { isRecipientUpdate, ContentHint.DEFAULT, message, + legacyGroupEvents, sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), () -> false); } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { From 43bcc95713f14bdf6103d63d2daed66c3e7df502 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Sep 2021 21:30:45 +0200 Subject: [PATCH 0787/2005] Add missing isActive check --- .../manager/storage/sessions/SessionStore.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index 5738408d..1b94642a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -109,11 +109,7 @@ public class SessionStore implements SignalServiceSessionStore { synchronized (cachedSessions) { final var session = loadSessionLocked(key); - if (session == null) { - return false; - } - - return session.hasSenderChain() && session.getSessionVersion() == CiphertextMessage.CURRENT_VERSION; + return isActive(session); } } @@ -158,6 +154,7 @@ public class SessionStore implements SignalServiceSessionStore { return recipientIdToNameMap.keySet() .stream() .flatMap(recipientId -> getKeysLocked(recipientId).stream()) + .filter(key -> isActive(this.loadSessionLocked(key))) .map(key -> new SignalProtocolAddress(recipientIdToNameMap.get(key.recipientId), key.getDeviceId())) .collect(Collectors.toSet()); } @@ -321,6 +318,12 @@ public class SessionStore implements SignalServiceSessionStore { } } + private static boolean isActive(SessionRecord record) { + return record != null + && record.hasSenderChain() + && record.getSessionVersion() == CiphertextMessage.CURRENT_VERSION; + } + private static final class Key { private final RecipientId recipientId; From 891c05210e947db28ce231266893206b4d22eec0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 4 Sep 2021 10:48:22 +0200 Subject: [PATCH 0788/2005] Improve comment in SessionStore --- .../asamk/signal/manager/storage/sessions/SessionStore.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java index 1b94642a..bae4fdf3 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/sessions/SessionStore.java @@ -179,7 +179,8 @@ public class SessionStore implements SignalServiceSessionStore { public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { synchronized (cachedSessions) { - final var otherHasSession = getKeysLocked(toBeMergedRecipientId).size() > 0; + final var keys = getKeysLocked(toBeMergedRecipientId); + final var otherHasSession = keys.size() > 0; if (!otherHasSession) { return; } @@ -189,8 +190,7 @@ public class SessionStore implements SignalServiceSessionStore { logger.debug("To be merged recipient had sessions, deleting."); deleteAllSessions(toBeMergedRecipientId); } else { - logger.debug("To be merged recipient had sessions, re-assigning to the new recipient."); - final var keys = getKeysLocked(toBeMergedRecipientId); + logger.debug("Only to be merged recipient had sessions, re-assigning to the new recipient."); for (var key : keys) { final var session = loadSessionLocked(key); deleteSessionLocked(key); From 35622ac6840defeecbbe382de3f9a0b81df72a1d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 4 Sep 2021 13:26:52 +0200 Subject: [PATCH 0789/2005] Use EMPTY send event listeners --- .../signal/manager/helper/SendHelper.java | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index dd07fc55..6ebc0254 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -44,30 +44,6 @@ public class SendHelper { private final GroupProvider groupProvider; private final RecipientRegistrationRefresher recipientRegistrationRefresher; - private final SignalServiceMessageSender.IndividualSendEvents sendEvents = new SignalServiceMessageSender.IndividualSendEvents() { - @Override - public void onMessageEncrypted() { - } - - @Override - public void onMessageSent() { - } - - @Override - public void onSyncMessageSent() { - } - }; - - private final SignalServiceMessageSender.LegacyGroupEvents legacyGroupEvents = new SignalServiceMessageSender.LegacyGroupEvents() { - @Override - public void onMessageSent() { - } - - @Override - public void onSyncMessageSent() { - } - }; - public SendHelper( final SignalAccount account, final SignalDependencies dependencies, @@ -277,7 +253,7 @@ public class SendHelper { isRecipientUpdate, ContentHint.DEFAULT, message, - legacyGroupEvents, + SignalServiceMessageSender.LegacyGroupEvents.EMPTY, sendResult -> logger.trace("Partial message send result: {}", sendResult.isSuccess()), () -> false); } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { @@ -297,14 +273,14 @@ public class SendHelper { unidentifiedAccessHelper.getAccessFor(recipientId), ContentHint.DEFAULT, message, - sendEvents); + SignalServiceMessageSender.IndividualSendEvents.EMPTY); } catch (UnregisteredUserException e) { final var newRecipientId = recipientRegistrationRefresher.refreshRecipientRegistration(recipientId); return messageSender.sendDataMessage(addressResolver.resolveSignalServiceAddress(newRecipientId), unidentifiedAccessHelper.getAccessFor(newRecipientId), ContentHint.DEFAULT, message, - sendEvents); + SignalServiceMessageSender.IndividualSendEvents.EMPTY); } } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); From ac18006abb2538b97c4d1e9ad658795748264d90 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 3 Sep 2021 22:38:45 +0200 Subject: [PATCH 0790/2005] Implement support for receiving sender key messages --- .../signal/manager/config/ServiceConfig.java | 7 +- .../helper/IncomingMessageHandler.java | 11 + .../helper/RecipientAddressResolver.java | 9 + .../signal/manager/storage/SignalAccount.java | 22 ++ .../storage/protocol/SignalProtocolStore.java | 16 +- .../senderKeys/SenderKeyRecordStore.java | 261 +++++++++++++++++ .../senderKeys/SenderKeySharedStore.java | 270 ++++++++++++++++++ .../storage/senderKeys/SenderKeyStore.java | 75 +++++ .../asamk/signal/ReceiveMessageHandler.java | 6 + 9 files changed, 664 insertions(+), 13 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/RecipientAddressResolver.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index 3f97be6b..5324439b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -34,12 +34,7 @@ public class ServiceConfig { } catch (Throwable ignored) { zkGroupAvailable = false; } - capabilities = new AccountAttributes.Capabilities(false, - zkGroupAvailable, - false, - zkGroupAvailable, - false, - true); + capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, true, true); try { TrustStore contactTrustStore = new IasTrustStore(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 57b71ee1..e6e43478 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -30,6 +30,7 @@ import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.SignalProtocolAddress; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -173,10 +174,20 @@ public final class IncomingMessageHandler { ) { var actions = new ArrayList(); final RecipientId sender; + final int senderDeviceId; if (!envelope.isUnidentifiedSender() && envelope.hasSourceUuid()) { sender = recipientResolver.resolveRecipient(envelope.getSourceAddress()); + senderDeviceId = envelope.getSourceDevice(); } else { sender = recipientResolver.resolveRecipient(content.getSender()); + senderDeviceId = content.getSenderDevice(); + } + + if (content.getSenderKeyDistributionMessage().isPresent()) { + final var message = content.getSenderKeyDistributionMessage().get(); + final var protocolAddress = new SignalProtocolAddress(addressResolver.resolveSignalServiceAddress(sender) + .getIdentifier(), senderDeviceId); + dependencies.getMessageSender().processSenderKeyDistributionMessage(protocolAddress, message); } if (content.getDataMessage().isPresent()) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/RecipientAddressResolver.java b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientAddressResolver.java new file mode 100644 index 00000000..e2c10f4e --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/RecipientAddressResolver.java @@ -0,0 +1,9 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +public interface RecipientAddressResolver { + + RecipientAddress resolveRecipientAddress(RecipientId recipientId); +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 4e240887..e75996c5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -24,6 +24,7 @@ import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientStore; +import org.asamk.signal.manager.storage.senderKeys.SenderKeyStore; import org.asamk.signal.manager.storage.sessions.SessionStore; import org.asamk.signal.manager.storage.stickers.StickerStore; import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore; @@ -95,6 +96,7 @@ public class SignalAccount implements Closeable { private SignedPreKeyStore signedPreKeyStore; private SessionStore sessionStore; private IdentityKeyStore identityKeyStore; + private SenderKeyStore senderKeyStore; private GroupStore groupStore; private GroupStore.Storage groupStoreStorage; private RecipientStore recipientStore; @@ -181,10 +183,15 @@ public class SignalAccount implements Closeable { identityKey, registrationId, trustNewIdentity); + senderKeyStore = new SenderKeyStore(getSharedSenderKeysFile(dataPath, username), + getSenderKeysPath(dataPath, username), + recipientStore::resolveRecipientAddress, + recipientStore); signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore, + senderKeyStore, this::isMultiDevice); messageCache = new MessageCache(getMessageCachePath(dataPath, username)); @@ -221,6 +228,7 @@ public class SignalAccount implements Closeable { account.setProvisioningData(username, uuid, password, encryptedDeviceName, deviceId, profileKey); account.recipientStore.resolveRecipientTrusted(account.getSelfAddress()); account.sessionStore.archiveAllSessions(); + account.senderKeyStore.deleteAll(); account.clearAllPreKeys(); return account; } @@ -303,6 +311,7 @@ public class SignalAccount implements Closeable { identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId); messageCache.mergeRecipients(recipientId, toBeMergedRecipientId); groupStore.mergeRecipients(recipientId, toBeMergedRecipientId); + senderKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId); } public static File getFileName(File dataPath, String username) { @@ -343,6 +352,14 @@ public class SignalAccount implements Closeable { return new File(getUserPath(dataPath, username), "sessions"); } + private static File getSenderKeysPath(File dataPath, String username) { + return new File(getUserPath(dataPath, username), "sender-keys"); + } + + private static File getSharedSenderKeysFile(File dataPath, String username) { + return new File(getUserPath(dataPath, username), "shared-sender-keys-store"); + } + private static File getRecipientsStoreFile(File dataPath, String username) { return new File(getUserPath(dataPath, username), "recipients-store"); } @@ -768,6 +785,10 @@ public class SignalAccount implements Closeable { return stickerStore; } + public SenderKeyStore getSenderKeyStore() { + return senderKeyStore; + } + public MessageCache getMessageCache() { return messageCache; } @@ -932,6 +953,7 @@ public class SignalAccount implements Closeable { save(); getSessionStore().archiveAllSessions(); + senderKeyStore.deleteAll(); final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress()); final var publicKey = getIdentityKeyPair().getPublicKey(); getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date()); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java index 77eb764a..7f200459 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/protocol/SignalProtocolStore.java @@ -13,6 +13,7 @@ import org.whispersystems.libsignal.state.SessionRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyStore; import org.whispersystems.signalservice.api.SignalServiceDataStore; +import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore; import org.whispersystems.signalservice.api.SignalServiceSessionStore; import org.whispersystems.signalservice.api.push.DistributionId; @@ -28,6 +29,7 @@ public class SignalProtocolStore implements SignalServiceDataStore { private final SignedPreKeyStore signedPreKeyStore; private final SignalServiceSessionStore sessionStore; private final IdentityKeyStore identityKeyStore; + private final SignalServiceSenderKeyStore senderKeyStore; private final Supplier isMultiDevice; public SignalProtocolStore( @@ -35,12 +37,14 @@ public class SignalProtocolStore implements SignalServiceDataStore { final SignedPreKeyStore signedPreKeyStore, final SignalServiceSessionStore sessionStore, final IdentityKeyStore identityKeyStore, + final SignalServiceSenderKeyStore senderKeyStore, final Supplier isMultiDevice ) { this.preKeyStore = preKeyStore; this.signedPreKeyStore = signedPreKeyStore; this.sessionStore = sessionStore; this.identityKeyStore = identityKeyStore; + this.senderKeyStore = senderKeyStore; this.isMultiDevice = isMultiDevice; } @@ -163,31 +167,29 @@ public class SignalProtocolStore implements SignalServiceDataStore { public void storeSenderKey( final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record ) { - // TODO + senderKeyStore.storeSenderKey(sender, distributionId, record); } @Override public SenderKeyRecord loadSenderKey(final SignalProtocolAddress sender, final UUID distributionId) { - // TODO - return null; + return senderKeyStore.loadSenderKey(sender, distributionId); } @Override public Set getSenderKeySharedWith(final DistributionId distributionId) { - // TODO - return null; + return senderKeyStore.getSenderKeySharedWith(distributionId); } @Override public void markSenderKeySharedWith( final DistributionId distributionId, final Collection addresses ) { - // TODO + senderKeyStore.markSenderKeySharedWith(distributionId, addresses); } @Override public void clearSenderKeySharedWith(final Collection addresses) { - // TODO + senderKeyStore.clearSenderKeySharedWith(addresses); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java new file mode 100644 index 00000000..f84903e4 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java @@ -0,0 +1,261 @@ +package org.asamk.signal.manager.storage.senderKeys; + +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.asamk.signal.manager.util.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.groups.state.SenderKeyRecord; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups.state.SenderKeyStore { + + private final static Logger logger = LoggerFactory.getLogger(SenderKeyRecordStore.class); + + private final Map cachedSenderKeys = new HashMap<>(); + + private final File senderKeysPath; + + private final RecipientResolver resolver; + + public SenderKeyRecordStore( + final File senderKeysPath, final RecipientResolver resolver + ) { + this.senderKeysPath = senderKeysPath; + this.resolver = resolver; + } + + @Override + public SenderKeyRecord loadSenderKey(final SignalProtocolAddress address, final UUID distributionId) { + final var key = getKey(address, distributionId); + + synchronized (cachedSenderKeys) { + return loadSenderKeyLocked(key); + } + } + + @Override + public void storeSenderKey( + final SignalProtocolAddress address, final UUID distributionId, final SenderKeyRecord record + ) { + final var key = getKey(address, distributionId); + + synchronized (cachedSenderKeys) { + storeSenderKeyLocked(key, record); + } + } + + public void deleteAll() { + synchronized (cachedSenderKeys) { + cachedSenderKeys.clear(); + final var files = senderKeysPath.listFiles((_file, s) -> senderKeyFileNamePattern.matcher(s).matches()); + if (files == null) { + return; + } + + for (final var file : files) { + try { + Files.delete(file.toPath()); + } catch (IOException e) { + logger.error("Failed to delete sender key file {}: {}", file, e.getMessage()); + } + } + } + } + + public void deleteAllFor(final RecipientId recipientId) { + synchronized (cachedSenderKeys) { + cachedSenderKeys.clear(); + final var keys = getKeysLocked(recipientId); + for (var key : keys) { + deleteSenderKeyLocked(key); + } + } + } + + public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { + synchronized (cachedSenderKeys) { + final var keys = getKeysLocked(toBeMergedRecipientId); + final var otherHasSenderKeys = keys.size() > 0; + if (!otherHasSenderKeys) { + return; + } + + logger.debug("Only to be merged recipient had sender keys, re-assigning to the new recipient."); + for (var key : keys) { + final var toBeMergedSenderKey = loadSenderKeyLocked(key); + deleteSenderKeyLocked(key); + if (toBeMergedSenderKey == null) { + continue; + } + + final var newKey = new Key(recipientId, key.getDeviceId(), key.distributionId); + final var senderKeyRecord = loadSenderKeyLocked(newKey); + if (senderKeyRecord != null) { + continue; + } + storeSenderKeyLocked(newKey, senderKeyRecord); + } + } + } + + /** + * @param identifier can be either a serialized uuid or a e164 phone number + */ + private RecipientId resolveRecipient(String identifier) { + return resolver.resolveRecipient(identifier); + } + + private Key getKey(final SignalProtocolAddress address, final UUID distributionId) { + final var recipientId = resolveRecipient(address.getName()); + return new Key(recipientId, address.getDeviceId(), distributionId); + } + + private List getKeysLocked(RecipientId recipientId) { + final var files = senderKeysPath.listFiles((_file, s) -> s.startsWith(recipientId.getId() + "_")); + if (files == null) { + return List.of(); + } + return parseFileNames(files); + } + + final Pattern senderKeyFileNamePattern = Pattern.compile("([0-9]+)_([0-9]+)_([0-9a-z\\-]+)"); + + private List parseFileNames(final File[] files) { + return Arrays.stream(files) + .map(f -> senderKeyFileNamePattern.matcher(f.getName())) + .filter(Matcher::matches) + .map(matcher -> new Key(RecipientId.of(Long.parseLong(matcher.group(1))), + Integer.parseInt(matcher.group(2)), + UUID.fromString(matcher.group(3)))) + .collect(Collectors.toList()); + } + + private File getSenderKeyFile(Key key) { + try { + IOUtils.createPrivateDirectories(senderKeysPath); + } catch (IOException e) { + throw new AssertionError("Failed to create sender keys path", e); + } + return new File(senderKeysPath, + key.getRecipientId().getId() + "_" + key.getDeviceId() + "_" + key.distributionId.toString()); + } + + private SenderKeyRecord loadSenderKeyLocked(final Key key) { + { + final var senderKeyRecord = cachedSenderKeys.get(key); + if (senderKeyRecord != null) { + return senderKeyRecord; + } + } + + final var file = getSenderKeyFile(key); + if (!file.exists()) { + return null; + } + try (var inputStream = new FileInputStream(file)) { + final var senderKeyRecord = new SenderKeyRecord(inputStream.readAllBytes()); + cachedSenderKeys.put(key, senderKeyRecord); + return senderKeyRecord; + } catch (IOException e) { + logger.warn("Failed to load sender key, resetting sender key: {}", e.getMessage()); + return null; + } + } + + private void storeSenderKeyLocked(final Key key, final SenderKeyRecord senderKeyRecord) { + cachedSenderKeys.put(key, senderKeyRecord); + + final var file = getSenderKeyFile(key); + try { + try (var outputStream = new FileOutputStream(file)) { + outputStream.write(senderKeyRecord.serialize()); + } + } catch (IOException e) { + logger.warn("Failed to store sender key, trying to delete file and retry: {}", e.getMessage()); + try { + Files.delete(file.toPath()); + try (var outputStream = new FileOutputStream(file)) { + outputStream.write(senderKeyRecord.serialize()); + } + } catch (IOException e2) { + logger.error("Failed to store sender key file {}: {}", file, e2.getMessage()); + } + } + } + + private void deleteSenderKeyLocked(final Key key) { + cachedSenderKeys.remove(key); + + final var file = getSenderKeyFile(key); + if (!file.exists()) { + return; + } + try { + Files.delete(file.toPath()); + } catch (IOException e) { + logger.error("Failed to delete sender key file {}: {}", file, e.getMessage()); + } + } + + private static final class Key { + + private final RecipientId recipientId; + private final int deviceId; + private final UUID distributionId; + + public Key( + final RecipientId recipientId, final int deviceId, final UUID distributionId + ) { + this.recipientId = recipientId; + this.deviceId = deviceId; + this.distributionId = distributionId; + } + + public RecipientId getRecipientId() { + return recipientId; + } + + public int getDeviceId() { + return deviceId; + } + + public UUID getDistributionId() { + return distributionId; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Key key = (Key) o; + + if (deviceId != key.deviceId) return false; + if (!recipientId.equals(key.recipientId)) return false; + return distributionId.equals(key.distributionId); + } + + @Override + public int hashCode() { + int result = recipientId.hashCode(); + result = 31 * result + deviceId; + result = 31 * result + distributionId.hashCode(); + return result; + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java new file mode 100644 index 00000000..3faf2e74 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeySharedStore.java @@ -0,0 +1,270 @@ +package org.asamk.signal.manager.storage.senderKeys; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.asamk.signal.manager.helper.RecipientAddressResolver; +import org.asamk.signal.manager.storage.Utils; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.signalservice.api.push.DistributionId; +import org.whispersystems.signalservice.api.util.UuidUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +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 java.util.stream.Collectors; + +public class SenderKeySharedStore { + + private final static Logger logger = LoggerFactory.getLogger(SenderKeySharedStore.class); + + private final Map> sharedSenderKeys; + + private final ObjectMapper objectMapper; + private final File file; + + private final RecipientResolver resolver; + private final RecipientAddressResolver addressResolver; + + public static SenderKeySharedStore load( + final File file, final RecipientAddressResolver addressResolver, final RecipientResolver resolver + ) throws IOException { + final var objectMapper = Utils.createStorageObjectMapper(); + try (var inputStream = new FileInputStream(file)) { + final var storage = objectMapper.readValue(inputStream, Storage.class); + final var sharedSenderKeys = new HashMap>(); + for (final var senderKey : storage.sharedSenderKeys) { + final var entry = new SenderKeySharedEntry(RecipientId.of(senderKey.recipientId), senderKey.deviceId); + final var uuid = UuidUtil.parseOrNull(senderKey.distributionId); + if (uuid == null) { + logger.warn("Read invalid distribution id from storage {}, ignoring", senderKey.distributionId); + continue; + } + final var distributionId = DistributionId.from(uuid); + var entries = sharedSenderKeys.get(distributionId); + if (entries == null) { + entries = new HashSet<>(); + } + entries.add(entry); + sharedSenderKeys.put(distributionId, entries); + } + + return new SenderKeySharedStore(sharedSenderKeys, objectMapper, file, addressResolver, resolver); + } catch (FileNotFoundException e) { + logger.debug("Creating new shared sender key store."); + return new SenderKeySharedStore(new HashMap<>(), objectMapper, file, addressResolver, resolver); + } + } + + private SenderKeySharedStore( + final Map> sharedSenderKeys, + final ObjectMapper objectMapper, + final File file, + final RecipientAddressResolver addressResolver, + final RecipientResolver resolver + ) { + this.sharedSenderKeys = sharedSenderKeys; + this.objectMapper = objectMapper; + this.file = file; + this.addressResolver = addressResolver; + this.resolver = resolver; + } + + public Set getSenderKeySharedWith(final DistributionId distributionId) { + synchronized (sharedSenderKeys) { + return sharedSenderKeys.get(distributionId) + .stream() + .map(k -> new SignalProtocolAddress(addressResolver.resolveRecipientAddress(k.getRecipientId()) + .getIdentifier(), k.getDeviceId())) + .collect(Collectors.toSet()); + } + } + + public void markSenderKeySharedWith( + final DistributionId distributionId, final Collection addresses + ) { + final var newEntries = addresses.stream() + .map(a -> new SenderKeySharedEntry(resolveRecipient(a.getName()), a.getDeviceId())) + .collect(Collectors.toSet()); + + synchronized (sharedSenderKeys) { + final var previousEntries = sharedSenderKeys.getOrDefault(distributionId, Set.of()); + + sharedSenderKeys.put(distributionId, new HashSet<>() { + { + addAll(previousEntries); + addAll(newEntries); + } + }); + saveLocked(); + } + } + + public void clearSenderKeySharedWith(final Collection addresses) { + final var entriesToDelete = addresses.stream() + .map(a -> new SenderKeySharedEntry(resolveRecipient(a.getName()), a.getDeviceId())) + .collect(Collectors.toSet()); + + synchronized (sharedSenderKeys) { + for (final var distributionId : sharedSenderKeys.keySet()) { + final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of()); + + sharedSenderKeys.put(distributionId, new HashSet<>(entries) { + { + removeAll(entriesToDelete); + } + }); + } + saveLocked(); + } + } + + public void deleteAll() { + synchronized (sharedSenderKeys) { + sharedSenderKeys.clear(); + saveLocked(); + } + } + + public void deleteAllFor(final RecipientId recipientId) { + synchronized (sharedSenderKeys) { + for (final var distributionId : sharedSenderKeys.keySet()) { + final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of()); + + sharedSenderKeys.put(distributionId, new HashSet<>(entries) { + { + entries.removeIf(e -> e.getRecipientId().equals(recipientId)); + } + }); + } + saveLocked(); + } + } + + public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { + synchronized (sharedSenderKeys) { + for (final var distributionId : sharedSenderKeys.keySet()) { + final var entries = sharedSenderKeys.getOrDefault(distributionId, Set.of()); + + sharedSenderKeys.put(distributionId, + entries.stream() + .map(e -> e.recipientId.equals(toBeMergedRecipientId) ? new SenderKeySharedEntry( + recipientId, + e.getDeviceId()) : e) + .collect(Collectors.toSet())); + } + saveLocked(); + } + } + + /** + * @param identifier can be either a serialized uuid or a e164 phone number + */ + private RecipientId resolveRecipient(String identifier) { + return resolver.resolveRecipient(identifier); + } + + private void saveLocked() { + var storage = new Storage(sharedSenderKeys.entrySet().stream().flatMap(pair -> { + final var sharedWith = pair.getValue(); + return sharedWith.stream() + .map(entry -> new Storage.SharedSenderKey(entry.getRecipientId().getId(), + entry.getDeviceId(), + pair.getKey().asUuid().toString())); + }).collect(Collectors.toList())); + + // Write to memory first to prevent corrupting the file in case of serialization errors + try (var inMemoryOutput = new ByteArrayOutputStream()) { + objectMapper.writeValue(inMemoryOutput, storage); + + var input = new ByteArrayInputStream(inMemoryOutput.toByteArray()); + try (var outputStream = new FileOutputStream(file)) { + input.transferTo(outputStream); + } + } catch (Exception e) { + logger.error("Error saving shared sender key store file: {}", e.getMessage()); + } + } + + private static class Storage { + + public List sharedSenderKeys; + + // For deserialization + private Storage() { + } + + public Storage(final List sharedSenderKeys) { + this.sharedSenderKeys = sharedSenderKeys; + } + + private static class SharedSenderKey { + + public long recipientId; + public int deviceId; + public String distributionId; + + // For deserialization + private SharedSenderKey() { + } + + public SharedSenderKey(final long recipientId, final int deviceId, final String distributionId) { + this.recipientId = recipientId; + this.deviceId = deviceId; + this.distributionId = distributionId; + } + } + } + + private static final class SenderKeySharedEntry { + + private final RecipientId recipientId; + private final int deviceId; + + public SenderKeySharedEntry( + final RecipientId recipientId, final int deviceId + ) { + this.recipientId = recipientId; + this.deviceId = deviceId; + } + + public RecipientId getRecipientId() { + return recipientId; + } + + public int getDeviceId() { + return deviceId; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final SenderKeySharedEntry that = (SenderKeySharedEntry) o; + + if (deviceId != that.deviceId) return false; + return recipientId.equals(that.recipientId); + } + + @Override + public int hashCode() { + int result = recipientId.hashCode(); + result = 31 * result + deviceId; + return result; + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java new file mode 100644 index 00000000..ab02d755 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyStore.java @@ -0,0 +1,75 @@ +package org.asamk.signal.manager.storage.senderKeys; + +import org.asamk.signal.manager.helper.RecipientAddressResolver; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientResolver; +import org.whispersystems.libsignal.SignalProtocolAddress; +import org.whispersystems.libsignal.groups.state.SenderKeyRecord; +import org.whispersystems.signalservice.api.SignalServiceSenderKeyStore; +import org.whispersystems.signalservice.api.push.DistributionId; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Set; +import java.util.UUID; + +public class SenderKeyStore implements SignalServiceSenderKeyStore { + + private final SenderKeyRecordStore senderKeyRecordStore; + private final SenderKeySharedStore senderKeySharedStore; + + public SenderKeyStore( + final File file, + final File senderKeysPath, + final RecipientAddressResolver addressResolver, + final RecipientResolver resolver + ) throws IOException { + this.senderKeyRecordStore = new SenderKeyRecordStore(senderKeysPath, resolver); + this.senderKeySharedStore = SenderKeySharedStore.load(file, addressResolver, resolver); + } + + @Override + public void storeSenderKey( + final SignalProtocolAddress sender, final UUID distributionId, final SenderKeyRecord record + ) { + senderKeyRecordStore.storeSenderKey(sender, distributionId, record); + } + + @Override + public SenderKeyRecord loadSenderKey(final SignalProtocolAddress sender, final UUID distributionId) { + return senderKeyRecordStore.loadSenderKey(sender, distributionId); + } + + @Override + public Set getSenderKeySharedWith(final DistributionId distributionId) { + return senderKeySharedStore.getSenderKeySharedWith(distributionId); + } + + @Override + public void markSenderKeySharedWith( + final DistributionId distributionId, final Collection addresses + ) { + senderKeySharedStore.markSenderKeySharedWith(distributionId, addresses); + } + + @Override + public void clearSenderKeySharedWith(final Collection addresses) { + senderKeySharedStore.clearSenderKeySharedWith(addresses); + } + + public void deleteAll() { + senderKeySharedStore.deleteAll(); + senderKeyRecordStore.deleteAll(); + } + + public void rotateSenderKeys(RecipientId recipientId) { + senderKeySharedStore.deleteAllFor(recipientId); + senderKeyRecordStore.deleteAllFor(recipientId); + } + + public void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) { + senderKeySharedStore.mergeRecipients(recipientId, toBeMergedRecipientId); + senderKeyRecordStore.mergeRecipients(recipientId, toBeMergedRecipientId); + } +} diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 4a516197..15dbd1af 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -82,6 +82,12 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { DateUtils.formatTimestamp(content.getServerReceivedTimestamp()), DateUtils.formatTimestamp(content.getServerDeliveredTimestamp())); + if (content.getSenderKeyDistributionMessage().isPresent()) { + final var message = content.getSenderKeyDistributionMessage().get(); + writer.println("Received a sender key distribution message for distributionId {}", + message.getDistributionId()); + } + if (content.getDataMessage().isPresent()) { var message = content.getDataMessage().get(); printDataMessage(writer, message); From 5a2e37a6e242b920e5647e3d98c2aecb1932f763 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 4 Sep 2021 15:06:25 +0200 Subject: [PATCH 0791/2005] Only handle jsonRpc requests, after receive thread has caught up with old messages --- .../org/asamk/signal/manager/Manager.java | 24 +++++++++++++++---- .../commands/JsonRpcDispatcherCommand.java | 10 ++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index c40fa7cd..c4c77b34 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -141,6 +141,7 @@ public class Manager implements Closeable { private final IncomingMessageHandler incomingMessageHandler; private final Context context; + private boolean hasCaughtUpWithOldMessages = false; Manager( SignalAccount account, @@ -865,7 +866,7 @@ public class Manager implements Closeable { final var signalWebSocket = dependencies.getSignalWebSocket(); signalWebSocket.connect(); - var hasCaughtUpWithOldMessages = false; + hasCaughtUpWithOldMessages = false; while (!Thread.interrupted()) { SignalServiceEnvelope envelope; @@ -885,11 +886,14 @@ public class Manager implements Closeable { envelope = result.get(); } else { // Received indicator that server queue is empty - hasCaughtUpWithOldMessages = true; - handleQueuedActions(queuedActions); queuedActions.clear(); + hasCaughtUpWithOldMessages = true; + synchronized (this) { + this.notifyAll(); + } + // Continue to wait another timeout for new messages continue; } @@ -936,17 +940,27 @@ public class Manager implements Closeable { handleQueuedActions(queuedActions); } + public boolean hasCaughtUpWithOldMessages() { + return hasCaughtUpWithOldMessages; + } + private void handleQueuedActions(final Collection queuedActions) { + var interrupted = false; for (var action : queuedActions) { try { action.execute(context); } catch (Throwable e) { - if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); + if ((e instanceof AssertionError || e instanceof RuntimeException) + && e.getCause() instanceof InterruptedException) { + interrupted = true; + continue; } logger.warn("Message action failed.", e); } } + if (interrupted) { + Thread.currentThread().interrupt(); + } } public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java index 16d0cf71..d0e4dfec 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java @@ -75,6 +75,16 @@ public class JsonRpcDispatcherCommand implements LocalCommand { objectMapper.valueToTree(s), null)), m, ignoreAttachments); + // Maybe this should be handled inside the Manager + while (!m.hasCaughtUpWithOldMessages()) { + try { + synchronized (m) { + m.wait(); + } + } catch (InterruptedException ignored) { + } + } + final BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); final var jsonRpcReader = new JsonRpcReader(jsonRpcSender, () -> { From 299671480fb79f0abcc67ec5f9ec89fac9605345 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 5 Sep 2021 11:41:38 +0200 Subject: [PATCH 0792/2005] Add possibility to update the device name --- CHANGELOG.md | 1 + .../java/org/asamk/signal/manager/Manager.java | 17 ++++++++++++----- .../signal/manager/storage/SignalAccount.java | 5 +++++ man/signal-cli.1.adoc | 3 +++ .../signal/commands/UpdateAccountCommand.java | 4 +++- 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2ba6843..fa27507d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Added - New global parameter `--trust-new-identities=always` to allow trusting any new identity key without verification +- New parameter `--device-name` for `updateAccount` command to update the device name ## [0.8.5] - 2021-08-07 ### Added diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index c4c77b34..366fb371 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -311,7 +311,7 @@ public class Manager implements Closeable { if (account.getUuid() == null) { account.setUuid(dependencies.getAccountManager().getOwnUuid()); } - updateAccountAttributes(); + updateAccountAttributes(null); } /** @@ -343,14 +343,21 @@ public class Manager implements Closeable { })); } - public void updateAccountAttributes() throws IOException { + public void updateAccountAttributes(String deviceName) throws IOException { + final String encryptedDeviceName; + if (deviceName == null) { + encryptedDeviceName = account.getEncryptedDeviceName(); + } else { + final var privateKey = account.getIdentityKeyPair().getPrivateKey(); + encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); + account.setEncryptedDeviceName(encryptedDeviceName); + } dependencies.getAccountManager() - .setAccountAttributes(account.getEncryptedDeviceName(), + .setAccountAttributes(encryptedDeviceName, null, account.getLocalRegistrationId(), true, - // set legacy pin only if no KBS master key is set - account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null, + null, account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), account.getSelfUnidentifiedAccessKey(), account.isUnrestrictedUnidentifiedAccess(), diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index e75996c5..efdcf798 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -818,6 +818,11 @@ public class SignalAccount implements Closeable { return encryptedDeviceName; } + public void setEncryptedDeviceName(final String encryptedDeviceName) { + this.encryptedDeviceName = encryptedDeviceName; + save(); + } + public int getDeviceId() { return deviceId; } diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index b8251eb1..b52612be 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -110,6 +110,9 @@ CAUTION: Only delete your account if you won't use this number again! Update the account attributes on the signal server. Can fix problems with receiving messages. +*-n* NAME, *--device-name* NAME:: +Set a new device name for the main or linked device + === setPin Set a registration lock pin, to prevent others from registering this number. diff --git a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java index f2ed6a98..600a38c4 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java @@ -20,14 +20,16 @@ public class UpdateAccountCommand implements JsonRpcLocalCommand { @Override public void attachToSubparser(final Subparser subparser) { subparser.help("Update the account attributes on the signal server."); + subparser.addArgument("-n", "--device-name").help("Specify a name to describe this device."); } @Override public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { + var deviceName = ns.getString("device-name"); try { - m.updateAccountAttributes(); + m.updateAccountAttributes(deviceName); } catch (IOException e) { throw new IOErrorException("UpdateAccount error: " + e.getMessage()); } From 2e01a05e7110b4f94abb10489a28b73d9f4be9c0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 24 May 2021 16:51:36 +0200 Subject: [PATCH 0793/2005] Implement retrieving data from remote storage Related #604 --- .../org/asamk/signal/manager/Manager.java | 13 +- .../signal/manager/ProvisioningManager.java | 3 + .../signal/manager/RegistrationManager.java | 4 +- .../org/asamk/signal/manager/TrustLevel.java | 15 ++ .../actions/RetrieveStorageDataAction.java | 26 ++ .../manager/actions/SendSyncKeysAction.java | 20 ++ .../helper/IncomingMessageHandler.java | 10 +- .../signal/manager/helper/StorageHelper.java | 222 ++++++++++++++++++ .../signal/manager/helper/SyncHelper.java | 6 + .../asamk/signal/manager/jobs/Context.java | 10 +- .../signal/manager/storage/SignalAccount.java | 22 ++ 11 files changed, 346 insertions(+), 5 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendSyncKeysAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 366fb371..a7a691cc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -42,6 +42,7 @@ import org.asamk.signal.manager.helper.IncomingMessageHandler; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.StorageHelper; import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; import org.asamk.signal.manager.jobs.Context; @@ -133,6 +134,7 @@ public class Manager implements Closeable { private final ProfileHelper profileHelper; private final PinHelper pinHelper; + private final StorageHelper storageHelper; private final SendHelper sendHelper; private final SyncHelper syncHelper; private final AttachmentHelper attachmentHelper; @@ -209,6 +211,7 @@ public class Manager implements Closeable { avatarStore, this::resolveSignalServiceAddress, account.getRecipientStore()); + this.storageHelper = new StorageHelper(account, dependencies, groupHelper); this.contactHelper = new ContactHelper(account); this.syncHelper = new SyncHelper(account, attachmentHelper, @@ -223,7 +226,8 @@ public class Manager implements Closeable { sendHelper, groupHelper, syncHelper, - profileHelper); + profileHelper, + storageHelper); var jobExecutor = new JobExecutor(context); this.incomingMessageHandler = new IncomingMessageHandler(account, @@ -747,6 +751,13 @@ public class Manager implements Closeable { public void requestAllSyncData() throws IOException { syncHelper.requestAllSyncData(); + retrieveRemoteStorage(); + } + + void retrieveRemoteStorage() throws IOException { + if (account.getStorageKey() != null) { + storageHelper.readDataFromStorage(); + } } private byte[] getSenderCertificate() { diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 80c214f7..90dc6c66 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -103,6 +103,7 @@ public class ProvisioningManager { ? null : DeviceNameUtil.encryptDeviceName(deviceName, ret.getIdentity().getPrivateKey()); + logger.debug("Finishing new device registration"); var deviceId = accountManager.finishNewDeviceRegistration(ret.getProvisioningCode(), false, true, @@ -129,6 +130,7 @@ public class ProvisioningManager { try { m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + logger.debug("Refreshing pre keys"); try { m.refreshPreKeys(); } catch (Exception e) { @@ -136,6 +138,7 @@ public class ProvisioningManager { throw e; } + logger.debug("Requesting sync data"); try { m.requestAllSyncData(); } catch (Exception e) { diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 2be3f719..7cc0a7bc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -174,7 +174,6 @@ public class RegistrationManager implements Closeable { masterKey = registrationLockData.getMasterKey(); } - // TODO response.isStorageCapable() //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.finishRegistration(UuidUtil.parseOrNull(response.getUuid()), masterKey, pin); @@ -186,6 +185,9 @@ public class RegistrationManager implements Closeable { m.refreshPreKeys(); // Set an initial empty profile so user can be added to groups m.setProfile(null, null, null, null, null); + if (response.isStorageCapable()) { + m.retrieveRemoteStorage(); + } final var result = m; m = null; diff --git a/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java b/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java index c9fa7a5e..5c712866 100644 --- a/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java +++ b/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java @@ -1,6 +1,7 @@ package org.asamk.signal.manager; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; +import org.whispersystems.signalservice.internal.storage.protos.ContactRecord; public enum TrustLevel { UNTRUSTED, @@ -16,6 +17,20 @@ public enum TrustLevel { return TrustLevel.cachedValues[i]; } + public static TrustLevel fromIdentityState(ContactRecord.IdentityState identityState) { + switch (identityState) { + case DEFAULT: + return TRUSTED_UNVERIFIED; + case UNVERIFIED: + return UNTRUSTED; + case VERIFIED: + return TRUSTED_VERIFIED; + case UNRECOGNIZED: + return null; + } + throw new RuntimeException("Unknown identity state: " + identityState); + } + public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) { switch (verifiedState) { case DEFAULT: diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java new file mode 100644 index 00000000..6585a99a --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/RetrieveStorageDataAction.java @@ -0,0 +1,26 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class RetrieveStorageDataAction implements HandleAction { + + private static final RetrieveStorageDataAction INSTANCE = new RetrieveStorageDataAction(); + + private RetrieveStorageDataAction() { + } + + public static RetrieveStorageDataAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + if (context.getAccount().getStorageKey() != null) { + context.getStorageHelper().readDataFromStorage(); + } else { + if (!context.getAccount().isMasterDevice()) { + context.getSyncHelper().requestAllSyncData(); + } + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncKeysAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncKeysAction.java new file mode 100644 index 00000000..fe609291 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncKeysAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class SendSyncKeysAction implements HandleAction { + + private static final SendSyncKeysAction INSTANCE = new SendSyncKeysAction(); + + private SendSyncKeysAction() { + } + + public static SendSyncKeysAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSyncHelper().sendKeysMessage(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index e6e43478..e46effc0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -8,12 +8,14 @@ import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.actions.RenewSessionAction; import org.asamk.signal.manager.actions.RetrieveProfileAction; +import org.asamk.signal.manager.actions.RetrieveStorageDataAction; import org.asamk.signal.manager.actions.SendGroupInfoAction; import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendReceiptAction; import org.asamk.signal.manager.actions.SendSyncBlockedListAction; import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction; +import org.asamk.signal.manager.actions.SendSyncKeysAction; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupUtils; @@ -237,7 +239,10 @@ public final class IncomingMessageHandler { if (rm.isBlockedListRequest()) { actions.add(SendSyncBlockedListAction.create()); } - // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); + if (rm.isKeysRequest()) { + actions.add(SendSyncKeysAction.create()); + } + // TODO Handle rm.isConfigurationRequest(); } if (syncMessage.getGroups().isPresent()) { logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring."); @@ -307,7 +312,7 @@ public final class IncomingMessageHandler { case LOCAL_PROFILE: actions.add(new RetrieveProfileAction(account.getSelfRecipientId())); case STORAGE_MANIFEST: - // TODO + actions.add(RetrieveStorageDataAction.create()); } } if (syncMessage.getKeys().isPresent()) { @@ -315,6 +320,7 @@ public final class IncomingMessageHandler { if (keysMessage.getStorageService().isPresent()) { final var storageKey = keysMessage.getStorageService().get(); account.setStorageKey(storageKey); + actions.add(RetrieveStorageDataAction.create()); } } if (syncMessage.getConfiguration().isPresent()) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java new file mode 100644 index 00000000..4caab519 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -0,0 +1,222 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; +import org.signal.zkgroup.profiles.ProfileKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.storage.SignalAccountRecord; +import org.whispersystems.signalservice.api.storage.SignalStorageManifest; +import org.whispersystems.signalservice.api.storage.SignalStorageRecord; +import org.whispersystems.signalservice.api.storage.StorageId; +import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord; + +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +public class StorageHelper { + + private final static Logger logger = LoggerFactory.getLogger(StorageHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final GroupHelper groupHelper; + + public StorageHelper( + final SignalAccount account, final SignalDependencies dependencies, final GroupHelper groupHelper + ) { + this.account = account; + this.dependencies = dependencies; + this.groupHelper = groupHelper; + } + + public void readDataFromStorage() throws IOException { + logger.debug("Reading data from remote storage"); + Optional manifest; + try { + manifest = dependencies.getAccountManager() + .getStorageManifestIfDifferentVersion(account.getStorageKey(), account.getStorageManifestVersion()); + } catch (InvalidKeyException e) { + logger.warn("Manifest couldn't be decrypted, ignoring."); + return; + } + + if (!manifest.isPresent()) { + logger.debug("Manifest is up to date, does not exist or couldn't be decrypted, ignoring."); + return; + } + + account.setStorageManifestVersion(manifest.get().getVersion()); + + readAccountRecord(manifest.get()); + + final var storageIds = manifest.get() + .getStorageIds() + .stream() + .filter(id -> !id.isUnknown() && id.getType() != ManifestRecord.Identifier.Type.ACCOUNT_VALUE) + .collect(Collectors.toList()); + + for (final var record : getSignalStorageRecords(storageIds)) { + if (record.getType() == ManifestRecord.Identifier.Type.GROUPV2_VALUE) { + readGroupV2Record(record); + } else if (record.getType() == ManifestRecord.Identifier.Type.GROUPV1_VALUE) { + readGroupV1Record(record); + } else if (record.getType() == ManifestRecord.Identifier.Type.CONTACT_VALUE) { + readContactRecord(record); + } + } + } + + private void readContactRecord(final SignalStorageRecord record) { + if (record == null || !record.getContact().isPresent()) { + return; + } + + final var contactRecord = record.getContact().get(); + final var address = contactRecord.getAddress(); + + final var recipientId = account.getRecipientStore().resolveRecipient(address); + final var contact = account.getContactStore().getContact(recipientId); + if (contactRecord.getGivenName().isPresent() || contactRecord.getFamilyName().isPresent() || ( + (contact == null || !contact.isBlocked()) && contactRecord.isBlocked() + )) { + final var newContact = (contact == null ? Contact.newBuilder() : Contact.newBuilder(contact)).withBlocked( + contactRecord.isBlocked()) + .withName((contactRecord.getGivenName().or("") + " " + contactRecord.getFamilyName().or("")).trim()) + .build(); + account.getContactStore().storeContact(recipientId, newContact); + } + + if (contactRecord.getProfileKey().isPresent()) { + try { + final var profileKey = new ProfileKey(contactRecord.getProfileKey().get()); + account.getProfileStore().storeProfileKey(recipientId, profileKey); + } catch (InvalidInputException e) { + logger.warn("Received invalid contact profile key from storage"); + } + } + if (contactRecord.getIdentityKey().isPresent()) { + try { + final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get()); + account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); + + final var trustLevel = TrustLevel.fromIdentityState(contactRecord.getIdentityState()); + if (trustLevel != null) { + account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identityKey, trustLevel); + } + } catch (InvalidKeyException e) { + logger.warn("Received invalid contact identity key from storage"); + } + } + } + + private void readGroupV1Record(final SignalStorageRecord record) { + if (record == null || !record.getGroupV1().isPresent()) { + return; + } + + final var groupV1Record = record.getGroupV1().get(); + final var groupIdV1 = GroupId.v1(groupV1Record.getGroupId()); + + final var group = account.getGroupStore().getGroup(groupIdV1); + if (group == null) { + try { + groupHelper.sendGroupInfoRequest(groupIdV1, account.getSelfRecipientId()); + } catch (Throwable e) { + logger.warn("Failed to send group request", e); + } + } + final var groupV1 = account.getGroupStore().getOrCreateGroupV1(groupIdV1); + if (groupV1.isBlocked() != groupV1Record.isBlocked()) { + groupV1.setBlocked(groupV1Record.isBlocked()); + account.getGroupStore().updateGroup(groupV1); + } + } + + private void readGroupV2Record(final SignalStorageRecord record) { + if (record == null || !record.getGroupV2().isPresent()) { + return; + } + + final var groupV2Record = record.getGroupV2().get(); + if (groupV2Record.isArchived()) { + return; + } + + final GroupMasterKey groupMasterKey; + try { + groupMasterKey = new GroupMasterKey(groupV2Record.getMasterKeyBytes()); + } catch (InvalidInputException e) { + logger.warn("Received invalid group master key from storage"); + return; + } + + final var group = groupHelper.getOrMigrateGroup(groupMasterKey, 0, null); + if (group.isBlocked() != groupV2Record.isBlocked()) { + group.setBlocked(groupV2Record.isBlocked()); + account.getGroupStore().updateGroup(group); + } + } + + private void readAccountRecord(final SignalStorageManifest manifest) throws IOException { + Optional accountId = manifest.getAccountStorageId(); + if (!accountId.isPresent()) { + logger.warn("Manifest has no account record, ignoring."); + return; + } + + SignalStorageRecord record = getSignalStorageRecord(accountId.get()); + if (record == null) { + logger.warn("Could not find account record, even though we had an ID, ignoring."); + return; + } + + SignalAccountRecord accountRecord = record.getAccount().orNull(); + if (accountRecord == null) { + logger.warn("The storage record didn't actually have an account, ignoring."); + return; + } + + if (accountRecord.getProfileKey().isPresent()) { + try { + account.setProfileKey(new ProfileKey(accountRecord.getProfileKey().get())); + } catch (InvalidInputException e) { + logger.warn("Received invalid profile key from storage"); + } + } + } + + private SignalStorageRecord getSignalStorageRecord(final StorageId accountId) throws IOException { + List records; + try { + records = dependencies.getAccountManager() + .readStorageRecords(account.getStorageKey(), Collections.singletonList(accountId)); + } catch (InvalidKeyException e) { + logger.warn("Failed to read storage records, ignoring."); + return null; + } + return records.size() > 0 ? records.get(0) : null; + } + + private List getSignalStorageRecords(final List storageIds) throws IOException { + List records; + try { + records = dependencies.getAccountManager().readStorageRecords(account.getStorageKey(), storageIds); + } catch (InvalidKeyException e) { + logger.warn("Failed to read storage records, ignoring."); + return List.of(); + } + return records; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index bcdf6ab1..6db1ca7d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -20,6 +20,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsI import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; +import org.whispersystems.signalservice.api.messages.multidevice.KeysMessage; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage; @@ -215,6 +216,11 @@ public class SyncHelper { sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } + public void sendKeysMessage() throws IOException { + var keysMessage = new KeysMessage(Optional.fromNullable(account.getStorageKey())); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage)); + } + public void handleSyncDeviceContacts(final InputStream input) throws IOException { final var s = new DeviceContactsInputStream(input); DeviceContact c; diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java index 142c148a..beb41969 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java @@ -5,6 +5,7 @@ import org.asamk.signal.manager.StickerPackStore; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.StorageHelper; import org.asamk.signal.manager.helper.SyncHelper; import org.asamk.signal.manager.storage.SignalAccount; @@ -17,6 +18,7 @@ public class Context { private final GroupHelper groupHelper; private final SyncHelper syncHelper; private final ProfileHelper profileHelper; + private final StorageHelper storageHelper; public Context( final SignalAccount account, @@ -25,7 +27,8 @@ public class Context { final SendHelper sendHelper, final GroupHelper groupHelper, final SyncHelper syncHelper, - final ProfileHelper profileHelper + final ProfileHelper profileHelper, + final StorageHelper storageHelper ) { this.account = account; this.dependencies = dependencies; @@ -34,6 +37,7 @@ public class Context { this.groupHelper = groupHelper; this.syncHelper = syncHelper; this.profileHelper = profileHelper; + this.storageHelper = storageHelper; } public SignalAccount getAccount() { @@ -63,4 +67,8 @@ public class Context { public ProfileHelper getProfileHelper() { return profileHelper; } + + public StorageHelper getStorageHelper() { + return storageHelper; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index efdcf798..fd4ec597 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -84,6 +84,7 @@ public class SignalAccount implements Closeable { private String registrationLockPin; private MasterKey pinMasterKey; private StorageKey storageKey; + private long storageManifestVersion = -1; private ProfileKey profileKey; private int preKeyIdOffset; private int nextSignedPreKeyId; @@ -291,6 +292,9 @@ public class SignalAccount implements Closeable { this.registered = true; this.isMultiDevice = true; this.lastReceiveTimestamp = 0; + this.pinMasterKey = null; + this.storageManifestVersion = -1; + this.storageKey = null; } private void migrateLegacyConfigs() { @@ -432,6 +436,9 @@ public class SignalAccount implements Closeable { if (rootNode.hasNonNull("storageKey")) { storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText())); } + if (rootNode.hasNonNull("storageManifestVersion")) { + storageManifestVersion = rootNode.get("storageManifestVersion").asLong(); + } if (rootNode.hasNonNull("preKeyIdOffset")) { preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(0); } else { @@ -693,6 +700,7 @@ public class SignalAccount implements Closeable { pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize())) .put("storageKey", storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize())) + .put("storageManifestVersion", storageManifestVersion == -1 ? null : storageManifestVersion) .put("preKeyIdOffset", preKeyIdOffset) .put("nextSignedPreKeyId", nextSignedPreKeyId) .put("profileKey", @@ -877,6 +885,18 @@ public class SignalAccount implements Closeable { save(); } + public long getStorageManifestVersion() { + return this.storageManifestVersion; + } + + public void setStorageManifestVersion(final long storageManifestVersion) { + if (storageManifestVersion == this.storageManifestVersion) { + return; + } + this.storageManifestVersion = storageManifestVersion; + save(); + } + public ProfileKey getProfileKey() { return profileKey; } @@ -948,6 +968,8 @@ public class SignalAccount implements Closeable { public void finishRegistration(final UUID uuid, final MasterKey masterKey, final String pin) { this.pinMasterKey = masterKey; + this.storageManifestVersion = -1; + this.storageKey = null; this.encryptedDeviceName = null; this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; this.isMultiDevice = false; From 656ca6b5e4f5fe131f3477e39fcc20372655ad59 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 5 Sep 2021 16:06:13 +0200 Subject: [PATCH 0794/2005] Prevent creation of RecipientAddress with UNKNOWN_UUID --- .../manager/storage/recipients/RecipientAddress.java | 7 +++---- .../signal/manager/storage/recipients/RecipientStore.java | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java index 29e964b0..88877d83 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java @@ -18,6 +18,7 @@ public class RecipientAddress { * @param e164 The phone number of the user, if available. */ public RecipientAddress(Optional uuid, Optional e164) { + uuid = uuid.isPresent() && uuid.get().equals(UuidUtil.UNKNOWN_UUID) ? Optional.empty() : uuid; if (!uuid.isPresent() && !e164.isPresent()) { throw new AssertionError("Must have either a UUID or E164 number!"); } @@ -31,13 +32,11 @@ public class RecipientAddress { } public RecipientAddress(SignalServiceAddress address) { - this.uuid = Optional.of(address.getUuid()); - this.e164 = Optional.ofNullable(address.getNumber().orNull()); + this(Optional.of(address.getUuid()), Optional.ofNullable(address.getNumber().orNull())); } public RecipientAddress(UUID uuid) { - this.uuid = Optional.of(uuid); - this.e164 = Optional.empty(); + this(Optional.of(uuid), Optional.empty()); } public Optional getNumber() { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 86164d58..bace6a6b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -308,7 +308,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile final var byNumber = address.getNumber().isEmpty() ? Optional.empty() : findByNumberLocked(address.getNumber().get()); - final var byUuid = address.getUuid().isEmpty() || address.getUuid().get().equals(UuidUtil.UNKNOWN_UUID) + final var byUuid = address.getUuid().isEmpty() ? Optional.empty() : findByUuidLocked(address.getUuid().get()); From 537b7049515387d866aaf42ebfb14a629db38e65 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 8 Sep 2021 20:09:22 +0200 Subject: [PATCH 0795/2005] Ignore set profile failure if libzkgroup is missing Fixes #709 --- .../java/org/asamk/signal/manager/RegistrationManager.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 7cc0a7bc..1b00e562 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -184,7 +184,11 @@ public class RegistrationManager implements Closeable { m.refreshPreKeys(); // Set an initial empty profile so user can be added to groups - m.setProfile(null, null, null, null, null); + try { + m.setProfile(null, null, null, null, null); + } catch (NoClassDefFoundError e) { + logger.warn("Failed to set default profile: {}", e.getMessage()); + } if (response.isStorageCapable()) { m.retrieveRemoteStorage(); } From e3c37a023960b728da773bf0120a86cb60c6c840 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 8 Sep 2021 20:10:07 +0200 Subject: [PATCH 0796/2005] Log error message if libzkgroup or libsignal-client is missing Fixes #660 --- .../org/asamk/signal/manager/config/ServiceConfig.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index 5324439b..3677bba1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -1,6 +1,8 @@ package org.asamk.signal.manager.config; import org.signal.zkgroup.internal.Native; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.push.TrustStore; @@ -15,6 +17,8 @@ import okhttp3.Interceptor; public class ServiceConfig { + private final static Logger logger = LoggerFactory.getLogger(ServiceConfig.class); + public final static int PREKEY_MINIMUM_COUNT = 20; public final static int PREKEY_BATCH_SIZE = 100; public final static int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024; @@ -31,7 +35,8 @@ public class ServiceConfig { try { Native.serverPublicParamsCheckValidContentsJNI(new byte[]{}); zkGroupAvailable = true; - } catch (Throwable ignored) { + } catch (Throwable e) { + logger.warn("Failed to call libzkgroup: {}", e.getMessage()); zkGroupAvailable = false; } capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, true, true); @@ -53,7 +58,8 @@ public class ServiceConfig { try { org.signal.client.internal.Native.DeviceTransfer_GeneratePrivateKey(); return true; - } catch (UnsatisfiedLinkError ignored) { + } catch (UnsatisfiedLinkError e) { + logger.warn("Failed to call libsignal-client: {}", e.getMessage()); return false; } } From 2044a7d7a58ada7ca1e67a80012e3ffdaf86c88c Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 8 Sep 2021 20:38:24 +0200 Subject: [PATCH 0797/2005] Print stack trace of exception causes in verbose mode --- src/main/java/org/asamk/signal/App.java | 10 +++++----- src/main/java/org/asamk/signal/Main.java | 6 +++++- .../org/asamk/signal/commands/AddDeviceCommand.java | 4 ++-- .../java/org/asamk/signal/commands/BlockCommand.java | 4 ++-- .../org/asamk/signal/commands/DaemonCommand.java | 4 ++-- .../asamk/signal/commands/GetUserStatusCommand.java | 3 +-- .../org/asamk/signal/commands/JoinGroupCommand.java | 4 ++-- .../java/org/asamk/signal/commands/LinkCommand.java | 2 +- .../asamk/signal/commands/ListDevicesCommand.java | 8 ++------ .../org/asamk/signal/commands/QuitGroupCommand.java | 2 +- .../org/asamk/signal/commands/ReceiveCommand.java | 4 ++-- .../org/asamk/signal/commands/RegisterCommand.java | 2 +- .../asamk/signal/commands/RemoteDeleteCommand.java | 4 ++-- .../asamk/signal/commands/RemoveDeviceCommand.java | 2 +- .../org/asamk/signal/commands/RemovePinCommand.java | 4 ++-- .../java/org/asamk/signal/commands/SendCommand.java | 12 ++++++------ .../asamk/signal/commands/SendContactsCommand.java | 2 +- .../asamk/signal/commands/SendReactionCommand.java | 4 ++-- .../signal/commands/SendSyncRequestCommand.java | 2 +- .../org/asamk/signal/commands/SetPinCommand.java | 5 +++-- .../org/asamk/signal/commands/UnblockCommand.java | 4 ++-- .../org/asamk/signal/commands/UnregisterCommand.java | 2 +- .../asamk/signal/commands/UpdateAccountCommand.java | 2 +- .../asamk/signal/commands/UpdateContactCommand.java | 2 +- .../asamk/signal/commands/UpdateGroupCommand.java | 4 ++-- .../asamk/signal/commands/UpdateProfileCommand.java | 2 +- .../signal/commands/UploadStickerPackCommand.java | 2 +- .../org/asamk/signal/commands/VerifyCommand.java | 4 ++-- .../signal/commands/exceptions/CommandException.java | 4 ++++ .../signal/commands/exceptions/IOErrorException.java | 6 ++++-- .../exceptions/UnexpectedErrorException.java | 4 ++-- src/main/java/org/asamk/signal/util/ErrorUtils.java | 2 +- 32 files changed, 66 insertions(+), 60 deletions(-) diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 1ff1a909..4aa510d6 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -16,6 +16,7 @@ import org.asamk.signal.commands.ProvisioningCommand; import org.asamk.signal.commands.RegistrationCommand; import org.asamk.signal.commands.SignalCreator; import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.Manager; @@ -225,7 +226,7 @@ public class App { + e.getMessage() + " (" + e.getClass().getSimpleName() - + ")"); + + ")", e); } try (var m = manager) { command.handleCommand(ns, m); @@ -299,20 +300,19 @@ public class App { } catch (NotRegisteredException e) { throw new UserErrorException("User " + username + " is not registered."); } catch (Throwable e) { - logger.debug("Loading state file failed", e); throw new UnexpectedErrorException("Error loading state file for user " + username + ": " + e.getMessage() + " (" + e.getClass().getSimpleName() - + ")"); + + ")", e); } try { manager.checkAccountState(); } catch (IOException e) { - throw new UnexpectedErrorException("Error while checking account " + username + ": " + e.getMessage()); + throw new IOErrorException("Error while checking account " + username + ": " + e.getMessage(), e); } return manager; @@ -337,7 +337,7 @@ public class App { } } catch (DBusException | IOException e) { logger.error("Dbus client failed", e); - throw new UnexpectedErrorException("Dbus client failed"); + throw new UnexpectedErrorException("Dbus client failed", e); } } diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index e0747500..fc63b89e 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -40,7 +40,8 @@ public class Main { installSecurityProviderWorkaround(); // Configuring the logger needs to happen before any logger is initialized - configureLogging(isVerbose(args)); + final var isVerbose = isVerbose(args); + configureLogging(isVerbose); var parser = App.buildArgumentParser(); @@ -51,6 +52,9 @@ public class Main { new App(ns).init(); } catch (CommandException e) { System.err.println(e.getMessage()); + if (isVerbose && e.getCause() != null) { + e.getCause().printStackTrace(); + } status = getStatusForError(e); } System.exit(status); diff --git a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java index 1616a01f..e609ec1e 100644 --- a/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/AddDeviceCommand.java @@ -42,12 +42,12 @@ public class AddDeviceCommand implements JsonRpcLocalCommand { m.addDeviceLink(new URI(ns.getString("uri"))); } catch (IOException e) { logger.error("Add device link failed", e); - throw new IOErrorException("Add device link failed"); + throw new IOErrorException("Add device link failed", e); } catch (URISyntaxException e) { throw new UserErrorException("Device link uri has invalid format: " + e.getMessage()); } catch (InvalidKeyException e) { logger.error("Add device link failed", e); - throw new UnexpectedErrorException("Add device link failed."); + throw new UnexpectedErrorException("Add device link failed.", e); } } } diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 77a622b1..5394022e 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -43,7 +43,7 @@ public class BlockCommand implements JsonRpcLocalCommand { } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); } catch (IOException e) { - throw new UnexpectedErrorException("Failed to sync block to linked devices: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to sync block to linked devices: " + e.getMessage(), e); } } @@ -55,7 +55,7 @@ public class BlockCommand implements JsonRpcLocalCommand { } catch (GroupNotFoundException e) { logger.warn("Group not found {}: {}", groupId.toBase64(), e.getMessage()); } catch (IOException e) { - throw new UnexpectedErrorException("Failed to sync block to linked devices: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to sync block to linked devices: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 0591486c..4a322b99 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -75,7 +75,7 @@ public class DaemonCommand implements MultiLocalCommand { } } catch (DBusException | IOException e) { logger.error("Dbus command failed", e); - throw new UnexpectedErrorException("Dbus command failed"); + throw new UnexpectedErrorException("Dbus command failed", e); } } @@ -113,7 +113,7 @@ public class DaemonCommand implements MultiLocalCommand { signalControl.run(); } catch (DBusException | IOException e) { logger.error("Dbus command failed", e); - throw new UnexpectedErrorException("Dbus command failed"); + throw new UnexpectedErrorException("Dbus command failed", e); } } diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index cf4be085..316f59b1 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -43,8 +43,7 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { try { registered = m.areUsersRegistered(new HashSet<>(ns.getList("recipient"))); } catch (IOException e) { - logger.debug("Failed to check registered users", e); - throw new IOErrorException("Unable to check if users are registered"); + throw new IOErrorException("Unable to check if users are registered", e); } // Output diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index 8c1b9fb2..f5585881 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -78,10 +78,10 @@ public class JoinGroupCommand implements JsonRpcLocalCommand { + e.getMessage() + " (" + e.getClass().getSimpleName() - + ")"); + + ")", e); } catch (DBusExecutionException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } catch (GroupLinkNotActiveException e) { throw new UserErrorException("Group link is not valid: " + e.getMessage()); } diff --git a/src/main/java/org/asamk/signal/commands/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java index 9fcaf04d..fbc03300 100644 --- a/src/main/java/org/asamk/signal/commands/LinkCommand.java +++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java @@ -49,7 +49,7 @@ public class LinkCommand implements ProvisioningCommand { } catch (TimeoutException e) { throw new UserErrorException("Link request timed out, please try again."); } catch (IOException e) { - throw new IOErrorException("Link request error: " + e.getMessage()); + throw new IOErrorException("Link request error: " + e.getMessage(), e); } catch (UserAlreadyExists e) { throw new UserErrorException("The user " + e.getUsername() diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java index 40f30681..ad0d3531 100644 --- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java @@ -40,8 +40,7 @@ public class ListDevicesCommand implements JsonRpcLocalCommand { try { devices = m.getLinkedDevices(); } catch (IOException e) { - logger.debug("Failed to get linked devices", e); - throw new IOErrorException("Failed to get linked devices: " + e.getMessage()); + throw new IOErrorException("Failed to get linked devices: " + e.getMessage(), e); } if (outputWriter instanceof PlainTextWriter) { @@ -71,10 +70,7 @@ public class ListDevicesCommand implements JsonRpcLocalCommand { public final long lastSeenTimestamp; private JsonDevice( - final long id, - final String name, - final long createdTimestamp, - final long lastSeenTimestamp + final long id, final String name, final long createdTimestamp, final long lastSeenTimestamp ) { this.id = id; this.name = name; diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index c64d19cc..67a6596b 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -70,7 +70,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { + e.getMessage() + " (" + e.getClass().getSimpleName() - + ")"); + + ")", e); } catch (GroupNotFoundException e) { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } catch (LastGroupAdminException e) { diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index f248d662..62b3164b 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -126,7 +126,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { } } catch (DBusException e) { logger.error("Dbus client failed", e); - throw new UnexpectedErrorException("Dbus client failed"); + throw new UnexpectedErrorException("Dbus client failed", e); } while (true) { try { @@ -157,7 +157,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { ignoreAttachments, handler); } catch (IOException e) { - throw new IOErrorException("Error while receiving messages: " + e.getMessage()); + throw new IOErrorException("Error while receiving messages: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index dad692d2..96530889 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -49,7 +49,7 @@ public class RegisterCommand implements RegistrationCommand { } throw new UserErrorException(message); } catch (IOException e) { - throw new IOErrorException("Request verify error: " + e.getMessage()); + throw new IOErrorException("Request verify error: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index 6e1e92f7..7d7067c4 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -65,7 +65,7 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } } @@ -106,7 +106,7 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } catch (DBusExecutionException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } } diff --git a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java index 1f47e2b3..d67cc5ea 100644 --- a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java @@ -34,7 +34,7 @@ public class RemoveDeviceCommand implements JsonRpcLocalCommand { int deviceId = ns.getInt("device-id"); m.removeLinkedDevices(deviceId); } catch (IOException e) { - throw new IOErrorException("Error while removing device: " + e.getMessage()); + throw new IOErrorException("Error while removing device: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java index 42ca8880..d1ad276a 100644 --- a/src/main/java/org/asamk/signal/commands/RemovePinCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemovePinCommand.java @@ -32,9 +32,9 @@ public class RemovePinCommand implements JsonRpcLocalCommand { try { m.setRegistrationLockPin(Optional.absent()); } catch (UnauthenticatedResponseException e) { - throw new UnexpectedErrorException("Remove pin failed with unauthenticated response: " + e.getMessage()); + throw new UnexpectedErrorException("Remove pin failed with unauthenticated response: " + e.getMessage(), e); } catch (IOException e) { - throw new IOErrorException("Remove pin error: " + e.getMessage()); + throw new IOErrorException("Remove pin error: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 7ab445fc..1973b1a1 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -86,7 +86,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { return; } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } } @@ -110,7 +110,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { ErrorUtils.handleSendMessageResults(results.getResults()); } catch (AttachmentInvalidException | IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new UserErrorException(e.getMessage()); } @@ -147,7 +147,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { .getSimpleName() + ")"); } catch (DBusExecutionException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } } @@ -176,7 +176,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { outputResult(outputWriter, timestamp); return; } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send group message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send group message: " + e.getMessage(), e); } } @@ -189,7 +189,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() .getSimpleName() + ")"); } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send note to self message: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to send note to self message: " + e.getMessage(), e); } } @@ -203,7 +203,7 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { .getSimpleName() + ")"); } catch (DBusExecutionException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } } diff --git a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java index 07dca322..1cc59bff 100644 --- a/src/main/java/org/asamk/signal/commands/SendContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendContactsCommand.java @@ -29,7 +29,7 @@ public class SendContactsCommand implements JsonRpcLocalCommand { try { m.sendContacts(); } catch (IOException e) { - throw new IOErrorException("SendContacts error: " + e.getMessage()); + throw new IOErrorException("SendContacts error: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 11a16b2e..338e70ac 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -81,7 +81,7 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } } @@ -129,7 +129,7 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { throw new UserErrorException("Failed to send to group: " + e.getMessage()); } catch (DBusExecutionException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } } diff --git a/src/main/java/org/asamk/signal/commands/SendSyncRequestCommand.java b/src/main/java/org/asamk/signal/commands/SendSyncRequestCommand.java index e9a2f94e..aef2d410 100644 --- a/src/main/java/org/asamk/signal/commands/SendSyncRequestCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendSyncRequestCommand.java @@ -29,7 +29,7 @@ public class SendSyncRequestCommand implements JsonRpcLocalCommand { try { m.requestAllSyncData(); } catch (IOException e) { - throw new IOErrorException("Request sync data error: " + e.getMessage()); + throw new IOErrorException("Request sync data error: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/SetPinCommand.java b/src/main/java/org/asamk/signal/commands/SetPinCommand.java index 3636a8b1..ec4a0e3b 100644 --- a/src/main/java/org/asamk/signal/commands/SetPinCommand.java +++ b/src/main/java/org/asamk/signal/commands/SetPinCommand.java @@ -35,9 +35,10 @@ public class SetPinCommand implements JsonRpcLocalCommand { var registrationLockPin = ns.getString("pin"); m.setRegistrationLockPin(Optional.of(registrationLockPin)); } catch (UnauthenticatedResponseException e) { - throw new UnexpectedErrorException("Set pin error failed with unauthenticated response: " + e.getMessage()); + throw new UnexpectedErrorException("Set pin error failed with unauthenticated response: " + e.getMessage(), + e); } catch (IOException e) { - throw new IOErrorException("Set pin error: " + e.getMessage()); + throw new IOErrorException("Set pin error: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 46bd9daa..812065bc 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -42,7 +42,7 @@ public class UnblockCommand implements JsonRpcLocalCommand { } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); } catch (IOException e) { - throw new UnexpectedErrorException("Failed to sync unblock to linked devices: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to sync unblock to linked devices: " + e.getMessage(), e); } } @@ -53,7 +53,7 @@ public class UnblockCommand implements JsonRpcLocalCommand { } catch (GroupNotFoundException e) { logger.warn("Unknown group id: {}", groupId); } catch (IOException e) { - throw new UnexpectedErrorException("Failed to sync unblock to linked devices: " + e.getMessage()); + throw new UnexpectedErrorException("Failed to sync unblock to linked devices: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java index cf09c480..60260046 100644 --- a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java @@ -37,7 +37,7 @@ public class UnregisterCommand implements LocalCommand { m.unregister(); } } catch (IOException e) { - throw new IOErrorException("Unregister error: " + e.getMessage()); + throw new IOErrorException("Unregister error: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java index 600a38c4..e8211aee 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateAccountCommand.java @@ -31,7 +31,7 @@ public class UpdateAccountCommand implements JsonRpcLocalCommand { try { m.updateAccountAttributes(deviceName); } catch (IOException e) { - throw new IOErrorException("UpdateAccount error: " + e.getMessage()); + throw new IOErrorException("UpdateAccount error: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index 8b9f9aa5..6c2916eb 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -46,7 +46,7 @@ public class UpdateContactCommand implements JsonRpcLocalCommand { m.setContactName(recipient, name); } } catch (IOException e) { - throw new IOErrorException("Update contact error: " + e.getMessage()); + throw new IOErrorException("Update contact error: " + e.getMessage(), e); } catch (NotMasterDeviceException e) { throw new UserErrorException("This command doesn't work on linked devices."); } diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 6df70ac2..b0269894 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -175,7 +175,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { throw new UserErrorException(e.getMessage()); } catch (IOException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } } @@ -212,7 +212,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); } catch (DBusExecutionException e) { throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); + .getSimpleName() + ")", e); } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index 15c29e85..f6dcb30e 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -51,7 +51,7 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand { try { m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile); } catch (IOException e) { - throw new IOErrorException("Update profile error: " + e.getMessage()); + throw new IOErrorException("Update profile error: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java index 7af6fed8..53b64b8c 100644 --- a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -50,7 +50,7 @@ public class UploadStickerPackCommand implements JsonRpcLocalCommand { writer.write(Map.of("url", url)); } } catch (IOException e) { - throw new IOErrorException("Upload error (maybe image size too large):" + e.getMessage()); + throw new IOErrorException("Upload error (maybe image size too large):" + e.getMessage(), e); } catch (StickerPackInvalidException e) { throw new UserErrorException("Invalid sticker pack: " + e.getMessage()); } diff --git a/src/main/java/org/asamk/signal/commands/VerifyCommand.java b/src/main/java/org/asamk/signal/commands/VerifyCommand.java index 2f9388ba..b7fffcd2 100644 --- a/src/main/java/org/asamk/signal/commands/VerifyCommand.java +++ b/src/main/java/org/asamk/signal/commands/VerifyCommand.java @@ -44,9 +44,9 @@ public class VerifyCommand implements RegistrationCommand { } catch (KeyBackupServicePinException e) { throw new UserErrorException("Verification failed! Invalid pin, tries remaining: " + e.getTriesRemaining()); } catch (KeyBackupSystemNoDataException e) { - throw new UnexpectedErrorException("Verification failed! No KBS data."); + throw new UnexpectedErrorException("Verification failed! No KBS data.", e); } catch (IOException e) { - throw new IOErrorException("Verify error: " + e.getMessage()); + throw new IOErrorException("Verify error: " + e.getMessage(), e); } } } diff --git a/src/main/java/org/asamk/signal/commands/exceptions/CommandException.java b/src/main/java/org/asamk/signal/commands/exceptions/CommandException.java index c82ef542..bae7af43 100644 --- a/src/main/java/org/asamk/signal/commands/exceptions/CommandException.java +++ b/src/main/java/org/asamk/signal/commands/exceptions/CommandException.java @@ -5,4 +5,8 @@ public class CommandException extends Exception { public CommandException(final String message) { super(message); } + + public CommandException(final String message, final Throwable cause) { + super(message, cause); + } } diff --git a/src/main/java/org/asamk/signal/commands/exceptions/IOErrorException.java b/src/main/java/org/asamk/signal/commands/exceptions/IOErrorException.java index e405600c..91436693 100644 --- a/src/main/java/org/asamk/signal/commands/exceptions/IOErrorException.java +++ b/src/main/java/org/asamk/signal/commands/exceptions/IOErrorException.java @@ -1,8 +1,10 @@ package org.asamk.signal.commands.exceptions; +import java.io.IOException; + public final class IOErrorException extends CommandException { - public IOErrorException(final String message) { - super(message); + public IOErrorException(final String message, IOException cause) { + super(message, cause); } } diff --git a/src/main/java/org/asamk/signal/commands/exceptions/UnexpectedErrorException.java b/src/main/java/org/asamk/signal/commands/exceptions/UnexpectedErrorException.java index b6f231df..7e893d35 100644 --- a/src/main/java/org/asamk/signal/commands/exceptions/UnexpectedErrorException.java +++ b/src/main/java/org/asamk/signal/commands/exceptions/UnexpectedErrorException.java @@ -2,7 +2,7 @@ package org.asamk.signal.commands.exceptions; public final class UnexpectedErrorException extends CommandException { - public UnexpectedErrorException(final String message) { - super(message); + public UnexpectedErrorException(final String message, final Throwable cause) { + super(message, cause); } } diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 8a3de142..39e32198 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -88,6 +88,6 @@ public class ErrorUtils { for (var error : errors) { message.append(error).append("\n"); } - throw new IOErrorException(message.toString()); + throw new IOErrorException(message.toString(), null); } } From 0e6644a8903acb13a72622548d663eeee1c6a762 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 8 Sep 2021 20:53:01 +0200 Subject: [PATCH 0798/2005] Remove unnecessary step from codeql analysis --- .github/workflows/codeql-analysis.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8e91e580..c55e656d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -28,11 +28,6 @@ jobs: # a pull request then we can checkout the head. fetch-depth: 2 - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v1 From a17262d9ff7379c4f1590a3669af52e7237144bb Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 9 Sep 2021 18:54:48 +0200 Subject: [PATCH 0799/2005] Catch ProofRequiredException from getPreKeys request and wrap in SendMessageResult --- .../main/java/org/asamk/signal/manager/helper/SendHelper.java | 3 +++ src/main/java/org/asamk/signal/util/ErrorUtils.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 6ebc0254..89e3eba2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -22,6 +22,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; +import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import java.io.IOException; @@ -282,6 +283,8 @@ public class SendHelper { message, SignalServiceMessageSender.IndividualSendEvents.EMPTY); } + } catch (ProofRequiredException e) { + return SendMessageResult.proofRequiredFailure(address, e); } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); } diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 39e32198..ef1956c3 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -67,7 +67,7 @@ public class ErrorUtils { } else if (result.getProofRequiredFailure() != null) { final var failure = result.getProofRequiredFailure(); return String.format( - "CAPTCHA proof required for sending to \"%s\", available options \"%s\" with token \"%s\", or wait \"%d\" seconds", + "CAPTCHA proof required for sending to \"%s\", available options \"%s\" with challenge token \"%s\", or wait \"%d\" seconds", identifier, failure.getOptions() .stream() From 1856e79a507e97c523f40426e0e30c5c72b50ad7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 9 Sep 2021 18:58:45 +0200 Subject: [PATCH 0800/2005] Add missing check if client zk operations are null Fixes #710 --- .../org/asamk/signal/manager/SignalDependencies.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java b/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java index 970a6741..3478239f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java +++ b/lib/src/main/java/org/asamk/signal/manager/SignalDependencies.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.signal.libsignal.metadata.certificate.CertificateValidator; +import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.KeyBackupService; import org.whispersystems.signalservice.api.SignalServiceAccountManager; @@ -93,6 +94,11 @@ public class SignalDependencies { : null); } + private ClientZkProfileOperations getClientZkProfileOperations() { + final var clientZkOperations = getClientZkOperations(); + return clientZkOperations == null ? null : clientZkOperations.getProfileOperations(); + } + public SignalWebSocket getSignalWebSocket() { return getOrCreate(() -> signalWebSocket, () -> { final var timer = new UptimeSleepTimer(); @@ -126,7 +132,7 @@ public class SignalDependencies { () -> messageReceiver = new SignalServiceMessageReceiver(serviceEnvironmentConfig.getSignalServiceConfiguration(), credentialsProvider, userAgent, - getClientZkOperations().getProfileOperations(), + getClientZkProfileOperations(), ServiceConfig.AUTOMATIC_NETWORK_RETRY)); } @@ -139,7 +145,7 @@ public class SignalDependencies { userAgent, getSignalWebSocket(), Optional.absent(), - getClientZkOperations().getProfileOperations(), + getClientZkProfileOperations(), executor, ServiceConfig.MAX_ENVELOPE_SIZE, ServiceConfig.AUTOMATIC_NETWORK_RETRY)); @@ -156,7 +162,7 @@ public class SignalDependencies { public ProfileService getProfileService() { return getOrCreate(() -> profileService, - () -> profileService = new ProfileService(getClientZkOperations().getProfileOperations(), + () -> profileService = new ProfileService(getClientZkProfileOperations(), getMessageReceiver(), getSignalWebSocket())); } From eee140f74fe9a01972b8d61193c1125c1d89e0df Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 9 Sep 2021 19:20:48 +0200 Subject: [PATCH 0801/2005] Add submitRateLimitChallenge command Related #708 --- .../org/asamk/signal/manager/Manager.java | 4 ++ .../org/asamk/signal/commands/Commands.java | 1 + .../SubmitRateLimitChallengeCommand.java | 44 +++++++++++++++++++ .../org/asamk/signal/util/ErrorUtils.java | 12 ++++- 4 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/asamk/signal/commands/SubmitRateLimitChallengeCommand.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index a7a691cc..a7e80f7c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -405,6 +405,10 @@ public class Manager implements Closeable { account.setRegistered(false); } + public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { + dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); + } + public List getLinkedDevices() throws IOException { var devices = dependencies.getAccountManager().getDevices(); account.setMultiDevice(devices.size() > 1); diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 90e8e114..5d637eee 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -34,6 +34,7 @@ public class Commands { addCommand(new SendSyncRequestCommand()); addCommand(new SendTypingCommand()); addCommand(new SetPinCommand()); + addCommand(new SubmitRateLimitChallengeCommand()); addCommand(new TrustCommand()); addCommand(new UnblockCommand()); addCommand(new UnregisterCommand()); diff --git a/src/main/java/org/asamk/signal/commands/SubmitRateLimitChallengeCommand.java b/src/main/java/org/asamk/signal/commands/SubmitRateLimitChallengeCommand.java new file mode 100644 index 00000000..46f69896 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/SubmitRateLimitChallengeCommand.java @@ -0,0 +1,44 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.OutputWriter; +import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.IOErrorException; +import org.asamk.signal.manager.Manager; + +import java.io.IOException; + +public class SubmitRateLimitChallengeCommand implements JsonRpcLocalCommand { + + @Override + public String getName() { + return "submitRateLimitChallenge"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help( + "Submit a captcha challenge to lift the rate limit. This command should only be necessary when sending fails with a proof required error."); + subparser.addArgument("--challenge") + .required(true) + .help("The challenge token taken from the proof required error."); + subparser.addArgument("--captcha") + .required(true) + .help("The captcha token from the solved captcha on the signal website."); + } + + @Override + public void handleCommand(final Namespace ns, final Manager m, OutputWriter outputWriter) throws CommandException { + final var challenge = ns.getString("challenge"); + final var captchaString = ns.getString("captcha"); + final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", ""); + + try { + m.submitRateLimitRecaptchaChallenge(challenge, captcha); + } catch (IOException e) { + throw new IOErrorException("Submit challenge error: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index ef1956c3..e2454925 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -67,7 +67,17 @@ public class ErrorUtils { } else if (result.getProofRequiredFailure() != null) { final var failure = result.getProofRequiredFailure(); return String.format( - "CAPTCHA proof required for sending to \"%s\", available options \"%s\" with challenge token \"%s\", or wait \"%d\" seconds", + "CAPTCHA proof required for sending to \"%s\", available options \"%s\" with challenge token \"%s\", or wait \"%d\" seconds.\n" + + ( + failure.getOptions().contains(ProofRequiredException.Option.RECAPTCHA) + ? + "To get the captcha token, go to https://signalcaptchas.org/registration/generate.html\n" + + "Check the developer tools (F12) console for a failed redirect to signalcaptcha://\n" + + "Everything after signalcaptcha:// is the captcha token.\n" + + "Use the following command to submit the captcha token:\n" + + "signal-cli submitRateLimitChallenge --challenge CHALLENGE_TOKEN --captcha CAPTCHA_TOKEN" + : "" + ), identifier, failure.getOptions() .stream() From 50e5acdf52139cc607a7bed615e4ab33912c8471 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 10 Sep 2021 10:13:51 +0200 Subject: [PATCH 0802/2005] Fix printing proof required error libsignal-service classifies it as network failure as well. --- .../java/org/asamk/signal/util/ErrorUtils.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index e2454925..8e824d34 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -58,13 +58,7 @@ public class ErrorUtils { public static String getErrorMessageFromSendMessageResult(SendMessageResult result) { var identifier = getLegacyIdentifier(result.getAddress()); - if (result.isNetworkFailure()) { - return String.format("Network failure for \"%s\"", identifier); - } else if (result.isUnregisteredFailure()) { - return String.format("Unregistered user \"%s\"", identifier); - } else if (result.getIdentityFailure() != null) { - return String.format("Untrusted Identity for \"%s\"", identifier); - } else if (result.getProofRequiredFailure() != null) { + if (result.getProofRequiredFailure() != null) { final var failure = result.getProofRequiredFailure(); return String.format( "CAPTCHA proof required for sending to \"%s\", available options \"%s\" with challenge token \"%s\", or wait \"%d\" seconds.\n" @@ -85,6 +79,12 @@ public class ErrorUtils { .collect(Collectors.joining(", ")), failure.getToken(), failure.getRetryAfterSeconds()); + } else if (result.isNetworkFailure()) { + return String.format("Network failure for \"%s\"", identifier); + } else if (result.isUnregisteredFailure()) { + return String.format("Unregistered user \"%s\"", identifier); + } else if (result.getIdentityFailure() != null) { + return String.format("Untrusted Identity for \"%s\"", identifier); } return null; } From 6ac4af4974f2d356c37c3cbd7132bdc4896ea6f0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 10 Sep 2021 17:23:46 +0200 Subject: [PATCH 0803/2005] Fix plain text output for getUserStatus command Fixes #711 --- .../java/org/asamk/signal/commands/GetUserStatusCommand.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index 316f59b1..be94fb36 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -61,7 +61,8 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { final var writer = (PlainTextWriter) outputWriter; for (var entry : registered.entrySet()) { - writer.println("{}: {}", entry.getKey(), entry.getValue()); + final var uuid = entry.getValue().second(); + writer.println("{}: {}", entry.getKey(), uuid != null); } } } From 74e576c907d7af5581203887f8c154baff80b798 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 10 Sep 2021 17:48:44 +0200 Subject: [PATCH 0804/2005] Convert RateLimitException to a network failure send message result --- .../main/java/org/asamk/signal/manager/helper/SendHelper.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 89e3eba2..7b37f205 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -23,6 +23,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException; +import org.whispersystems.signalservice.api.push.exceptions.RateLimitException; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import java.io.IOException; @@ -285,6 +286,9 @@ public class SendHelper { } } catch (ProofRequiredException e) { return SendMessageResult.proofRequiredFailure(address, e); + } catch (RateLimitException e) { + logger.warn("Sending failed due to rate limiting from the signal server: {}", e.getMessage()); + return SendMessageResult.networkFailure(address); } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); } From 2196ac69751e5ac9ffcdfa4d7633b3e3f469d8c4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Sep 2021 11:59:20 +0200 Subject: [PATCH 0805/2005] Extract PreKeyHelper from Manager --- .../org/asamk/signal/manager/Manager.java | 42 +++---------- .../signal/manager/helper/PreKeyHelper.java | 61 +++++++++++++++++++ 2 files changed, 68 insertions(+), 35 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index a7e80f7c..d0deaaed 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -40,6 +40,7 @@ import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; import org.asamk.signal.manager.helper.IncomingMessageHandler; import org.asamk.signal.manager.helper.PinHelper; +import org.asamk.signal.manager.helper.PreKeyHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.StorageHelper; @@ -62,14 +63,11 @@ import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; -import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.fingerprint.Fingerprint; import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; -import org.whispersystems.libsignal.state.PreKeyRecord; -import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; @@ -141,6 +139,7 @@ public class Manager implements Closeable { private final GroupHelper groupHelper; private final ContactHelper contactHelper; private final IncomingMessageHandler incomingMessageHandler; + private final PreKeyHelper preKeyHelper; private final Context context; private boolean hasCaughtUpWithOldMessages = false; @@ -219,6 +218,7 @@ public class Manager implements Closeable { groupHelper, avatarStore, this::resolveSignalServiceAddress); + preKeyHelper = new PreKeyHelper(account, dependencies); this.context = new Context(account, dependencies, @@ -249,10 +249,6 @@ public class Manager implements Closeable { return account.getSelfRecipientId(); } - private IdentityKeyPair getIdentityKeyPair() { - return account.getIdentityKeyPair(); - } - public int getDeviceId() { return account.getDeviceId(); } @@ -309,9 +305,7 @@ public class Manager implements Closeable { days); } } - if (dependencies.getAccountManager().getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { - refreshPreKeys(); - } + preKeyHelper.refreshPreKeysIfNecessary(); if (account.getUuid() == null) { account.setUuid(dependencies.getAccountManager().getOwnUuid()); } @@ -439,7 +433,7 @@ public class Manager implements Closeable { } private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { - var identityKeyPair = getIdentityKeyPair(); + var identityKeyPair = account.getIdentityKeyPair(); var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); dependencies.getAccountManager() @@ -472,29 +466,7 @@ public class Manager implements Closeable { } void refreshPreKeys() throws IOException { - var oneTimePreKeys = generatePreKeys(); - final var identityKeyPair = getIdentityKeyPair(); - var signedPreKeyRecord = generateSignedPreKey(identityKeyPair); - - dependencies.getAccountManager().setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); - } - - private List generatePreKeys() { - final var offset = account.getPreKeyIdOffset(); - - var records = KeyUtils.generatePreKeyRecords(offset, ServiceConfig.PREKEY_BATCH_SIZE); - account.addPreKeys(records); - - return records; - } - - private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) { - final var signedPreKeyId = account.getNextSignedPreKeyId(); - - var record = KeyUtils.generateSignedPreKeyRecord(identityKeyPair, signedPreKeyId); - account.addSignedPreKey(record); - - return record; + preKeyHelper.refreshPreKeys(); } public Profile getRecipientProfile(RecipientId recipientId) { @@ -1175,7 +1147,7 @@ public class Manager implements Closeable { ) { return Utils.computeSafetyNumber(capabilities.isUuid(), account.getSelfAddress(), - getIdentityKeyPair().getPublicKey(), + account.getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java new file mode 100644 index 00000000..f56a7055 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/PreKeyHelper.java @@ -0,0 +1,61 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.util.KeyUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKeyPair; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; + +import java.io.IOException; +import java.util.List; + +public class PreKeyHelper { + + private final static Logger logger = LoggerFactory.getLogger(PreKeyHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + + public PreKeyHelper( + final SignalAccount account, final SignalDependencies dependencies + ) { + this.account = account; + this.dependencies = dependencies; + } + + public void refreshPreKeysIfNecessary() throws IOException { + if (dependencies.getAccountManager().getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { + refreshPreKeys(); + } + } + + public void refreshPreKeys() throws IOException { + var oneTimePreKeys = generatePreKeys(); + final var identityKeyPair = account.getIdentityKeyPair(); + var signedPreKeyRecord = generateSignedPreKey(identityKeyPair); + + dependencies.getAccountManager().setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); + } + + private List generatePreKeys() { + final var offset = account.getPreKeyIdOffset(); + + var records = KeyUtils.generatePreKeyRecords(offset, ServiceConfig.PREKEY_BATCH_SIZE); + account.addPreKeys(records); + + return records; + } + + private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) { + final var signedPreKeyId = account.getNextSignedPreKeyId(); + + var record = KeyUtils.generateSignedPreKeyRecord(identityKeyPair, signedPreKeyId); + account.addSignedPreKey(record); + + return record; + } +} From e3d5ebaa9e062a7d04091797111ba2afade61473 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Sep 2021 12:04:28 +0200 Subject: [PATCH 0806/2005] Refresh prekeys after receiving a pre key message, if necessary --- .../org/asamk/signal/manager/Manager.java | 3 ++- .../manager/actions/RefreshPreKeysAction.java | 20 +++++++++++++++++++ .../helper/IncomingMessageHandler.java | 8 +++++++- .../asamk/signal/manager/jobs/Context.java | 10 +++++++++- 4 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/RefreshPreKeysAction.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index d0deaaed..b74e8660 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -227,7 +227,8 @@ public class Manager implements Closeable { groupHelper, syncHelper, profileHelper, - storageHelper); + storageHelper, + preKeyHelper); var jobExecutor = new JobExecutor(context); this.incomingMessageHandler = new IncomingMessageHandler(account, diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/RefreshPreKeysAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/RefreshPreKeysAction.java new file mode 100644 index 00000000..82d0d290 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/RefreshPreKeysAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class RefreshPreKeysAction implements HandleAction { + + private static final RefreshPreKeysAction INSTANCE = new RefreshPreKeysAction(); + + private RefreshPreKeysAction() { + } + + public static RefreshPreKeysAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getPreKeyHelper().refreshPreKeysIfNecessary(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index e46effc0..81aaf0a8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -6,6 +6,7 @@ import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.TrustLevel; import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.actions.HandleAction; +import org.asamk.signal.manager.actions.RefreshPreKeysAction; import org.asamk.signal.manager.actions.RenewSessionAction; import org.asamk.signal.manager.actions.RetrieveProfileAction; import org.asamk.signal.manager.actions.RetrieveStorageDataAction; @@ -87,6 +88,11 @@ public final class IncomingMessageHandler { final boolean ignoreAttachments, final Manager.ReceiveMessageHandler handler ) { + final List actions = new ArrayList<>(); + if (envelope.isPreKeySignalMessage()) { + actions.add(RefreshPreKeysAction.create()); + } + SignalServiceContent content = null; if (!envelope.isReceipt()) { try { @@ -100,7 +106,7 @@ public final class IncomingMessageHandler { return new Pair<>(List.of(), e); } } - final var actions = checkAndHandleMessage(envelope, content, ignoreAttachments, handler, null); + actions.addAll(checkAndHandleMessage(envelope, content, ignoreAttachments, handler, null)); return new Pair<>(actions, null); } diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java index beb41969..7dd99779 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/Context.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager.jobs; import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.StickerPackStore; import org.asamk.signal.manager.helper.GroupHelper; +import org.asamk.signal.manager.helper.PreKeyHelper; import org.asamk.signal.manager.helper.ProfileHelper; import org.asamk.signal.manager.helper.SendHelper; import org.asamk.signal.manager.helper.StorageHelper; @@ -19,6 +20,7 @@ public class Context { private final SyncHelper syncHelper; private final ProfileHelper profileHelper; private final StorageHelper storageHelper; + private final PreKeyHelper preKeyHelper; public Context( final SignalAccount account, @@ -28,7 +30,8 @@ public class Context { final GroupHelper groupHelper, final SyncHelper syncHelper, final ProfileHelper profileHelper, - final StorageHelper storageHelper + final StorageHelper storageHelper, + final PreKeyHelper preKeyHelper ) { this.account = account; this.dependencies = dependencies; @@ -38,6 +41,7 @@ public class Context { this.syncHelper = syncHelper; this.profileHelper = profileHelper; this.storageHelper = storageHelper; + this.preKeyHelper = preKeyHelper; } public SignalAccount getAccount() { @@ -71,4 +75,8 @@ public class Context { public StorageHelper getStorageHelper() { return storageHelper; } + + public PreKeyHelper getPreKeyHelper() { + return preKeyHelper; + } } From fbafa75fe258026b35bcbc63d1a67503c48e574e Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Sep 2021 13:13:58 +0200 Subject: [PATCH 0807/2005] Store announcement group capability --- .../org/asamk/signal/manager/storage/recipients/Profile.java | 3 ++- .../main/java/org/asamk/signal/manager/util/ProfileUtils.java | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java index 87828953..d61a81b5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java @@ -134,7 +134,8 @@ public class Profile { gv2, storage, gv1Migration, - senderKey; + senderKey, + announcementGroup; static Capability valueOfOrNull(String value) { try { diff --git a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java index f6f76c2c..7ceb07f6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java @@ -65,6 +65,10 @@ public class ProfileUtils { if (encryptedProfile.getCapabilities().isSenderKey()) { capabilities.add(Profile.Capability.senderKey); } + if (encryptedProfile.getCapabilities().isAnnouncementGroup()) { + capabilities.add(Profile.Capability.announcementGroup); + } + return capabilities; } From 62d8873a9288bcfe79d8eb3a2b7b3b467451db72 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Sep 2021 13:13:45 +0200 Subject: [PATCH 0808/2005] Request message resend if incoming message can't be decrypted --- .../org/asamk/signal/manager/Manager.java | 5 +- .../SendRetryMessageRequestAction.java | 89 +++++++++++++++++++ .../helper/IncomingMessageHandler.java | 28 +++++- .../manager/helper/ProfileProvider.java | 2 +- .../signal/manager/helper/SendHelper.java | 20 +++++ .../storage/recipients/RecipientId.java | 5 ++ .../asamk/signal/ReceiveMessageHandler.java | 15 ++++ 7 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index b74e8660..05700379 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -239,6 +239,7 @@ public class Manager implements Closeable { contactHelper, attachmentHelper, syncHelper, + this::getRecipientProfile, jobExecutor); } @@ -876,11 +877,11 @@ public class Manager implements Closeable { // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); }); - logger.debug("New message received from server"); if (result.isPresent()) { envelope = result.get(); + logger.debug("New message received from server"); } else { - // Received indicator that server queue is empty + logger.debug("Received indicator that server queue is empty"); handleQueuedActions(queuedActions); queuedActions.clear(); diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java new file mode 100644 index 00000000..ecd5597d --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java @@ -0,0 +1,89 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.signal.libsignal.metadata.ProtocolException; +import org.whispersystems.libsignal.protocol.CiphertextMessage; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; + +public class SendRetryMessageRequestAction implements HandleAction { + + private final RecipientId recipientId; + private final ProtocolException protocolException; + private final SignalServiceEnvelope envelope; + + public SendRetryMessageRequestAction( + final RecipientId recipientId, + final ProtocolException protocolException, + final SignalServiceEnvelope envelope + ) { + this.recipientId = recipientId; + this.protocolException = protocolException; + this.envelope = envelope; + } + + @Override + public void execute(Context context) throws Throwable { + context.getAccount().getSessionStore().archiveSessions(recipientId); + + int senderDevice = protocolException.getSenderDevice(); + Optional groupId = protocolException.getGroupId().isPresent() ? Optional.of(GroupId.unknownVersion( + protocolException.getGroupId().get())) : Optional.absent(); + + byte[] originalContent; + int envelopeType; + if (protocolException.getUnidentifiedSenderMessageContent().isPresent()) { + final var messageContent = protocolException.getUnidentifiedSenderMessageContent().get(); + originalContent = messageContent.getContent(); + envelopeType = messageContent.getType(); + } else { + originalContent = envelope.getContent(); + envelopeType = envelopeTypeToCiphertextMessageType(envelope.getType()); + } + + DecryptionErrorMessage decryptionErrorMessage = DecryptionErrorMessage.forOriginalMessage(originalContent, + envelopeType, + envelope.getTimestamp(), + senderDevice); + + context.getSendHelper().sendRetryReceipt(decryptionErrorMessage, recipientId, groupId); + } + + private static int envelopeTypeToCiphertextMessageType(int envelopeType) { + switch (envelopeType) { + case SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE: + return CiphertextMessage.PREKEY_TYPE; + case SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE: + return CiphertextMessage.SENDERKEY_TYPE; + case SignalServiceProtos.Envelope.Type.PLAINTEXT_CONTENT_VALUE: + return CiphertextMessage.PLAINTEXT_CONTENT_TYPE; + case SignalServiceProtos.Envelope.Type.CIPHERTEXT_VALUE: + default: + return CiphertextMessage.WHISPER_TYPE; + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final SendRetryMessageRequestAction that = (SendRetryMessageRequestAction) o; + + if (!recipientId.equals(that.recipientId)) return false; + if (!protocolException.equals(that.protocolException)) return false; + return envelope.equals(that.envelope); + } + + @Override + public int hashCode() { + int result = recipientId.hashCode(); + result = 31 * result + protocolException.hashCode(); + result = 31 * result + envelope.hashCode(); + return result; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 81aaf0a8..0917a214 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -13,6 +13,7 @@ import org.asamk.signal.manager.actions.RetrieveStorageDataAction; import org.asamk.signal.manager.actions.SendGroupInfoAction; import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendReceiptAction; +import org.asamk.signal.manager.actions.SendRetryMessageRequestAction; import org.asamk.signal.manager.actions.SendSyncBlockedListAction; import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction; @@ -23,12 +24,17 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.StickerPackId; +import org.signal.libsignal.metadata.ProtocolInvalidKeyException; +import org.signal.libsignal.metadata.ProtocolInvalidKeyIdException; import org.signal.libsignal.metadata.ProtocolInvalidMessageException; +import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; +import org.signal.libsignal.metadata.SelfSendException; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; @@ -59,6 +65,7 @@ public final class IncomingMessageHandler { private final ContactHelper contactHelper; private final AttachmentHelper attachmentHelper; private final SyncHelper syncHelper; + private final ProfileProvider profileProvider; private final JobExecutor jobExecutor; public IncomingMessageHandler( @@ -70,6 +77,7 @@ public final class IncomingMessageHandler { final ContactHelper contactHelper, final AttachmentHelper attachmentHelper, final SyncHelper syncHelper, + final ProfileProvider profileProvider, final JobExecutor jobExecutor ) { this.account = account; @@ -80,6 +88,7 @@ public final class IncomingMessageHandler { this.contactHelper = contactHelper; this.attachmentHelper = attachmentHelper; this.syncHelper = syncHelper; + this.profileProvider = profileProvider; this.jobExecutor = jobExecutor; } @@ -131,11 +140,24 @@ public final class IncomingMessageHandler { actions.add(new RetrieveProfileAction(recipientId)); exception = new UntrustedIdentityException(addressResolver.resolveSignalServiceAddress(recipientId), e.getSenderDevice()); - } catch (ProtocolInvalidMessageException e) { + } catch (ProtocolInvalidKeyIdException | ProtocolInvalidKeyException | ProtocolNoSessionException | ProtocolInvalidMessageException e) { final var sender = account.getRecipientStore().resolveRecipient(e.getSender()); - logger.debug("Received invalid message, queuing renew session action."); - actions.add(new RenewSessionAction(sender)); + final var senderProfile = profileProvider.getProfile(sender); + final var selfProfile = profileProvider.getProfile(account.getSelfRecipientId()); + if (senderProfile != null + && senderProfile.getCapabilities().contains(Profile.Capability.senderKey) + && selfProfile != null + && selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) { + logger.debug("Received invalid message, requesting message resend."); + actions.add(new SendRetryMessageRequestAction(sender, e, envelope)); + } else { + logger.debug("Received invalid message, queuing renew session action."); + actions.add(new RenewSessionAction(sender)); + } exception = e; + } catch (SelfSendException e) { + logger.debug("Dropping unidentified message from self."); + return new Pair<>(List.of(), null); } catch (Exception e) { exception = e; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java index 22915a95..5216b030 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileProvider.java @@ -5,5 +5,5 @@ import org.asamk.signal.manager.storage.recipients.RecipientId; public interface ProfileProvider { - Profile getProfile(RecipientId address); + Profile getProfile(RecipientId recipientId); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 7b37f205..c0953f1f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -13,6 +13,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalServiceMessageSender; import org.whispersystems.signalservice.api.crypto.ContentHint; @@ -156,6 +157,25 @@ public class SendHelper { } } + public void sendRetryReceipt( + DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional groupId + ) throws IOException, UntrustedIdentityException { + var messageSender = dependencies.getMessageSender(); + final var address = addressResolver.resolveSignalServiceAddress(recipientId); + logger.debug("Sending retry receipt for {} to {}, device: {}", + errorMessage.getTimestamp(), + recipientId, + errorMessage.getDeviceId()); + try { + messageSender.sendRetryReceipt(address, + unidentifiedAccessHelper.getAccessFor(recipientId), + groupId.transform(GroupId::serialize), + errorMessage); + } catch (org.whispersystems.signalservice.api.crypto.UntrustedIdentityException e) { + throw new UntrustedIdentityException(address); + } + } + public SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException { var messageSender = dependencies.getMessageSender(); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientId.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientId.java index 9d22d672..f093ca33 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientId.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientId.java @@ -16,6 +16,11 @@ public class RecipientId { return id; } + @Override + public String toString() { + return "RecipientId{" + "id=" + id + '}'; + } + @Override public boolean equals(final Object o) { if (this == o) return true; diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 15dbd1af..bc9244f8 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -8,6 +8,7 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.util.DateUtils; import org.asamk.signal.util.Util; import org.slf4j.helpers.MessageFormatter; +import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; @@ -113,6 +114,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { var typingMessage = content.getTypingMessage().get(); printTypingMessage(writer.indentedWriter(), typingMessage); } + if (content.getDecryptionErrorMessage().isPresent()) { + writer.println("Received a decryption error message (resend request)"); + var decryptionErrorMessage = content.getDecryptionErrorMessage().get(); + printDecryptionErrorMessage(writer.indentedWriter(), decryptionErrorMessage); + } } } else { writer.println("Unknown message received."); @@ -215,6 +221,15 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { } } + private void printDecryptionErrorMessage( + final PlainTextWriter writer, final DecryptionErrorMessage decryptionErrorMessage + ) { + writer.println("Device id: {}", decryptionErrorMessage.getDeviceId()); + writer.println("Timestamp: {}", DateUtils.formatTimestamp(decryptionErrorMessage.getTimestamp())); + writer.println("Ratchet key: {}", + decryptionErrorMessage.getRatchetKey().isPresent() ? "is present" : "not present"); + } + private void printReceiptMessage( final PlainTextWriter writer, final SignalServiceReceiptMessage receiptMessage ) { From f48593f26551f7719beda57c755eab30e1e89703 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Sep 2021 14:37:56 +0200 Subject: [PATCH 0809/2005] Exit immediately if an uncaught error is thrown on the main thread --- src/main/java/org/asamk/signal/Main.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index fc63b89e..26079ec6 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -56,6 +56,9 @@ public class Main { e.getCause().printStackTrace(); } status = getStatusForError(e); + } catch (Throwable e) { + e.printStackTrace(); + status = 2; } System.exit(status); } From 882e45de5522adb235ced3e40b9cc39592e6d3b0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Sep 2021 14:48:01 +0200 Subject: [PATCH 0810/2005] Update graalvm config --- graalvm-config-dir/jni-config.json | 23 +++++++++++++++++++++++ graalvm-config-dir/reflect-config.json | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/graalvm-config-dir/jni-config.json b/graalvm-config-dir/jni-config.json index d18c13e2..8c8c30f5 100644 --- a/graalvm-config-dir/jni-config.json +++ b/graalvm-config-dir/jni-config.json @@ -17,6 +17,10 @@ "name":"java.lang.UnsatisfiedLinkError", "methods":[{"name":"","parameterTypes":["java.lang.String"] }] }, +{ + "name":"java.util.UUID", + "methods":[{"name":"","parameterTypes":["long","long"] }] +}, { "name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader" }, @@ -28,10 +32,12 @@ {"name":"getLocalRegistrationId","parameterTypes":[] }, {"name":"isTrustedIdentity","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.IdentityKey","org.whispersystems.libsignal.state.IdentityKeyStore$Direction"] }, {"name":"loadPreKey","parameterTypes":["int"] }, + {"name":"loadSenderKey","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","java.util.UUID"] }, {"name":"loadSession","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress"] }, {"name":"loadSignedPreKey","parameterTypes":["int"] }, {"name":"removePreKey","parameterTypes":["int"] }, {"name":"saveIdentity","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.IdentityKey"] }, + {"name":"storeSenderKey","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","java.util.UUID","org.whispersystems.libsignal.groups.state.SenderKeyRecord"] }, {"name":"storeSession","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.state.SessionRecord"] } ] }, @@ -66,10 +72,24 @@ "name":"org.whispersystems.libsignal.UntrustedIdentityException", "methods":[{"name":"","parameterTypes":["java.lang.String"] }] }, +{ + "name":"org.whispersystems.libsignal.groups.state.SenderKeyRecord", + "methods":[ + {"name":"","parameterTypes":["long"] }, + {"name":"nativeHandle","parameterTypes":[] } + ] +}, +{ + "name":"org.whispersystems.libsignal.groups.state.SenderKeyStore" +}, { "name":"org.whispersystems.libsignal.logging.Log", "methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }] }, +{ + "name":"org.whispersystems.libsignal.protocol.PlaintextContent", + "methods":[{"name":"nativeHandle","parameterTypes":[] }] +}, { "name":"org.whispersystems.libsignal.protocol.PreKeySignalMessage", "methods":[ @@ -77,6 +97,9 @@ {"name":"nativeHandle","parameterTypes":[] } ] }, +{ + "name":"org.whispersystems.libsignal.protocol.SenderKeyMessage" +}, { "name":"org.whispersystems.libsignal.protocol.SignalMessage", "methods":[ diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 2db7538b..819eafa7 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -697,6 +697,18 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore$Storage", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore$Storage$SharedSenderKey", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.asamk.signal.manager.storage.stickers.StickerStore", "allDeclaredFields":true, @@ -1505,6 +1517,12 @@ "name":"org.whispersystems.signalservice.api.push.SignedPreKeyEntity$ByteArraySerializer", "methods":[{"name":"","parameterTypes":[] }] }, +{ + "name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.whispersystems.signalservice.internal.contacts.crypto.SignatureBodyEntity", "allDeclaredFields":true, From cbff7217c1a3af50fe16aaf37f616c9fd9a8eb44 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Sep 2021 17:47:02 +0200 Subject: [PATCH 0811/2005] Bump version --- CHANGELOG.md | 21 ++++++++++++++++++--- README.md | 1 + build.gradle.kts | 2 +- man/signal-cli.1.adoc | 2 ++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa27507d..26b96784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,31 @@ # Changelog ## [Unreleased] + +## [0.9.0] - 2021-09-12 +**Attention**: Now requires native libsignal-client version 0.9 + ### Breaking changes -- Removed deprecated `--json` parameter, use `--output=json` instead +- Removed deprecated `--json` parameter, use global parameter `--output=json` instead - Json output format of `listGroups` command changed: - Members are now arrays of `{"number":"...","uuid":"..."}` instead of arrays of strings. + Members are now arrays of `{"number":"...","uuid":"..."}` objects instead of arrays of strings. - Removed deprecated fallback data paths, only `$XDG_DATA_HOME/signal-cli` is used now For those still using the old paths (`$HOME/.config/signal`, `$HOME/.config/textsecure`) you need to move those to the new location. ### Added - New global parameter `--trust-new-identities=always` to allow trusting any new identity key without verification -- New parameter `--device-name` for `updateAccount` command to update the device name +- New parameter `--device-name` for `updateAccount` command to change the device name (also works for the main device) +- New SignalControl DBus interface, to register/verify/link new accounts +- New `jsonRpc` command that provides a JSON-RPC based API on stdout/stdin +- Support for announcement groups +- New parameter `--set-permission-send-messages` for `updateGroup` to create an announcement group +- New `sendReceipt` command to send read and viewed receipts +- Support for receiving sender key messages, mobile apps can now send messages more efficiently with server-side fan-out to groups with signal-cli members. +- Support for reading data from remote Signal storage. Now v2 groups will be shown after linking a new device. +- New `submitRateLimitChallenge` command that can be used to lift some rate-limits by solving a captcha + +### Fixed +- Store identity key correctly when sending a message after a recipient has changed keys ## [0.8.5] - 2021-08-07 ### Added diff --git a/README.md b/README.md index 9658dcba..178573fe 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ signal-cli is a commandline interface for [libsignal-service-java](https://githu To be able to link to an existing Signal-Android/signal-cli instance, signal-cli uses a [patched libsignal-service-java](https://github.com/AsamK/libsignal-service-java), because libsignal-service-java does not yet support [provisioning as a linked device](https://github.com/WhisperSystems/libsignal-service-java/pull/21). For registering you need a phone number where you can receive SMS or incoming calls. signal-cli is primarily intended to be used on servers to notify admins of important events. For this use-case, it has a dbus interface ([man page](https://github.com/AsamK/signal-cli/blob/master/man/signal-cli-dbus.5.adoc)), that can be used to send messages from any programming language that has dbus bindings. +It also has a JSON-RPC based interface, see the [documentation](https://github.com/AsamK/signal-cli/wiki/JSON-RPC-service) for more information. ## Installation diff --git a/build.gradle.kts b/build.gradle.kts index c7de229c..37b94492 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ plugins { `check-lib-versions` } -version = "0.8.5" +version = "0.9.0" java { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index b52612be..573ade7c 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -362,6 +362,8 @@ Trust all known keys of this user, only use this for testing. *-v* VERIFIED_SAFETY_NUMBER, *--verified-safety-number* VERIFIED_SAFETY_NUMBER:: Specify the safety number of the key, only use this option if you have verified the safety number. +Can be either the plain text numbers shown in the app or the bytes from the QR-code, +encoded as base64. === updateProfile From 627a587952391414c66403e45aa46261f16f0f4f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Sep 2021 19:08:47 +0200 Subject: [PATCH 0812/2005] Use official graalvm native-image gradle plugin --- README.md | 4 ++-- build.gradle.kts | 49 ++++++++++-------------------------------------- run_tests.sh | 9 +++++++-- 3 files changed, 19 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 178573fe..b63733d1 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,9 @@ This is still experimental and will not work in all situations. 2. [Install prerequisites](https://www.graalvm.org/reference-manual/native-image/#prerequisites) 3. Execute Gradle: - ./gradlew assembleNativeImage + ./gradlew nativeCompile - The binary is available at *build/native-image/signal-cli* + The binary is available at *build/native/nativeCompile/signal-cli* ## FAQ and Troubleshooting For frequently asked questions and issues have a look at the [wiki](https://github.com/AsamK/signal-cli/wiki/FAQ) diff --git a/build.gradle.kts b/build.gradle.kts index 37b94492..0ef51bda 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,6 +3,7 @@ plugins { application eclipse `check-lib-versions` + id("org.graalvm.buildtools.native") version "0.9.5" } version = "0.9.0" @@ -16,6 +17,15 @@ application { mainClass.set("org.asamk.signal.Main") } +graalvmNative { + binaries { + this["main"].run { + configurationFileDirectories.from(file("graalvm-config-dir")) + buildArgs.add("--allow-incomplete-classpath") + } + } +} + repositories { mavenLocal() mavenCentral() @@ -63,42 +73,3 @@ tasks.withType { args = groovy.util.Eval.me(appArgs) as MutableList } } - -val assembleNativeImage by tasks.registering { - dependsOn("assemble") - - var graalVMHome = "" - doFirst { - graalVMHome = System.getenv("GRAALVM_HOME") - ?: throw GradleException("Required GRAALVM_HOME environment variable not set.") - } - - doLast { - val nativeBinaryOutputPath = "$buildDir/native-image" - val nativeBinaryName = "signal-cli" - - mkdir(nativeBinaryOutputPath) - - exec { - workingDir = File(".") - commandLine( - "$graalVMHome/bin/native-image", - "-H:Path=$nativeBinaryOutputPath", - "-H:Name=$nativeBinaryName", - "-H:JNIConfigurationFiles=graalvm-config-dir/jni-config.json", - "-H:DynamicProxyConfigurationFiles=graalvm-config-dir/proxy-config.json", - "-H:ResourceConfigurationFiles=graalvm-config-dir/resource-config.json", - "-H:ReflectionConfigurationFiles=graalvm-config-dir/reflect-config.json", - "--no-fallback", - "--allow-incomplete-classpath", - "--report-unsupported-elements-at-runtime", - "--enable-url-protocols=http,https", - "--enable-https", - "--enable-all-security-services", - "-cp", - sourceSets.main.get().runtimeClasspath.asPath, - application.mainClass.get() - ) - } - } -} diff --git a/run_tests.sh b/run_tests.sh index 5978eed9..d306dfa9 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -11,17 +11,22 @@ if [ ! -z "$GRAALVM_HOME" ]; then export JAVA_HOME=$GRAALVM_HOME export SIGNAL_CLI_OPTS='-agentlib:native-image-agent=config-merge-dir=graalvm-config-dir/' fi -export SIGNAL_CLI="$PWD/build/install/signal-cli/bin/signal-cli" NUMBER_1="$1" NUMBER_2="$2" TEST_PIN_1=456test_pin_foo123 +NATIVE=1 PATH_TEST_CONFIG="$PWD/build/test-config" PATH_MAIN="$PATH_TEST_CONFIG/main" PATH_LINK="$PATH_TEST_CONFIG/link" -./gradlew installDist +if [ "$NATIVE" -eq 1 ]; then + SIGNAL_CLI="$PWD/build/native/nativeCompile/signal-cli" +else + ./gradlew installDist + SIGNAL_CLI="$PWD/build/install/signal-cli/bin/signal-cli" +fi run() { set -x From 12e85ec6718ecd1176d54f52b9061ecec6624b77 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 12 Sep 2021 19:20:21 +0200 Subject: [PATCH 0813/2005] Remove custom -PappArgs handling, gradle now supports --args --- README.md | 4 ++++ build.gradle.kts | 9 --------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b63733d1..9a11ee6e 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ dependencies. If you have a recent gradle version installed, you can replace `./ ./gradlew distTar +5. Compile and run signal-cli: + + ./gradlew run --args="--help" + ### Building a native binary with GraalVM (EXPERIMENTAL) It is possible to build a native binary with [GraalVM](https://www.graalvm.org). diff --git a/build.gradle.kts b/build.gradle.kts index 0ef51bda..51b2ef75 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -64,12 +64,3 @@ tasks.withType { ) } } - -tasks.withType { - val appArgs: String? by project - if (appArgs != null) { - // allow passing command-line arguments to the main application e.g.: - // $ gradle run -PappArgs="['-u', '+...', 'daemon', '--json']" - args = groovy.util.Eval.me(appArgs) as MutableList - } -} From 11b3758416ea395d04b8538a28be07c240749767 Mon Sep 17 00:00:00 2001 From: JtheSaw <21310929+JtheSaw@users.noreply.github.com> Date: Mon, 13 Sep 2021 17:01:26 +0200 Subject: [PATCH 0814/2005] Add sendTyping and sendReceipt to dbus interface (#718) * Add sendTyping and sendReceipt to dbus interface * Resolve requested changes * Adapt documentation --- man/signal-cli-dbus.5.adoc | 14 ++++++++ src/main/java/org/asamk/Signal.java | 8 +++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 35 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index ece2460f..4ff5e994 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -33,6 +33,7 @@ Where is according to DBus specification: * : Byte Array * : Array of Byte Arrays * : String Array +* : Array of signed 64 bit integer * : Boolean (0|1) * : Signed 64 bit integer * <> : no return value @@ -125,6 +126,19 @@ Depending on the type of the recipient field this sends a message to one or mult Exceptions: AttachmentInvalid, Failure, InvalidNumber, UntrustedIdentity +sendTyping(recipient, stop) -> <>:: +* recipient : Phone number of a single recipient +* targetSentTimestamp : True, if typing state should be stopped + +Exceptions: Failure, GroupNotFound, UntrustedIdentity + + +sendReadReceipt(recipient, targetSentTimestamp) -> <>:: +* recipient : Phone number of a single recipient +* targetSentTimestamp : Array of Longs to identify the corresponding signal messages + +Exceptions: Failure, UntrustedIdentity + sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, groupId) -> timestamp:: * emoji : Unicode grapheme cluster of the emoji * remove : Boolean, whether a previously sent reaction (emoji) should be removed diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index cd101929..868de02b 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -21,6 +21,14 @@ public interface Signal extends DBusInterface { String message, List attachments, List recipients ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity; + void sendTyping( + String recipient, boolean stop + ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity; + + void sendReadReceipt( + String recipient, List targetSentTimestamp + ) throws Error.Failure, Error.UntrustedIdentity; + long sendRemoteDeleteMessage( long targetSentTimestamp, String recipient ) throws Error.Failure, Error.InvalidNumber; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 0bb0c435..5e8fd432 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -5,8 +5,10 @@ import org.asamk.signal.BaseConfig; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; +import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupNotFoundException; @@ -165,6 +167,39 @@ public class DbusSignalImpl implements Signal { } } + @Override + public void sendTyping( + final String recipient, final boolean stop + ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity { + try { + var recipients = new ArrayList(1); + recipients.add(recipient); + m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START, + getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + .map(RecipientIdentifier.class::cast) + .collect(Collectors.toSet())); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (UntrustedIdentityException e) { + throw new Error.UntrustedIdentity(e.getMessage()); + } + } + + @Override + public void sendReadReceipt( + final String recipient, final List timestamps + ) throws Error.Failure, Error.UntrustedIdentity { + try { + m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getUsername()), timestamps); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (UntrustedIdentityException e) { + throw new Error.UntrustedIdentity(e.getMessage()); + } + } + @Override public long sendNoteToSelfMessage( final String message, final List attachments From 8e2bb1d393414572fc7d996319b2bcd7ae135a16 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 15 Sep 2021 21:25:46 +0200 Subject: [PATCH 0815/2005] Update FUNDING.yml --- FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/FUNDING.yml b/FUNDING.yml index 308e2dd5..9d269f9a 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1,2 +1,3 @@ liberapay: asamk +ko_fi: asamk bitcoin: bc1qykae53fry8a8ycgdzgv0rlxfc959hmmllvz698 From e562daa1f332f22f13c9856c623f1b830744f70d Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 15 Sep 2021 21:34:05 +0200 Subject: [PATCH 0816/2005] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9a11ee6e..fe435849 100644 --- a/README.md +++ b/README.md @@ -81,15 +81,15 @@ dependencies. If you have a recent gradle version installed, you can replace `./ ./gradlew build -3. Create shell wrapper in *build/install/signal-cli/bin*: + 3a. Create shell wrapper in *build/install/signal-cli/bin*: ./gradlew installDist -4. Create tar file in *build/distributions*: + 3b. Create tar file in *build/distributions*: ./gradlew distTar -5. Compile and run signal-cli: + 3c. Compile and run signal-cli: ./gradlew run --args="--help" From 6c29d90503f7bfcde9fa5a4ae70b617cd041555c Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 15 Sep 2021 21:34:46 +0200 Subject: [PATCH 0817/2005] Adapt visibility --- .../main/java/org/asamk/signal/manager/RegistrationManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 1b00e562..443a7969 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -58,7 +58,7 @@ public class RegistrationManager implements Closeable { private final SignalServiceAccountManager accountManager; private final PinHelper pinHelper; - public RegistrationManager( + private RegistrationManager( SignalAccount account, PathConfig pathConfig, ServiceEnvironmentConfig serviceEnvironmentConfig, From d622967192ce03d3650f2cbc582c6555ab0ca23c Mon Sep 17 00:00:00 2001 From: John Freed Date: Tue, 21 Sep 2021 22:26:26 +0200 Subject: [PATCH 0818/2005] Implement Dbus setPin and removePin (#733) and update documentation --- .gitignore | 1 + man/signal-cli-dbus.5.adoc | 13 ++++++++++ src/main/java/org/asamk/Signal.java | 7 ++++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 24 +++++++++++++++++++ 4 files changed, 45 insertions(+) diff --git a/.gitignore b/.gitignore index 8fa9c8bd..e41d1e40 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ local.properties .settings/ out/ .DS_Store +/bin/ diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 4ff5e994..d562d064 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -229,6 +229,19 @@ isGroupBlocked(groupId) -> state:: Exceptions: None, for unknown groups 0 (false) is returned +removePin() -> <>:: + +Removes registration PIN protection. + +Exception: Failure + +setPin(pin) -> <>:: +* pin : PIN you set after registration (resets after 7 days of inactivity) + +Sets a registration lock PIN, to prevent others from registering your number. + +Exception: Failure + version() -> version:: * version : Version string of signal-cli diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 868de02b..a30f8f3b 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,10 +1,13 @@ package org.asamk; +import org.asamk.Signal.Error; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.messages.DBusSignal; +import org.whispersystems.libsignal.util.guava.Optional; +import java.io.IOException; import java.util.List; /** @@ -87,6 +90,10 @@ public interface Signal extends DBusInterface { String name, String about, String aboutEmoji, String avatarPath, boolean removeAvatar ) throws Error.Failure; + void removePin(); + + void setPin(String registrationLockPin); + String version(); List listNumbers(); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 5e8fd432..44250d5b 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -18,6 +18,7 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Util; + import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -25,6 +26,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; import java.io.File; import java.io.IOException; @@ -413,6 +415,28 @@ public class DbusSignalImpl implements Signal { } } + @Override + public void removePin() { + try { + m.setRegistrationLockPin(Optional.absent()); + } catch (UnauthenticatedResponseException e) { + throw new Error.Failure("Remove pin failed with unauthenticated response: " + e.getMessage()); + } catch (IOException e) { + throw new Error.Failure("Remove pin error: " + e.getMessage()); + } + } + + @Override + public void setPin(String registrationLockPin) { + try { + m.setRegistrationLockPin(Optional.of(registrationLockPin)); + } catch (UnauthenticatedResponseException e) { + throw new Error.Failure("Set pin error failed with unauthenticated response: " + e.getMessage()); + } catch (IOException e) { + throw new Error.Failure("Set pin error: " + e.getMessage()); + } + } + // Provide option to query a version string in order to react on potential // future interface changes @Override From 982e887c9ffaa58cc018fdcdb8ffc5f0251ba04f Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 21 Sep 2021 22:30:27 +0200 Subject: [PATCH 0819/2005] Reformat code --- src/main/java/org/asamk/Signal.java | 3 --- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index a30f8f3b..821e04d9 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,13 +1,10 @@ package org.asamk; -import org.asamk.Signal.Error; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; import org.freedesktop.dbus.messages.DBusSignal; -import org.whispersystems.libsignal.util.guava.Optional; -import java.io.IOException; import java.util.List; /** diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 44250d5b..89703387 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -18,7 +18,6 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Util; - import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -427,7 +426,7 @@ public class DbusSignalImpl implements Signal { } @Override - public void setPin(String registrationLockPin) { + public void setPin(String registrationLockPin) { try { m.setRegistrationLockPin(Optional.of(registrationLockPin)); } catch (UnauthenticatedResponseException e) { From 1ca0e75ef185a5f690162ff82e22732052e9ef57 Mon Sep 17 00:00:00 2001 From: John Freed Date: Sun, 26 Sep 2021 08:59:38 +0200 Subject: [PATCH 0820/2005] implement Dbus stickerpack method (#740) implement uploadStickerPack update documentation --- man/signal-cli-dbus.5.adoc | 6 ++++++ src/main/java/org/asamk/Signal.java | 2 ++ .../java/org/asamk/signal/dbus/DbusSignalImpl.java | 13 +++++++++++++ 3 files changed, 21 insertions(+) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index d562d064..b7dfcfe1 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -248,6 +248,12 @@ version() -> version:: isRegistred -> result:: * result : Currently always returns 1=true +uploadStickerPack(stickerPackPath) -> url:: +* stickerPackPath : Path to the manifest.json file or a zip file in the same directory +* url : URL of sticker pack after successful upload + +Exception: Failure + == Signals SyncMessageReceived (timestamp, sender, destination, groupId,message, attachments):: diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 821e04d9..1eb96510 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -107,6 +107,8 @@ public interface Signal extends DBusInterface { byte[] joinGroup(final String groupLink) throws Error.Failure; + String uploadStickerPack(String stickerPackPath) throws Error.Failure; + class MessageReceived extends DBusSignal { private final long timestamp; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 89703387..0fde767d 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -5,6 +5,7 @@ import org.asamk.signal.BaseConfig; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; +import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -536,6 +537,18 @@ public class DbusSignalImpl implements Signal { } } + @Override + public String uploadStickerPack(String stickerPackPath) { + File path = new File(stickerPackPath); + try { + return m.uploadStickerPack(path).toString(); + } catch (IOException e) { + throw new Error.Failure("Upload error (maybe image size is too large):" + e.getMessage()); + } catch (StickerPackInvalidException e) { + throw new Error.Failure("Invalid sticker pack: " + e.getMessage()); + } + } + private static void checkSendMessageResult(long timestamp, SendMessageResult result) throws DBusExecutionException { var error = ErrorUtils.getErrorMessageFromSendMessageResult(result); From 8bee08fd96571f0f08ffa713b7bd20a2b251d277 Mon Sep 17 00:00:00 2001 From: John Freed Date: Sun, 26 Sep 2021 09:00:26 +0200 Subject: [PATCH 0821/2005] implement Dbus sync methods (#737) implement two Dbus methods: - sendContacts - sendSyncRequest update documentation --- man/signal-cli-dbus.5.adoc | 12 ++++++++++++ src/main/java/org/asamk/Signal.java | 4 ++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 18 ++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index b7dfcfe1..8cc234bc 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -107,6 +107,18 @@ sendGroupMessage(message, attachments, groupId) -> timestamp:: Exceptions: GroupNotFound, Failure, AttachmentInvalid +sendContacts() -> <>:: + +Sends a synchronization message with the local contacts list to all linked devices. This command should only be used if this is the primary device. + +Exceptions: Failure + +sendSyncRequest() -> <>:: + +Sends a synchronization request to the primary device (for group, contacts, ...). Only works if sent from a secondary device. + +Exception: Failure + sendNoteToSelfMessage(message, attachments) -> timestamp:: * message : Text to send (can be UTF8) * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 1eb96510..d981e024 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -49,6 +49,10 @@ public interface Signal extends DBusInterface { String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients ) throws Error.InvalidNumber, Error.Failure; + void sendContacts() throws Error.Failure; + + void sendSyncRequest() throws Error.Failure; + long sendNoteToSelfMessage( String message, List attachments ) throws Error.AttachmentInvalid, Error.Failure; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 0fde767d..4a478f13 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -202,6 +202,24 @@ public class DbusSignalImpl implements Signal { } } + @Override + public void sendContacts() { + try { + m.sendContacts(); + } catch (IOException e) { + throw new Error.Failure("SendContacts error: " + e.getMessage()); + } + } + + @Override + public void sendSyncRequest() { + try { + m.requestAllSyncData(); + } catch (IOException e) { + throw new Error.Failure("Request sync data error: " + e.getMessage()); + } + } + @Override public long sendNoteToSelfMessage( final String message, final List attachments From d47574351e0e27cf308ddacac2b3abf597d34fcb Mon Sep 17 00:00:00 2001 From: John Freed Date: Sun, 26 Sep 2021 09:04:40 +0200 Subject: [PATCH 0822/2005] implement Dbus setExpirationTimer (#735) implement method update documentation --- man/signal-cli-dbus.5.adoc | 7 +++++++ src/main/java/org/asamk/Signal.java | 2 ++ src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 9 +++++++++ 3 files changed, 18 insertions(+) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 8cc234bc..12b87d2b 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -61,6 +61,13 @@ updateProfile(newName, about , aboutEmoji , avatar, remove) -> <> Exceptions: Failure + +setExpirationTimer(number, expiration) -> <>:: +* number : Phone number of recipient +* expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration. + +Exceptions: Failure + setContactBlocked(number, block) -> <>:: * number : Phone number affected by method * block : 0=remove block , 1=blocked diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index d981e024..c5839d14 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -71,6 +71,8 @@ public interface Signal extends DBusInterface { void setContactName(String number, String name) throws Error.InvalidNumber; + void setExpirationTimer(final String number, final int expiration) throws Error.Failure; + void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 4a478f13..dfd55f62 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -305,6 +305,15 @@ public class DbusSignalImpl implements Signal { } } + @Override + public void setExpirationTimer(final String number, final int expiration) { + try { + m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getUsername()), expiration); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + @Override public void setContactBlocked(final String number, final boolean blocked) { try { From e78463ea0a81ef326b6adf85a50887de5192fcf7 Mon Sep 17 00:00:00 2001 From: John Freed Date: Sun, 26 Sep 2021 09:26:12 +0200 Subject: [PATCH 0823/2005] implement Dbus updateAccount and listDevices (#730) * implement Dbus updateAccount and listDevices implement updateAccount(deviceName) to change device name implement listDevices update documentation * implement Dbus addDevice and removeDevice update documentation as well * Dbus add/remove/list/update devices modifications responding to requests by AsamK * Dbus incorporating InvalidUri error Co-authored-by: AsamK --- man/signal-cli-dbus.5.adoc | 80 +++++++++++++++++++ src/main/java/org/asamk/Signal.java | 15 ++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 49 ++++++++++++ 3 files changed, 144 insertions(+) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 12b87d2b..5d65c48f 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -44,6 +44,64 @@ Phone numbers always have the format + == Methods +=== Control methods +These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`). +Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to +`/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_). +Only `version()` is activated in single-user mode; the rest are disabled. + +link() -> deviceLinkUri:: +link(newDeviceName) -> deviceLinkUri:: +* newDeviceName : Name to give new device (defaults to "cli" if no name is given) +* deviceLinkUri : URI of newly linked device + +Returns a URI of the form "tsdevice:/?uuid=...". This can be piped to a QR encoder to create a display that +can be captured by a Signal smartphone client. For example: + +`dbus-send --session --dest=org.asamk.Signal --type=method_call --print-reply /org/asamk/Signal org.asamk.Signal.link string:"My secondary client"|tr '\n' '\0'|sed 's/.*string //g'|sed 's/\"//g'|qrencode -s10 -tANSI256` + +Exception: Failure + +listAccounts() -> accountList:: +* accountList : Array of all attached accounts in DBus object path form + +Exceptions: None + +register(number, voiceVerification) -> <>:: +* number : Phone number +* voiceVerification : true = use voice verification; false = use SMS verification + +Exceptions: Failure, InvalidNumber, RequiresCaptcha + +registerWithCaptcha(number, voiceVerification, captcha) -> <>:: +* number : Phone number +* voiceVerification : true = use voice verification; false = use SMS verification +* captcha : Captcha string + +Exceptions: Failure, InvalidNumber, RequiresCaptcha + +verify(number, verificationCode) -> <>:: +* number : Phone number +* verificationCode : Code received from Signal after successful registration request + +Command fails if PIN was set after previous registration; use verifyWithPin instead. + +Exception: Failure, InvalidNumber + +verifyWithPin(number, verificationCode, pin) -> <>:: +* number : Phone number +* verificationCode : Code received from Signal after successful registration request +* pin : PIN you set with setPin command after verifying previous registration + +Exception: Failure, InvalidNumber + +version() -> version:: +* version : Version string of signal-cli + +Exceptions: None + +=== Other methods + updateGroup(groupId, newName, members, avatar) -> groupId:: * groupId : Byte array representing the internal group identifier * newName : New name of group (empty if unchanged) @@ -267,6 +325,28 @@ version() -> version:: isRegistred -> result:: * result : Currently always returns 1=true +addDevice(deviceUri) -> <>:: +* deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app + +Exception: InvalidUri + +listDevices() -> devices:: +* devices : String array of linked devices + +Exception: Failure + +removeDevice(deviceId) -> <>:: +* deviceId : Device ID to remove, obtained from listDevices() command + +Exception: Failure + +updateDeviceName(deviceName) -> <>:: +* deviceName : New name + +Set a new name for this device (main or linked). + +Exception: Failure + uploadStickerPack(stickerPackPath) -> url:: * stickerPackPath : Path to the manifest.json file or a zip file in the same directory * url : URL of sticker pack after successful upload diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index c5839d14..55585c0d 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -89,6 +89,14 @@ public interface Signal extends DBusInterface { boolean isRegistered(); + void addDevice(String uri) throws Error.InvalidUri; + + void removeDevice(int deviceId) throws Error.Failure; + + List listDevices() throws Error.Failure; + + void updateDeviceName(String deviceName) throws Error.Failure; + void updateProfile( String name, String about, String aboutEmoji, String avatarPath, boolean removeAvatar ) throws Error.Failure; @@ -241,6 +249,13 @@ public interface Signal extends DBusInterface { } } + class InvalidUri extends DBusExecutionException { + + public InvalidUri(final String message) { + super(message); + } + } + class Failure extends DBusExecutionException { public Failure(final String message) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index dfd55f62..768f6e89 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -7,6 +7,7 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.UntrustedIdentityException; +import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; @@ -20,6 +21,7 @@ import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; @@ -30,6 +32,8 @@ import org.whispersystems.signalservice.internal.contacts.crypto.Unauthenticated import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -62,6 +66,51 @@ public class DbusSignalImpl implements Signal { return objectPath; } + @Override + public void addDevice(String uri) { + try { + m.addDeviceLink(new URI(uri)); + } catch (IOException | InvalidKeyException e) { + throw new Error.Failure(e.getClass().getSimpleName() + " Add device link failed. " + e.getMessage()); + } catch (URISyntaxException e) { + throw new Error.InvalidUri(e.getClass().getSimpleName() + " Device link uri has invalid format: " + e.getMessage()); + } + } + + @Override + public void removeDevice(int deviceId) { + try { + m.removeLinkedDevices(deviceId); + } catch (IOException e) { + throw new Error.Failure(e.getClass().getSimpleName() + ": Error while removing device: " + e.getMessage()); + } + } + + @Override + public List listDevices() { + List devices; + List results = new ArrayList(); + + try { + devices = m.getLinkedDevices(); + } catch (IOException | Error.Failure e) { + throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); + } + + return devices.stream() + .map(d -> d.getName() == null ? "" : d.getName()) + .collect(Collectors.toList()); + } + + @Override + public void updateDeviceName(String deviceName) { + try { + m.updateAccountAttributes(deviceName); + } catch (IOException | Signal.Error.Failure e) { + throw new Error.Failure("UpdateAccount error: " + e.getMessage()); + } + } + @Override public long sendMessage(final String message, final List attachments, final String recipient) { var recipients = new ArrayList(1); From df8dd54791090b0d9fae82a94af5554f79a7d71d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 26 Sep 2021 09:27:55 +0200 Subject: [PATCH 0824/2005] Reformat code --- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 768f6e89..7e78d85b 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -73,7 +73,9 @@ public class DbusSignalImpl implements Signal { } catch (IOException | InvalidKeyException e) { throw new Error.Failure(e.getClass().getSimpleName() + " Add device link failed. " + e.getMessage()); } catch (URISyntaxException e) { - throw new Error.InvalidUri(e.getClass().getSimpleName() + " Device link uri has invalid format: " + e.getMessage()); + throw new Error.InvalidUri(e.getClass().getSimpleName() + + " Device link uri has invalid format: " + + e.getMessage()); } } @@ -97,9 +99,7 @@ public class DbusSignalImpl implements Signal { throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); } - return devices.stream() - .map(d -> d.getName() == null ? "" : d.getName()) - .collect(Collectors.toList()); + return devices.stream().map(d -> d.getName() == null ? "" : d.getName()).collect(Collectors.toList()); } @Override From 1c4a32fef4a3273099f0bfdd1b0dea72d32324ae Mon Sep 17 00:00:00 2001 From: John Freed Date: Sun, 26 Sep 2021 20:09:57 +0200 Subject: [PATCH 0825/2005] implement Dbus isRegistered() methods (#729) * implement Dbus isRegistered() methods isRegistered(number) returns a boolean isRegistered(numbers) returns an array of Booleans * Dbus isRegistered() methods restore isRegistered() and respond to other requests by AsamK --- man/signal-cli-dbus.5.adoc | 11 +++++-- src/main/java/org/asamk/Signal.java | 6 +++- .../org/asamk/signal/dbus/DbusSignalImpl.java | 29 ++++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 5d65c48f..6b5d1a86 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -322,8 +322,15 @@ Exception: Failure version() -> version:: * version : Version string of signal-cli -isRegistred -> result:: -* result : Currently always returns 1=true +isRegistered() -> result:: +isRegistered(number) -> result:: +isRegistered(numbers) -> results:: +* number : Phone number +* numbers : String array of phone numbers +* result : true=number is registered, false=number is not registered +* results : Boolean array of results + +Exception: InvalidNumber for an incorrectly formatted phone number. For unknown numbers, false is returned, but no exception is raised. If no number is given, returns whether you are registered (presumably true). addDevice(deviceUri) -> <>:: * deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 55585c0d..3bfeb5bd 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -87,7 +87,11 @@ public interface Signal extends DBusInterface { byte[] groupId, String name, List members, String avatar ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId; - boolean isRegistered(); + boolean isRegistered() throws Error.Failure, Error.InvalidNumber; + + boolean isRegistered(String number) throws Error.Failure, Error.InvalidNumber; + + List isRegistered(List numbers) throws Error.Failure, Error.InvalidNumber; void addDevice(String uri) throws Error.InvalidUri; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 7e78d85b..82cd8f8d 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -467,7 +468,33 @@ public class DbusSignalImpl implements Signal { @Override public boolean isRegistered() { - return true; + var result = isRegistered(List.of(m.getUsername())); + return result.get(0); + } + + @Override + public boolean isRegistered(String number) { + var result = isRegistered(List.of(number)); + return result.get(0); + } + + @Override + public List isRegistered(List numbers) { + var results = new ArrayList (); + Map> registered; + if (numbers.isEmpty()) { + return results; + } + try { + registered = m.areUsersRegistered(new HashSet(numbers)); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + for (String number : numbers) { + UUID uuid = registered.get(number).second(); + results.add(uuid != null); + } + return results; } @Override From 375c9d60cf27d10882bd1a4fd5d5f7ca90eca8ed Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 26 Sep 2021 20:16:27 +0200 Subject: [PATCH 0826/2005] Refactor isRegistered --- .../org/asamk/signal/dbus/DbusSignalImpl.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 82cd8f8d..63764a2f 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -468,8 +468,7 @@ public class DbusSignalImpl implements Signal { @Override public boolean isRegistered() { - var result = isRegistered(List.of(m.getUsername())); - return result.get(0); + return true; } @Override @@ -480,21 +479,22 @@ public class DbusSignalImpl implements Signal { @Override public List isRegistered(List numbers) { - var results = new ArrayList (); - Map> registered; + var results = new ArrayList(); if (numbers.isEmpty()) { return results; } + + Map> registered; try { - registered = m.areUsersRegistered(new HashSet(numbers)); + registered = m.areUsersRegistered(new HashSet<>(numbers)); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } - for (String number : numbers) { - UUID uuid = registered.get(number).second(); - results.add(uuid != null); - } - return results; + + return numbers.stream().map(number -> { + var uuid = registered.get(number).second(); + return uuid != null; + }).collect(Collectors.toList()); } @Override From ba817e2ae4147b201fbb3e5eb8c86e359873ec02 Mon Sep 17 00:00:00 2001 From: John Freed Date: Tue, 28 Sep 2021 18:41:10 +0200 Subject: [PATCH 0827/2005] Implement Dbus updateProfile with givenName (#734) two versions of updateProfile implemented: - one with givenName and familyName - one with just name update documentation --- man/signal-cli-dbus.5.adoc | 9 +++++--- src/main/java/org/asamk/Signal.java | 4 ++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 22 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 6b5d1a86..e7cd083f 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -110,12 +110,15 @@ updateGroup(groupId, newName, members, avatar) -> groupId:: Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound -updateProfile(newName, about , aboutEmoji , avatar, remove) -> <>:: -* newName : New name for your own profile (empty if unchanged) +updateProfile(name, about, aboutEmoji , avatar, remove) -> <>:: +updateProfile(givenName, familyName, about, aboutEmoji , avatar, remove) -> <>:: +* name : Name for your own profile (empty if unchanged) +* givenName : Given name for your own profile (empty if unchanged) +* familyName : Family name for your own profile (empty if unchanged) * about : About message for profile (empty if unchanged) * aboutEmoji : Emoji for profile (empty if unchanged) * avatar : Filename of avatar picture for profile (empty if unchanged) -* remove : Set to 1 if the existing avatar picture should be removed +* remove : Set to true if the existing avatar picture should be removed Exceptions: Failure diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 3bfeb5bd..59aa03ce 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -101,6 +101,10 @@ public interface Signal extends DBusInterface { void updateDeviceName(String deviceName) throws Error.Failure; + void updateProfile( + String givenName, String familyName, String about, String aboutEmoji, String avatarPath, boolean removeAvatar + ) throws Error.Failure; + void updateProfile( String name, String about, String aboutEmoji, String avatarPath, boolean removeAvatar ) throws Error.Failure; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 63764a2f..c73918ef 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -497,6 +497,28 @@ public class DbusSignalImpl implements Signal { }).collect(Collectors.toList()); } + @Override + public void updateProfile( + final String givenName, + final String familyName, + final String about, + final String aboutEmoji, + String avatarPath, + final boolean removeAvatar + ) { + try { + if (avatarPath.isEmpty()) { + avatarPath = null; + } + Optional avatarFile = removeAvatar + ? Optional.absent() + : avatarPath == null ? null : Optional.of(new File(avatarPath)); + m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + @Override public void updateProfile( final String name, From 4acab9043c1f479adf735e193f9404a014b52ed7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 18:42:05 +0200 Subject: [PATCH 0828/2005] Reformat code --- src/main/java/org/asamk/Signal.java | 7 ++++++- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 59aa03ce..b19fba8d 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -102,7 +102,12 @@ public interface Signal extends DBusInterface { void updateDeviceName(String deviceName) throws Error.Failure; void updateProfile( - String givenName, String familyName, String about, String aboutEmoji, String avatarPath, boolean removeAvatar + String givenName, + String familyName, + String about, + String aboutEmoji, + String avatarPath, + boolean removeAvatar ) throws Error.Failure; void updateProfile( diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index c73918ef..12cf7d4c 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -505,14 +505,14 @@ public class DbusSignalImpl implements Signal { final String aboutEmoji, String avatarPath, final boolean removeAvatar - ) { + ) { try { if (avatarPath.isEmpty()) { avatarPath = null; } Optional avatarFile = removeAvatar ? Optional.absent() - : avatarPath == null ? null : Optional.of(new File(avatarPath)); + : avatarPath == null ? null : Optional.of(new File(avatarPath)); m.setProfile(givenName, familyName, about, aboutEmoji, avatarFile); } catch (IOException e) { throw new Error.Failure(e.getMessage()); From 7c9fd9d0fb7b303e8194a6de9aed852c488afc25 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 21:11:53 +0200 Subject: [PATCH 0829/2005] Refactor NoteToSelf to singleton class --- .../asamk/signal/manager/api/RecipientIdentifier.java | 9 ++------- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 2 +- src/main/java/org/asamk/signal/util/CommandUtil.java | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java index 4a66cbb3..cb0a08bb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -12,14 +12,9 @@ public abstract class RecipientIdentifier { public static class NoteToSelf extends RecipientIdentifier { - @Override - public boolean equals(final Object obj) { - return obj instanceof NoteToSelf; - } + public static NoteToSelf INSTANCE = new NoteToSelf(); - @Override - public int hashCode() { - return 5; + private NoteToSelf() { } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 12cf7d4c..e975a671 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -276,7 +276,7 @@ public class DbusSignalImpl implements Signal { ) throws Error.AttachmentInvalid, Error.Failure, Error.UntrustedIdentity { try { final var results = m.sendMessage(new Message(message, attachments), - Set.of(new RecipientIdentifier.NoteToSelf())); + Set.of(RecipientIdentifier.NoteToSelf.INSTANCE)); checkSendMessageResults(results.getTimestamp(), results.getResults()); return results.getTimestamp(); } catch (AttachmentInvalidException e) { diff --git a/src/main/java/org/asamk/signal/util/CommandUtil.java b/src/main/java/org/asamk/signal/util/CommandUtil.java index 83674876..18b38a2a 100644 --- a/src/main/java/org/asamk/signal/util/CommandUtil.java +++ b/src/main/java/org/asamk/signal/util/CommandUtil.java @@ -25,7 +25,7 @@ public class CommandUtil { ) throws UserErrorException { final var recipientIdentifiers = new HashSet(); if (isNoteToSelf) { - recipientIdentifiers.add(new RecipientIdentifier.NoteToSelf()); + recipientIdentifiers.add(RecipientIdentifier.NoteToSelf.INSTANCE); } if (recipientStrings != null) { final var localNumber = m.getUsername(); From 1a81bbecbb1d40ef08ab6b3b1913dfe73c678262 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 21:12:37 +0200 Subject: [PATCH 0830/2005] Do not send message resend request to own device --- .../asamk/signal/manager/helper/IncomingMessageHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 0917a214..45173da4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -144,7 +144,8 @@ public final class IncomingMessageHandler { final var sender = account.getRecipientStore().resolveRecipient(e.getSender()); final var senderProfile = profileProvider.getProfile(sender); final var selfProfile = profileProvider.getProfile(account.getSelfRecipientId()); - if (senderProfile != null + if (e.getSenderDevice() != account.getDeviceId() + && senderProfile != null && senderProfile.getCapabilities().contains(Profile.Capability.senderKey) && selfProfile != null && selfProfile.getCapabilities().contains(Profile.Capability.senderKey)) { From b91c162159c7c28d049ceb8889c419791573d3bb Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 15 Sep 2021 21:40:47 +0200 Subject: [PATCH 0831/2005] Extract Manager interface --- .../org/asamk/signal/manager/Manager.java | 1134 ++------------- .../org/asamk/signal/manager/ManagerImpl.java | 1240 +++++++++++++++++ .../signal/manager/ProvisioningManager.java | 6 +- .../signal/manager/RegistrationManager.java | 4 +- 4 files changed, 1324 insertions(+), 1060 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 05700379..d2eb0f8f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,22 +1,5 @@ -/* - Copyright (C) 2015-2021 AsamK and contributors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ package org.asamk.signal.manager; -import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -25,7 +8,6 @@ import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; -import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupLinkState; @@ -34,233 +16,46 @@ import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.manager.helper.AttachmentHelper; -import org.asamk.signal.manager.helper.ContactHelper; -import org.asamk.signal.manager.helper.GroupHelper; -import org.asamk.signal.manager.helper.GroupV2Helper; -import org.asamk.signal.manager.helper.IncomingMessageHandler; -import org.asamk.signal.manager.helper.PinHelper; -import org.asamk.signal.manager.helper.PreKeyHelper; -import org.asamk.signal.manager.helper.ProfileHelper; -import org.asamk.signal.manager.helper.SendHelper; -import org.asamk.signal.manager.helper.StorageHelper; -import org.asamk.signal.manager.helper.SyncHelper; -import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; -import org.asamk.signal.manager.jobs.Context; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; -import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; -import org.asamk.signal.manager.storage.stickers.Sticker; -import org.asamk.signal.manager.storage.stickers.StickerPackId; -import org.asamk.signal.manager.util.KeyUtils; -import org.asamk.signal.manager.util.StickerUtils; -import org.asamk.signal.manager.util.Utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; -import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.fingerprint.Fingerprint; -import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; -import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; -import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceContent; -import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; -import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; -import org.whispersystems.signalservice.api.util.DeviceNameUtil; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; -import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; -import org.whispersystems.signalservice.internal.contacts.crypto.Quote; -import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; -import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; -import org.whispersystems.signalservice.internal.util.Hex; -import org.whispersystems.signalservice.internal.util.Util; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.security.SignatureException; import java.util.Arrays; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Function; import java.util.stream.Collectors; -import static org.asamk.signal.manager.config.ServiceConfig.capabilities; +public interface Manager extends Closeable { -public class Manager implements Closeable { - - private final static Logger logger = LoggerFactory.getLogger(Manager.class); - - private final ServiceEnvironmentConfig serviceEnvironmentConfig; - private final SignalDependencies dependencies; - - private SignalAccount account; - - private final ExecutorService executor = Executors.newCachedThreadPool(); - - private final ProfileHelper profileHelper; - private final PinHelper pinHelper; - private final StorageHelper storageHelper; - private final SendHelper sendHelper; - private final SyncHelper syncHelper; - private final AttachmentHelper attachmentHelper; - private final GroupHelper groupHelper; - private final ContactHelper contactHelper; - private final IncomingMessageHandler incomingMessageHandler; - private final PreKeyHelper preKeyHelper; - - private final Context context; - private boolean hasCaughtUpWithOldMessages = false; - - Manager( - SignalAccount account, - PathConfig pathConfig, - ServiceEnvironmentConfig serviceEnvironmentConfig, - String userAgent - ) { - this.account = account; - this.serviceEnvironmentConfig = serviceEnvironmentConfig; - - final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(), - account.getUsername(), - account.getPassword(), - account.getDeviceId()); - final var sessionLock = new SignalSessionLock() { - private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); - - @Override - public Lock acquire() { - LEGACY_LOCK.lock(); - return LEGACY_LOCK::unlock; - } - }; - this.dependencies = new SignalDependencies(serviceEnvironmentConfig, - userAgent, - credentialsProvider, - account.getSignalProtocolStore(), - executor, - sessionLock); - final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); - final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); - final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); - - this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); - this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); - final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, - account.getProfileStore()::getProfileKey, - this::getRecipientProfile, - this::getSenderCertificate); - this.profileHelper = new ProfileHelper(account, - dependencies, - avatarStore, - account.getProfileStore()::getProfileKey, - unidentifiedAccessHelper::getAccessFor, - this::resolveSignalServiceAddress); - final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, - this::getRecipientProfile, - account::getSelfRecipientId, - dependencies.getGroupsV2Operations(), - dependencies.getGroupsV2Api(), - this::resolveSignalServiceAddress); - this.sendHelper = new SendHelper(account, - dependencies, - unidentifiedAccessHelper, - this::resolveSignalServiceAddress, - account.getRecipientStore(), - this::handleIdentityFailure, - this::getGroup, - this::refreshRegisteredUser); - this.groupHelper = new GroupHelper(account, - dependencies, - attachmentHelper, - sendHelper, - groupV2Helper, - avatarStore, - this::resolveSignalServiceAddress, - account.getRecipientStore()); - this.storageHelper = new StorageHelper(account, dependencies, groupHelper); - this.contactHelper = new ContactHelper(account); - this.syncHelper = new SyncHelper(account, - attachmentHelper, - sendHelper, - groupHelper, - avatarStore, - this::resolveSignalServiceAddress); - preKeyHelper = new PreKeyHelper(account, dependencies); - - this.context = new Context(account, - dependencies, - stickerPackStore, - sendHelper, - groupHelper, - syncHelper, - profileHelper, - storageHelper, - preKeyHelper); - var jobExecutor = new JobExecutor(context); - - this.incomingMessageHandler = new IncomingMessageHandler(account, - dependencies, - account.getRecipientStore(), - this::resolveSignalServiceAddress, - groupHelper, - contactHelper, - attachmentHelper, - syncHelper, - this::getRecipientProfile, - jobExecutor); - } - - public String getUsername() { - return account.getUsername(); - } - - public RecipientId getSelfRecipientId() { - return account.getSelfRecipientId(); - } - - public int getDeviceId() { - return account.getDeviceId(); - } - - public static Manager init( + static Manager init( String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent, - final TrustNewIdentity trustNewIdentity + TrustNewIdentity trustNewIdentity ) throws IOException, NotRegisteredException { var pathConfig = PathConfig.createDefault(settingsPath); @@ -276,10 +71,10 @@ public class Manager implements Closeable { final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent); - return new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + return new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); } - public static List getAllLocalUsernames(File settingsPath) { + static List getAllLocalUsernames(File settingsPath) { var pathConfig = PathConfig.createDefault(settingsPath); final var dataPath = pathConfig.getDataPath(); final var files = dataPath.listFiles(); @@ -295,208 +90,51 @@ public class Manager implements Closeable { .collect(Collectors.toList()); } - public void checkAccountState() throws IOException { - if (account.getLastReceiveTimestamp() == 0) { - logger.info("The Signal protocol expects that incoming messages are regularly received."); - } else { - var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp(); - long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS); - if (days > 7) { - logger.warn( - "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.", - days); - } - } - preKeyHelper.refreshPreKeysIfNecessary(); - if (account.getUuid() == null) { - account.setUuid(dependencies.getAccountManager().getOwnUuid()); - } - updateAccountAttributes(null); - } + String getUsername(); - /** - * This is used for checking a set of phone numbers for registration on Signal - * - * @param numbers The set of phone number in question - * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. - * @throws IOException if its unable to get the contacts to check if they're registered - */ - public Map> areUsersRegistered(Set numbers) throws IOException { - Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { - try { - return PhoneNumberFormatter.formatNumber(n, account.getUsername()); - } catch (InvalidNumberException e) { - return ""; - } - })); + RecipientId getSelfRecipientId(); - // Note "registeredUsers" has no optionals. It only gives us info on users who are registered - var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values() - .stream() - .filter(s -> !s.isEmpty()) - .collect(Collectors.toSet())); + int getDeviceId(); - return numbers.stream().collect(Collectors.toMap(n -> n, n -> { - final var number = canonicalizedNumbers.get(n); - final var uuid = registeredUsers.get(number); - return new Pair<>(number.isEmpty() ? null : number, uuid); - })); - } + void checkAccountState() throws IOException; - public void updateAccountAttributes(String deviceName) throws IOException { - final String encryptedDeviceName; - if (deviceName == null) { - encryptedDeviceName = account.getEncryptedDeviceName(); - } else { - final var privateKey = account.getIdentityKeyPair().getPrivateKey(); - encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); - account.setEncryptedDeviceName(encryptedDeviceName); - } - dependencies.getAccountManager() - .setAccountAttributes(encryptedDeviceName, - null, - account.getLocalRegistrationId(), - true, - null, - account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - capabilities, - account.isDiscoverableByPhoneNumber()); - } + Map> areUsersRegistered(Set numbers) throws IOException; - /** - * @param givenName if null, the previous givenName will be kept - * @param familyName if null, the previous familyName will be kept - * @param about if null, the previous about text will be kept - * @param aboutEmoji if null, the previous about emoji will be kept - * @param avatar if avatar is null the image from the local avatar store is used (if present), - */ - public void setProfile( - String givenName, final String familyName, String about, String aboutEmoji, Optional avatar - ) throws IOException { - profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); - syncHelper.sendSyncFetchProfileMessage(); - } + void updateAccountAttributes(String deviceName) throws IOException; - public void unregister() throws IOException { - // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. - // If this is the master device, other users can't send messages to this number anymore. - // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. - dependencies.getAccountManager().setGcmId(Optional.absent()); + void setProfile( + String givenName, String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException; - account.setRegistered(false); - } + void unregister() throws IOException; - public void deleteAccount() throws IOException { - try { - pinHelper.removeRegistrationLockPin(); - } catch (UnauthenticatedResponseException e) { - logger.warn("Failed to remove registration lock pin"); - } - account.setRegistrationLockPin(null, null); + void deleteAccount() throws IOException; - dependencies.getAccountManager().deleteAccount(); + void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException; - account.setRegistered(false); - } + List getLinkedDevices() throws IOException; - public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { - dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); - } + void removeLinkedDevices(int deviceId) throws IOException; - public List getLinkedDevices() throws IOException { - var devices = dependencies.getAccountManager().getDevices(); - account.setMultiDevice(devices.size() > 1); - var identityKey = account.getIdentityKeyPair().getPrivateKey(); - return devices.stream().map(d -> { - String deviceName = d.getName(); - if (deviceName != null) { - try { - deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey); - } catch (IOException e) { - logger.debug("Failed to decrypt device name, maybe plain text?", e); - } - } - return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen()); - }).collect(Collectors.toList()); - } + void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException; - public void removeLinkedDevices(int deviceId) throws IOException { - dependencies.getAccountManager().removeDevice(deviceId); - var devices = dependencies.getAccountManager().getDevices(); - account.setMultiDevice(devices.size() > 1); - } + void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException; - public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); + Profile getRecipientProfile(RecipientId recipientId); - addDevice(info.deviceIdentifier, info.deviceKey); - } + List getGroups(); - private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { - var identityKeyPair = account.getIdentityKeyPair(); - var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); - - dependencies.getAccountManager() - .addDevice(deviceIdentifier, - deviceKey, - identityKeyPair, - Optional.of(account.getProfileKey().serialize()), - verificationCode); - account.setMultiDevice(true); - } - - public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException { - if (!account.isMasterDevice()) { - throw new RuntimeException("Only master device can set a PIN"); - } - if (pin.isPresent()) { - final var masterKey = account.getPinMasterKey() != null - ? account.getPinMasterKey() - : KeyUtils.createMasterKey(); - - pinHelper.setRegistrationLockPin(pin.get(), masterKey); - - account.setRegistrationLockPin(pin.get(), masterKey); - } else { - // Remove KBS Pin - pinHelper.removeRegistrationLockPin(); - - account.setRegistrationLockPin(null, null); - } - } - - void refreshPreKeys() throws IOException { - preKeyHelper.refreshPreKeys(); - } - - public Profile getRecipientProfile(RecipientId recipientId) { - return profileHelper.getRecipientProfile(recipientId); - } - - public List getGroups() { - return account.getGroupStore().getGroups(); - } - - public SendGroupMessageResults quitGroup( + SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins - ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - final var newAdmins = resolveRecipients(groupAdmins); - return groupHelper.quitGroup(groupId, newAdmins); - } + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException; - public void deleteGroup(GroupId groupId) throws IOException { - groupHelper.deleteGroup(groupId); - } + void deleteGroup(GroupId groupId) throws IOException; - public Pair createGroup( + Pair createGroup( String name, Set members, File avatarFile - ) throws IOException, AttachmentInvalidException { - return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile); - } + ) throws IOException, AttachmentInvalidException; - public SendGroupMessageResults updateGroup( + SendGroupMessageResults updateGroup( GroupId groupId, String name, String description, @@ -511,724 +149,110 @@ public class Manager implements Closeable { File avatarFile, Integer expirationTimer, Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { - return groupHelper.updateGroup(groupId, - name, - description, - members == null ? null : resolveRecipients(members), - removeMembers == null ? null : resolveRecipients(removeMembers), - admins == null ? null : resolveRecipients(admins), - removeAdmins == null ? null : resolveRecipients(removeAdmins), - resetGroupLink, - groupLinkState, - addMemberPermission, - editDetailsPermission, - avatarFile, - expirationTimer, - isAnnouncementGroup); - } + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException; - public Pair joinGroup( + Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl - ) throws IOException, GroupLinkNotActiveException { - return groupHelper.joinGroup(inviteLinkUrl); - } + ) throws IOException, GroupLinkNotActiveException; - public SendMessageResults sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - var results = new HashMap>(); - long timestamp = System.currentTimeMillis(); - messageBuilder.withTimestamp(timestamp); - for (final var recipient : recipients) { - if (recipient instanceof RecipientIdentifier.Single) { - final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); - final var result = sendHelper.sendMessage(messageBuilder, recipientId); - results.put(recipient, List.of(result)); - } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { - final var result = sendHelper.sendSelfMessage(messageBuilder); - results.put(recipient, List.of(result)); - } else if (recipient instanceof RecipientIdentifier.Group) { - final var groupId = ((RecipientIdentifier.Group) recipient).groupId; - final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId); - results.put(recipient, result); - } - } - return new SendMessageResults(timestamp, results); - } + void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - public void sendTypingMessage( - SignalServiceTypingMessage.Action action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - final var timestamp = System.currentTimeMillis(); - for (var recipient : recipients) { - if (recipient instanceof RecipientIdentifier.Single) { - final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); - final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); - sendHelper.sendTypingMessage(message, recipientId); - } else if (recipient instanceof RecipientIdentifier.Group) { - final var groupId = ((RecipientIdentifier.Group) recipient).groupId; - final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); - sendHelper.sendGroupTypingMessage(message, groupId); - } - } - } - - public void sendReadReceipt( + void sendReadReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException, UntrustedIdentityException { - var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, - messageIds, - System.currentTimeMillis()); + ) throws IOException, UntrustedIdentityException; - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); - } - - public void sendViewedReceipt( + void sendViewedReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException, UntrustedIdentityException { - var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, - messageIds, - System.currentTimeMillis()); + ) throws IOException, UntrustedIdentityException; - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); - } - - public SendMessageResults sendMessage( + SendMessageResults sendMessage( Message message, Set recipients - ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - final var messageBuilder = SignalServiceDataMessage.newBuilder(); - applyMessage(messageBuilder, message); - return sendMessage(messageBuilder, recipients); - } + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - private void applyMessage( - final SignalServiceDataMessage.Builder messageBuilder, final Message message - ) throws AttachmentInvalidException, IOException { - messageBuilder.withBody(message.getMessageText()); - final var attachments = message.getAttachments(); - if (attachments != null) { - messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments)); - } - } - - public SendMessageResults sendRemoteDeleteMessage( + SendMessageResults sendRemoteDeleteMessage( long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); - return sendMessage(messageBuilder, recipients); - } + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - public SendMessageResults sendMessageReaction( + SendMessageResults sendMessageReaction( String emoji, boolean remove, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients - ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - var targetAuthorRecipientId = resolveRecipient(targetAuthor); - var reaction = new SignalServiceDataMessage.Reaction(emoji, - remove, - resolveSignalServiceAddress(targetAuthorRecipientId), - targetSentTimestamp); - final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); - return sendMessage(messageBuilder, recipients); - } + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { - var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); + SendMessageResults sendEndSessionMessage(Set recipients) throws IOException; - try { - return sendMessage(messageBuilder, - recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); - } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { - throw new AssertionError(e); - } finally { - for (var recipient : recipients) { - final var recipientId = resolveRecipient(recipient); - account.getSessionStore().deleteAllSessions(recipientId); - } - } - } - - public void setContactName( + void setContactName( RecipientIdentifier.Single recipient, String name - ) throws NotMasterDeviceException, UnregisteredUserException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); - } - contactHelper.setContactName(resolveRecipient(recipient), name); - } + ) throws NotMasterDeviceException, UnregisteredUserException; - public void setContactBlocked( + void setContactBlocked( RecipientIdentifier.Single recipient, boolean blocked - ) throws NotMasterDeviceException, IOException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); - } - contactHelper.setContactBlocked(resolveRecipient(recipient), blocked); - // TODO cycle our profile key - syncHelper.sendBlockedList(); - } + ) throws NotMasterDeviceException, IOException; - public void setGroupBlocked( - final GroupId groupId, final boolean blocked - ) throws GroupNotFoundException, IOException { - groupHelper.setGroupBlocked(groupId, blocked); - // TODO cycle our profile key - syncHelper.sendBlockedList(); - } + void setGroupBlocked( + GroupId groupId, boolean blocked + ) throws GroupNotFoundException, IOException; - /** - * Change the expiration timer for a contact - */ - public void setExpirationTimer( + void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer - ) throws IOException { - var recipientId = resolveRecipient(recipient); - contactHelper.setExpirationTimer(recipientId, messageExpirationTimer); - final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - try { - sendMessage(messageBuilder, Set.of(recipient)); - } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { - throw new AssertionError(e); - } - } + ) throws IOException; - /** - * Upload the sticker pack from path. - * - * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file - * @return if successful, returns the URL to install the sticker pack in the signal app - */ - public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { - var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); + URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException; - var messageSender = dependencies.getMessageSender(); + void requestAllSyncData() throws IOException; - var packKey = KeyUtils.createStickerUploadKey(); - var packIdString = messageSender.uploadStickerManifest(manifest, packKey); - var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString)); - - var sticker = new Sticker(packId, packKey); - account.getStickerStore().updateSticker(sticker); - - try { - return new URI("https", - "signal.art", - "/addstickers/", - "pack_id=" - + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) - + "&pack_key=" - + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)); - } catch (URISyntaxException e) { - throw new AssertionError(e); - } - } - - public void requestAllSyncData() throws IOException { - syncHelper.requestAllSyncData(); - retrieveRemoteStorage(); - } - - void retrieveRemoteStorage() throws IOException { - if (account.getStorageKey() != null) { - storageHelper.readDataFromStorage(); - } - } - - private byte[] getSenderCertificate() { - byte[] certificate; - try { - if (account.isPhoneNumberShared()) { - certificate = dependencies.getAccountManager().getSenderCertificate(); - } else { - certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy(); - } - } catch (IOException e) { - logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); - return null; - } - // TODO cache for a day - return certificate; - } - - private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException { - final var address = resolveSignalServiceAddress(recipientId); - if (!address.getNumber().isPresent()) { - return recipientId; - } - final var number = address.getNumber().get(); - final var uuid = getRegisteredUser(number); - return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); - } - - private UUID getRegisteredUser(final String number) throws IOException { - final Map uuidMap; - try { - uuidMap = getRegisteredUsers(Set.of(number)); - } catch (NumberFormatException e) { - throw new UnregisteredUserException(number, e); - } - final var uuid = uuidMap.get(number); - if (uuid == null) { - throw new UnregisteredUserException(number, null); - } - return uuid; - } - - private Map getRegisteredUsers(final Set numbers) throws IOException { - final Map registeredUsers; - try { - registeredUsers = dependencies.getAccountManager() - .getRegisteredUsers(ServiceConfig.getIasKeyStore(), - numbers, - serviceEnvironmentConfig.getCdsMrenclave()); - } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { - throw new IOException(e); - } - - // Store numbers as recipients so we have the number/uuid association - registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); - - return registeredUsers; - } - - public void sendTypingMessage( - TypingAction action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - sendTypingMessage(action.toSignalService(), recipients); - } - - private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { - Set queuedActions = new HashSet<>(); - for (var cachedMessage : account.getMessageCache().getCachedMessages()) { - var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); - if (actions != null) { - queuedActions.addAll(actions); - } - } - handleQueuedActions(queuedActions); - } - - private List retryFailedReceivedMessage( - final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage - ) { - var envelope = cachedMessage.loadEnvelope(); - if (envelope == null) { - cachedMessage.delete(); - return null; - } - - final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler); - final var actions = result.first(); - final var exception = result.second(); - - if (exception instanceof UntrustedIdentityException) { - if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { - // Envelope is more than a month old, cleaning up. - cachedMessage.delete(); - return null; - } - if (!envelope.hasSourceUuid()) { - final var identifier = ((UntrustedIdentityException) exception).getSender(); - final var recipientId = account.getRecipientStore().resolveRecipient(identifier); - try { - account.getMessageCache().replaceSender(cachedMessage, recipientId); - } catch (IOException ioException) { - logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage()); - } - } - return null; - } - - // If successful and for all other errors that are not recoverable, delete the cached message - cachedMessage.delete(); - return actions; - } - - public void receiveMessages( + void receiveMessages( long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler - ) throws IOException { - retryFailedReceivedMessages(handler, ignoreAttachments); + ) throws IOException; - Set queuedActions = new HashSet<>(); + boolean hasCaughtUpWithOldMessages(); - final var signalWebSocket = dependencies.getSignalWebSocket(); - signalWebSocket.connect(); + boolean isContactBlocked(RecipientIdentifier.Single recipient); - hasCaughtUpWithOldMessages = false; + File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId); - while (!Thread.interrupted()) { - SignalServiceEnvelope envelope; - final CachedMessage[] cachedMessage = {null}; - account.setLastReceiveTimestamp(System.currentTimeMillis()); - logger.debug("Checking for new message from server"); - try { - var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> { - final var recipientId = envelope1.hasSourceUuid() - ? resolveRecipient(envelope1.getSourceAddress()) - : null; - // store message on disk, before acknowledging receipt to the server - cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); - }); - if (result.isPresent()) { - envelope = result.get(); - logger.debug("New message received from server"); - } else { - logger.debug("Received indicator that server queue is empty"); - handleQueuedActions(queuedActions); - queuedActions.clear(); + void sendContacts() throws IOException; - hasCaughtUpWithOldMessages = true; - synchronized (this) { - this.notifyAll(); - } + List> getContacts(); - // Continue to wait another timeout for new messages - continue; - } - } catch (AssertionError e) { - if (e.getCause() instanceof InterruptedException) { - Thread.currentThread().interrupt(); - break; - } else { - throw e; - } - } catch (WebSocketUnavailableException e) { - logger.debug("Pipe unexpectedly unavailable, connecting"); - signalWebSocket.connect(); - continue; - } catch (TimeoutException e) { - if (returnOnTimeout) return; - continue; - } + String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier); - final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); - queuedActions.addAll(result.first()); - final var exception = result.second(); + GroupInfo getGroup(GroupId groupId); - if (hasCaughtUpWithOldMessages) { - handleQueuedActions(queuedActions); - } - if (cachedMessage[0] != null) { - if (exception instanceof UntrustedIdentityException) { - final var address = ((UntrustedIdentityException) exception).getSender(); - final var recipientId = resolveRecipient(address); - if (!envelope.hasSourceUuid()) { - try { - cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); - } catch (IOException ioException) { - logger.warn("Failed to move cached message to recipient folder: {}", - ioException.getMessage()); - } - } - } else { - cachedMessage[0].delete(); - } - } - } - handleQueuedActions(queuedActions); - } + List getIdentities(); - public boolean hasCaughtUpWithOldMessages() { - return hasCaughtUpWithOldMessages; - } + List getIdentities(RecipientIdentifier.Single recipient); - private void handleQueuedActions(final Collection queuedActions) { - var interrupted = false; - for (var action : queuedActions) { - try { - action.execute(context); - } catch (Throwable e) { - if ((e instanceof AssertionError || e instanceof RuntimeException) - && e.getCause() instanceof InterruptedException) { - interrupted = true; - continue; - } - logger.warn("Message action failed.", e); - } - } - if (interrupted) { - Thread.currentThread().interrupt(); - } - } + boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint); - public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { - final RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - return contactHelper.isContactBlocked(recipientId); - } + boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber); - public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { - return attachmentHelper.getAttachmentFile(attachmentId); - } + boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber); - public void sendContacts() throws IOException { - syncHelper.sendContacts(); - } + boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient); - public List> getContacts() { - return account.getContactStore().getContacts(); - } + String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { - final RecipientId recipientId; - try { - recipientId = resolveRecipient(recipientIdentifier); - } catch (UnregisteredUserException e) { - return null; - } + byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - final var contact = account.getContactStore().getContact(recipientId); - if (contact != null && !Util.isEmpty(contact.getName())) { - return contact.getName(); - } + SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address); - final var profile = getRecipientProfile(recipientId); - if (profile != null) { - return profile.getDisplayName(); - } + SignalServiceAddress resolveSignalServiceAddress(UUID uuid); - return null; - } - - public GroupInfo getGroup(GroupId groupId) { - return groupHelper.getGroup(groupId); - } - - public List getIdentities() { - return account.getIdentityKeyStore().getIdentities(); - } - - public List getIdentities(RecipientIdentifier.Single recipient) { - IdentityInfo identity; - try { - identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); - } catch (UnregisteredUserException e) { - identity = null; - } - return identity == null ? List.of() : List.of(identity); - } - - /** - * Trust this the identity with this fingerprint - * - * @param recipient username of the identity - * @param fingerprint Fingerprint - */ - public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - return trustIdentity(recipientId, - identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), - TrustLevel.TRUSTED_VERIFIED); - } - - /** - * Trust this the identity with this safety number - * - * @param recipient username of the identity - * @param safetyNumber Safety number - */ - public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - var address = resolveSignalServiceAddress(recipientId); - return trustIdentity(recipientId, - identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), - TrustLevel.TRUSTED_VERIFIED); - } - - /** - * Trust this the identity with this scannable safety number - * - * @param recipient username of the identity - * @param safetyNumber Scannable safety number - */ - public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - var address = resolveSignalServiceAddress(recipientId); - return trustIdentity(recipientId, identityKey -> { - final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); - try { - return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber); - } catch (FingerprintVersionMismatchException | FingerprintParsingException e) { - return false; - } - }, TrustLevel.TRUSTED_VERIFIED); - } - - /** - * Trust all keys of this identity without verification - * - * @param recipient username of the identity - */ - public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); - } - - private boolean trustIdentity( - RecipientId recipientId, Function verifier, TrustLevel trustLevel - ) { - var identity = account.getIdentityKeyStore().getIdentity(recipientId); - if (identity == null) { - return false; - } - - if (!verifier.apply(identity.getIdentityKey())) { - return false; - } - - account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); - try { - var address = resolveSignalServiceAddress(recipientId); - syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); - } catch (IOException e) { - logger.warn("Failed to send verification sync message: {}", e.getMessage()); - } - - return true; - } - - private void handleIdentityFailure( - final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure - ) { - final var identityKey = identityFailure.getIdentityKey(); - if (identityKey != null) { - final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); - if (newIdentity) { - account.getSessionStore().archiveSessions(recipientId); - } - } else { - // Retrieve profile to get the current identity key from the server - profileHelper.refreshRecipientProfile(recipientId); - } - } - - public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); - } - - public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); - } - - private Fingerprint computeSafetyNumberFingerprint( - final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey - ) { - return Utils.computeSafetyNumber(capabilities.isUuid(), - account.getSelfAddress(), - account.getIdentityKeyPair().getPublicKey(), - theirAddress, - theirIdentityKey); - } - - public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { - return resolveSignalServiceAddress(resolveRecipient(address)); - } - - public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { - return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); - } - - public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { - final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); - if (address.getUuid().isPresent()) { - return address.toSignalServiceAddress(); - } - - // Address in recipient store doesn't have a uuid, this shouldn't happen - // Try to retrieve the uuid from the server - final var number = address.getNumber().get(); - try { - return resolveSignalServiceAddress(getRegisteredUser(number)); - } catch (IOException e) { - logger.warn("Failed to get uuid for e164 number: {}", number, e); - // Return SignalServiceAddress with unknown UUID - return address.toSignalServiceAddress(); - } - } - - private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { - final var recipientIds = new HashSet(recipients.size()); - for (var number : recipients) { - final var recipientId = resolveRecipient(number); - recipientIds.add(recipientId); - } - return recipientIds; - } - - private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { - if (recipient instanceof RecipientIdentifier.Uuid) { - return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); - } else { - final var number = ((RecipientIdentifier.Number) recipient).number; - return account.getRecipientStore().resolveRecipient(number, () -> { - try { - return getRegisteredUser(number); - } catch (IOException e) { - return null; - } - }); - } - } - - private RecipientId resolveRecipient(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipient(address); - } - - private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipientTrusted(address); - } + SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId); @Override - public void close() throws IOException { - close(true); - } + void close() throws IOException; - private void close(boolean closeAccount) throws IOException { - executor.shutdown(); - - dependencies.getSignalWebSocket().disconnect(); - - if (closeAccount && account != null) { - account.close(); - } - account = null; - } - - public interface ReceiveMessageHandler { + interface ReceiveMessageHandler { void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e); } diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java new file mode 100644 index 00000000..d0fab350 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -0,0 +1,1240 @@ +/* + Copyright (C) 2015-2021 AsamK and contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ +package org.asamk.signal.manager; + +import org.asamk.signal.manager.actions.HandleAction; +import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.SendGroupMessageResults; +import org.asamk.signal.manager.api.SendMessageResults; +import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.config.ServiceEnvironmentConfig; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupLinkState; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; +import org.asamk.signal.manager.groups.LastGroupAdminException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.helper.AttachmentHelper; +import org.asamk.signal.manager.helper.ContactHelper; +import org.asamk.signal.manager.helper.GroupHelper; +import org.asamk.signal.manager.helper.GroupV2Helper; +import org.asamk.signal.manager.helper.IncomingMessageHandler; +import org.asamk.signal.manager.helper.PinHelper; +import org.asamk.signal.manager.helper.PreKeyHelper; +import org.asamk.signal.manager.helper.ProfileHelper; +import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.StorageHelper; +import org.asamk.signal.manager.helper.SyncHelper; +import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.manager.storage.messageCache.CachedMessage; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.stickers.Sticker; +import org.asamk.signal.manager.storage.stickers.StickerPackId; +import org.asamk.signal.manager.util.KeyUtils; +import org.asamk.signal.manager.util.StickerUtils; +import org.asamk.signal.manager.util.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; +import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; +import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalSessionLock; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.DeviceNameUtil; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; +import org.whispersystems.signalservice.internal.contacts.crypto.Quote; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; +import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; +import org.whispersystems.signalservice.internal.util.Hex; +import org.whispersystems.signalservice.internal.util.Util; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.SignatureException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.asamk.signal.manager.config.ServiceConfig.capabilities; + +public class ManagerImpl implements Manager { + + private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.class); + + private final ServiceEnvironmentConfig serviceEnvironmentConfig; + private final SignalDependencies dependencies; + + private SignalAccount account; + + private final ExecutorService executor = Executors.newCachedThreadPool(); + + private final ProfileHelper profileHelper; + private final PinHelper pinHelper; + private final StorageHelper storageHelper; + private final SendHelper sendHelper; + private final SyncHelper syncHelper; + private final AttachmentHelper attachmentHelper; + private final GroupHelper groupHelper; + private final ContactHelper contactHelper; + private final IncomingMessageHandler incomingMessageHandler; + private final PreKeyHelper preKeyHelper; + + private final Context context; + private boolean hasCaughtUpWithOldMessages = false; + + ManagerImpl( + SignalAccount account, + PathConfig pathConfig, + ServiceEnvironmentConfig serviceEnvironmentConfig, + String userAgent + ) { + this.account = account; + this.serviceEnvironmentConfig = serviceEnvironmentConfig; + + final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(), + account.getUsername(), + account.getPassword(), + account.getDeviceId()); + final var sessionLock = new SignalSessionLock() { + private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); + + @Override + public Lock acquire() { + LEGACY_LOCK.lock(); + return LEGACY_LOCK::unlock; + } + }; + this.dependencies = new SignalDependencies(serviceEnvironmentConfig, + userAgent, + credentialsProvider, + account.getSignalProtocolStore(), + executor, + sessionLock); + final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); + final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + + this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); + this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); + final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, + account.getProfileStore()::getProfileKey, + this::getRecipientProfile, + this::getSenderCertificate); + this.profileHelper = new ProfileHelper(account, + dependencies, + avatarStore, + account.getProfileStore()::getProfileKey, + unidentifiedAccessHelper::getAccessFor, + this::resolveSignalServiceAddress); + final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, + this::getRecipientProfile, + account::getSelfRecipientId, + dependencies.getGroupsV2Operations(), + dependencies.getGroupsV2Api(), + this::resolveSignalServiceAddress); + this.sendHelper = new SendHelper(account, + dependencies, + unidentifiedAccessHelper, + this::resolveSignalServiceAddress, + account.getRecipientStore(), + this::handleIdentityFailure, + this::getGroup, + this::refreshRegisteredUser); + this.groupHelper = new GroupHelper(account, + dependencies, + attachmentHelper, + sendHelper, + groupV2Helper, + avatarStore, + this::resolveSignalServiceAddress, + account.getRecipientStore()); + this.storageHelper = new StorageHelper(account, dependencies, groupHelper); + this.contactHelper = new ContactHelper(account); + this.syncHelper = new SyncHelper(account, + attachmentHelper, + sendHelper, + groupHelper, + avatarStore, + this::resolveSignalServiceAddress); + preKeyHelper = new PreKeyHelper(account, dependencies); + + this.context = new Context(account, + dependencies, + stickerPackStore, + sendHelper, + groupHelper, + syncHelper, + profileHelper, + storageHelper, + preKeyHelper); + var jobExecutor = new JobExecutor(context); + + this.incomingMessageHandler = new IncomingMessageHandler(account, + dependencies, + account.getRecipientStore(), + this::resolveSignalServiceAddress, + groupHelper, + contactHelper, + attachmentHelper, + syncHelper, + this::getRecipientProfile, + jobExecutor); + } + + @Override + public String getUsername() { + return account.getUsername(); + } + + @Override + public RecipientId getSelfRecipientId() { + return account.getSelfRecipientId(); + } + + @Override + public int getDeviceId() { + return account.getDeviceId(); + } + + @Override + public void checkAccountState() throws IOException { + if (account.getLastReceiveTimestamp() == 0) { + logger.info("The Signal protocol expects that incoming messages are regularly received."); + } else { + var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp(); + long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS); + if (days > 7) { + logger.warn( + "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.", + days); + } + } + preKeyHelper.refreshPreKeysIfNecessary(); + if (account.getUuid() == null) { + account.setUuid(dependencies.getAccountManager().getOwnUuid()); + } + updateAccountAttributes(null); + } + + /** + * This is used for checking a set of phone numbers for registration on Signal + * + * @param numbers The set of phone number in question + * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. + * @throws IOException if its unable to get the contacts to check if they're registered + */ + @Override + public Map> areUsersRegistered(Set numbers) throws IOException { + Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { + try { + return PhoneNumberFormatter.formatNumber(n, account.getUsername()); + } catch (InvalidNumberException e) { + return ""; + } + })); + + // Note "registeredUsers" has no optionals. It only gives us info on users who are registered + var registeredUsers = getRegisteredUsers(canonicalizedNumbers.values() + .stream() + .filter(s -> !s.isEmpty()) + .collect(Collectors.toSet())); + + return numbers.stream().collect(Collectors.toMap(n -> n, n -> { + final var number = canonicalizedNumbers.get(n); + final var uuid = registeredUsers.get(number); + return new Pair<>(number.isEmpty() ? null : number, uuid); + })); + } + + @Override + public void updateAccountAttributes(String deviceName) throws IOException { + final String encryptedDeviceName; + if (deviceName == null) { + encryptedDeviceName = account.getEncryptedDeviceName(); + } else { + final var privateKey = account.getIdentityKeyPair().getPrivateKey(); + encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); + account.setEncryptedDeviceName(encryptedDeviceName); + } + dependencies.getAccountManager() + .setAccountAttributes(encryptedDeviceName, + null, + account.getLocalRegistrationId(), + true, + null, + account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), + capabilities, + account.isDiscoverableByPhoneNumber()); + } + + /** + * @param givenName if null, the previous givenName will be kept + * @param familyName if null, the previous familyName will be kept + * @param about if null, the previous about text will be kept + * @param aboutEmoji if null, the previous about emoji will be kept + * @param avatar if avatar is null the image from the local avatar store is used (if present), + */ + @Override + public void setProfile( + String givenName, final String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException { + profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); + syncHelper.sendSyncFetchProfileMessage(); + } + + @Override + public void unregister() throws IOException { + // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. + // If this is the master device, other users can't send messages to this number anymore. + // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. + dependencies.getAccountManager().setGcmId(Optional.absent()); + + account.setRegistered(false); + } + + @Override + public void deleteAccount() throws IOException { + try { + pinHelper.removeRegistrationLockPin(); + } catch (UnauthenticatedResponseException e) { + logger.warn("Failed to remove registration lock pin"); + } + account.setRegistrationLockPin(null, null); + + dependencies.getAccountManager().deleteAccount(); + + account.setRegistered(false); + } + + @Override + public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { + dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); + } + + @Override + public List getLinkedDevices() throws IOException { + var devices = dependencies.getAccountManager().getDevices(); + account.setMultiDevice(devices.size() > 1); + var identityKey = account.getIdentityKeyPair().getPrivateKey(); + return devices.stream().map(d -> { + String deviceName = d.getName(); + if (deviceName != null) { + try { + deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey); + } catch (IOException e) { + logger.debug("Failed to decrypt device name, maybe plain text?", e); + } + } + return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen()); + }).collect(Collectors.toList()); + } + + @Override + public void removeLinkedDevices(int deviceId) throws IOException { + dependencies.getAccountManager().removeDevice(deviceId); + var devices = dependencies.getAccountManager().getDevices(); + account.setMultiDevice(devices.size() > 1); + } + + @Override + public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { + var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); + + addDevice(info.deviceIdentifier, info.deviceKey); + } + + private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { + var identityKeyPair = account.getIdentityKeyPair(); + var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); + + dependencies.getAccountManager() + .addDevice(deviceIdentifier, + deviceKey, + identityKeyPair, + Optional.of(account.getProfileKey().serialize()), + verificationCode); + account.setMultiDevice(true); + } + + @Override + public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException { + if (!account.isMasterDevice()) { + throw new RuntimeException("Only master device can set a PIN"); + } + if (pin.isPresent()) { + final var masterKey = account.getPinMasterKey() != null + ? account.getPinMasterKey() + : KeyUtils.createMasterKey(); + + pinHelper.setRegistrationLockPin(pin.get(), masterKey); + + account.setRegistrationLockPin(pin.get(), masterKey); + } else { + // Remove KBS Pin + pinHelper.removeRegistrationLockPin(); + + account.setRegistrationLockPin(null, null); + } + } + + void refreshPreKeys() throws IOException { + preKeyHelper.refreshPreKeys(); + } + + @Override + public Profile getRecipientProfile(RecipientId recipientId) { + return profileHelper.getRecipientProfile(recipientId); + } + + @Override + public List getGroups() { + return account.getGroupStore().getGroups(); + } + + @Override + public SendGroupMessageResults quitGroup( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { + final var newAdmins = resolveRecipients(groupAdmins); + return groupHelper.quitGroup(groupId, newAdmins); + } + + @Override + public void deleteGroup(GroupId groupId) throws IOException { + groupHelper.deleteGroup(groupId); + } + + @Override + public Pair createGroup( + String name, Set members, File avatarFile + ) throws IOException, AttachmentInvalidException { + return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile); + } + + @Override + public SendGroupMessageResults updateGroup( + GroupId groupId, + String name, + String description, + Set members, + Set removeMembers, + Set admins, + Set removeAdmins, + boolean resetGroupLink, + GroupLinkState groupLinkState, + GroupPermission addMemberPermission, + GroupPermission editDetailsPermission, + File avatarFile, + Integer expirationTimer, + Boolean isAnnouncementGroup + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { + return groupHelper.updateGroup(groupId, + name, + description, + members == null ? null : resolveRecipients(members), + removeMembers == null ? null : resolveRecipients(removeMembers), + admins == null ? null : resolveRecipients(admins), + removeAdmins == null ? null : resolveRecipients(removeAdmins), + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer, + isAnnouncementGroup); + } + + @Override + public Pair joinGroup( + GroupInviteLinkUrl inviteLinkUrl + ) throws IOException, GroupLinkNotActiveException { + return groupHelper.joinGroup(inviteLinkUrl); + } + + private SendMessageResults sendMessage( + SignalServiceDataMessage.Builder messageBuilder, Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + var results = new HashMap>(); + long timestamp = System.currentTimeMillis(); + messageBuilder.withTimestamp(timestamp); + for (final var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + final var result = sendHelper.sendMessage(messageBuilder, recipientId); + results.put(recipient, List.of(result)); + } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { + final var result = sendHelper.sendSelfMessage(messageBuilder); + results.put(recipient, List.of(result)); + } else if (recipient instanceof RecipientIdentifier.Group) { + final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId); + results.put(recipient, result); + } + } + return new SendMessageResults(timestamp, results); + } + + private void sendTypingMessage( + SignalServiceTypingMessage.Action action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + final var timestamp = System.currentTimeMillis(); + for (var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); + final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + sendHelper.sendTypingMessage(message, recipientId); + } else if (recipient instanceof RecipientIdentifier.Group) { + final var groupId = ((RecipientIdentifier.Group) recipient).groupId; + final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); + sendHelper.sendGroupTypingMessage(message, groupId); + } + } + } + + @Override + public void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + sendTypingMessage(action.toSignalService(), recipients); + } + + @Override + public void sendReadReceipt( + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, + messageIds, + System.currentTimeMillis()); + + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + } + + @Override + public void sendViewedReceipt( + RecipientIdentifier.Single sender, List messageIds + ) throws IOException, UntrustedIdentityException { + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, + messageIds, + System.currentTimeMillis()); + + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); + } + + @Override + public SendMessageResults sendMessage( + Message message, Set recipients + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + final var messageBuilder = SignalServiceDataMessage.newBuilder(); + applyMessage(messageBuilder, message); + return sendMessage(messageBuilder, recipients); + } + + private void applyMessage( + final SignalServiceDataMessage.Builder messageBuilder, final Message message + ) throws AttachmentInvalidException, IOException { + messageBuilder.withBody(message.getMessageText()); + final var attachments = message.getAttachments(); + if (attachments != null) { + messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments)); + } + } + + @Override + public SendMessageResults sendRemoteDeleteMessage( + long targetSentTimestamp, Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); + return sendMessage(messageBuilder, recipients); + } + + @Override + public SendMessageResults sendMessageReaction( + String emoji, + boolean remove, + RecipientIdentifier.Single targetAuthor, + long targetSentTimestamp, + Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + var targetAuthorRecipientId = resolveRecipient(targetAuthor); + var reaction = new SignalServiceDataMessage.Reaction(emoji, + remove, + resolveSignalServiceAddress(targetAuthorRecipientId), + targetSentTimestamp); + final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); + return sendMessage(messageBuilder, recipients); + } + + @Override + public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { + var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); + + try { + return sendMessage(messageBuilder, + recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new AssertionError(e); + } finally { + for (var recipient : recipients) { + final var recipientId = resolveRecipient(recipient); + account.getSessionStore().deleteAllSessions(recipientId); + } + } + } + + @Override + public void setContactName( + RecipientIdentifier.Single recipient, String name + ) throws NotMasterDeviceException, UnregisteredUserException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + contactHelper.setContactName(resolveRecipient(recipient), name); + } + + @Override + public void setContactBlocked( + RecipientIdentifier.Single recipient, boolean blocked + ) throws NotMasterDeviceException, IOException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + contactHelper.setContactBlocked(resolveRecipient(recipient), blocked); + // TODO cycle our profile key + syncHelper.sendBlockedList(); + } + + @Override + public void setGroupBlocked( + final GroupId groupId, final boolean blocked + ) throws GroupNotFoundException, IOException { + groupHelper.setGroupBlocked(groupId, blocked); + // TODO cycle our profile key + syncHelper.sendBlockedList(); + } + + /** + * Change the expiration timer for a contact + */ + @Override + public void setExpirationTimer( + RecipientIdentifier.Single recipient, int messageExpirationTimer + ) throws IOException { + var recipientId = resolveRecipient(recipient); + contactHelper.setExpirationTimer(recipientId, messageExpirationTimer); + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + try { + sendMessage(messageBuilder, Set.of(recipient)); + } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { + throw new AssertionError(e); + } + } + + /** + * Upload the sticker pack from path. + * + * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file + * @return if successful, returns the URL to install the sticker pack in the signal app + */ + @Override + public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { + var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); + + var messageSender = dependencies.getMessageSender(); + + var packKey = KeyUtils.createStickerUploadKey(); + var packIdString = messageSender.uploadStickerManifest(manifest, packKey); + var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString)); + + var sticker = new Sticker(packId, packKey); + account.getStickerStore().updateSticker(sticker); + + try { + return new URI("https", + "signal.art", + "/addstickers/", + "pack_id=" + + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) + + "&pack_key=" + + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + @Override + public void requestAllSyncData() throws IOException { + syncHelper.requestAllSyncData(); + retrieveRemoteStorage(); + } + + void retrieveRemoteStorage() throws IOException { + if (account.getStorageKey() != null) { + storageHelper.readDataFromStorage(); + } + } + + private byte[] getSenderCertificate() { + byte[] certificate; + try { + if (account.isPhoneNumberShared()) { + certificate = dependencies.getAccountManager().getSenderCertificate(); + } else { + certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy(); + } + } catch (IOException e) { + logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); + return null; + } + // TODO cache for a day + return certificate; + } + + private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException { + final var address = resolveSignalServiceAddress(recipientId); + if (!address.getNumber().isPresent()) { + return recipientId; + } + final var number = address.getNumber().get(); + final var uuid = getRegisteredUser(number); + return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); + } + + private UUID getRegisteredUser(final String number) throws IOException { + final Map uuidMap; + try { + uuidMap = getRegisteredUsers(Set.of(number)); + } catch (NumberFormatException e) { + throw new UnregisteredUserException(number, e); + } + final var uuid = uuidMap.get(number); + if (uuid == null) { + throw new UnregisteredUserException(number, null); + } + return uuid; + } + + private Map getRegisteredUsers(final Set numbers) throws IOException { + final Map registeredUsers; + try { + registeredUsers = dependencies.getAccountManager() + .getRegisteredUsers(ServiceConfig.getIasKeyStore(), + numbers, + serviceEnvironmentConfig.getCdsMrenclave()); + } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { + throw new IOException(e); + } + + // Store numbers as recipients so we have the number/uuid association + registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); + + return registeredUsers; + } + + private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { + Set queuedActions = new HashSet<>(); + for (var cachedMessage : account.getMessageCache().getCachedMessages()) { + var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); + if (actions != null) { + queuedActions.addAll(actions); + } + } + handleQueuedActions(queuedActions); + } + + private List retryFailedReceivedMessage( + final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage + ) { + var envelope = cachedMessage.loadEnvelope(); + if (envelope == null) { + cachedMessage.delete(); + return null; + } + + final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler); + final var actions = result.first(); + final var exception = result.second(); + + if (exception instanceof UntrustedIdentityException) { + if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { + // Envelope is more than a month old, cleaning up. + cachedMessage.delete(); + return null; + } + if (!envelope.hasSourceUuid()) { + final var identifier = ((UntrustedIdentityException) exception).getSender(); + final var recipientId = account.getRecipientStore().resolveRecipient(identifier); + try { + account.getMessageCache().replaceSender(cachedMessage, recipientId); + } catch (IOException ioException) { + logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage()); + } + } + return null; + } + + // If successful and for all other errors that are not recoverable, delete the cached message + cachedMessage.delete(); + return actions; + } + + @Override + public void receiveMessages( + long timeout, + TimeUnit unit, + boolean returnOnTimeout, + boolean ignoreAttachments, + ReceiveMessageHandler handler + ) throws IOException { + retryFailedReceivedMessages(handler, ignoreAttachments); + + Set queuedActions = new HashSet<>(); + + final var signalWebSocket = dependencies.getSignalWebSocket(); + signalWebSocket.connect(); + + hasCaughtUpWithOldMessages = false; + + while (!Thread.interrupted()) { + SignalServiceEnvelope envelope; + final CachedMessage[] cachedMessage = {null}; + account.setLastReceiveTimestamp(System.currentTimeMillis()); + logger.debug("Checking for new message from server"); + try { + var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> { + final var recipientId = envelope1.hasSourceUuid() + ? resolveRecipient(envelope1.getSourceAddress()) + : null; + // store message on disk, before acknowledging receipt to the server + cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); + }); + if (result.isPresent()) { + envelope = result.get(); + logger.debug("New message received from server"); + } else { + logger.debug("Received indicator that server queue is empty"); + handleQueuedActions(queuedActions); + queuedActions.clear(); + + hasCaughtUpWithOldMessages = true; + synchronized (this) { + this.notifyAll(); + } + + // Continue to wait another timeout for new messages + continue; + } + } catch (AssertionError e) { + if (e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + break; + } else { + throw e; + } + } catch (WebSocketUnavailableException e) { + logger.debug("Pipe unexpectedly unavailable, connecting"); + signalWebSocket.connect(); + continue; + } catch (TimeoutException e) { + if (returnOnTimeout) return; + continue; + } + + final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); + queuedActions.addAll(result.first()); + final var exception = result.second(); + + if (hasCaughtUpWithOldMessages) { + handleQueuedActions(queuedActions); + } + if (cachedMessage[0] != null) { + if (exception instanceof UntrustedIdentityException) { + final var address = ((UntrustedIdentityException) exception).getSender(); + final var recipientId = resolveRecipient(address); + if (!envelope.hasSourceUuid()) { + try { + cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); + } catch (IOException ioException) { + logger.warn("Failed to move cached message to recipient folder: {}", + ioException.getMessage()); + } + } + } else { + cachedMessage[0].delete(); + } + } + } + handleQueuedActions(queuedActions); + } + + @Override + public boolean hasCaughtUpWithOldMessages() { + return hasCaughtUpWithOldMessages; + } + + private void handleQueuedActions(final Collection queuedActions) { + var interrupted = false; + for (var action : queuedActions) { + try { + action.execute(context); + } catch (Throwable e) { + if ((e instanceof AssertionError || e instanceof RuntimeException) + && e.getCause() instanceof InterruptedException) { + interrupted = true; + continue; + } + logger.warn("Message action failed.", e); + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + @Override + public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + return contactHelper.isContactBlocked(recipientId); + } + + @Override + public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { + return attachmentHelper.getAttachmentFile(attachmentId); + } + + @Override + public void sendContacts() throws IOException { + syncHelper.sendContacts(); + } + + @Override + public List> getContacts() { + return account.getContactStore().getContacts(); + } + + @Override + public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipientIdentifier); + } catch (UnregisteredUserException e) { + return null; + } + + final var contact = account.getContactStore().getContact(recipientId); + if (contact != null && !Util.isEmpty(contact.getName())) { + return contact.getName(); + } + + final var profile = getRecipientProfile(recipientId); + if (profile != null) { + return profile.getDisplayName(); + } + + return null; + } + + @Override + public GroupInfo getGroup(GroupId groupId) { + return groupHelper.getGroup(groupId); + } + + @Override + public List getIdentities() { + return account.getIdentityKeyStore().getIdentities(); + } + + @Override + public List getIdentities(RecipientIdentifier.Single recipient) { + IdentityInfo identity; + try { + identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); + } catch (UnregisteredUserException e) { + identity = null; + } + return identity == null ? List.of() : List.of(identity); + } + + /** + * Trust this the identity with this fingerprint + * + * @param recipient username of the identity + * @param fingerprint Fingerprint + */ + @Override + public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + return trustIdentity(recipientId, + identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), + TrustLevel.TRUSTED_VERIFIED); + } + + /** + * Trust this the identity with this safety number + * + * @param recipient username of the identity + * @param safetyNumber Safety number + */ + @Override + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(recipientId); + return trustIdentity(recipientId, + identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), + TrustLevel.TRUSTED_VERIFIED); + } + + /** + * Trust this the identity with this scannable safety number + * + * @param recipient username of the identity + * @param safetyNumber Scannable safety number + */ + @Override + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(recipientId); + return trustIdentity(recipientId, identityKey -> { + final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); + try { + return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber); + } catch (FingerprintVersionMismatchException | FingerprintParsingException e) { + return false; + } + }, TrustLevel.TRUSTED_VERIFIED); + } + + /** + * Trust all keys of this identity without verification + * + * @param recipient username of the identity + */ + @Override + public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); + } + + private boolean trustIdentity( + RecipientId recipientId, Function verifier, TrustLevel trustLevel + ) { + var identity = account.getIdentityKeyStore().getIdentity(recipientId); + if (identity == null) { + return false; + } + + if (!verifier.apply(identity.getIdentityKey())) { + return false; + } + + account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); + try { + var address = resolveSignalServiceAddress(recipientId); + syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); + } catch (IOException e) { + logger.warn("Failed to send verification sync message: {}", e.getMessage()); + } + + return true; + } + + private void handleIdentityFailure( + final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure + ) { + final var identityKey = identityFailure.getIdentityKey(); + if (identityKey != null) { + final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } + } else { + // Retrieve profile to get the current identity key from the server + profileHelper.refreshRecipientProfile(recipientId); + } + } + + @Override + public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); + } + + @Override + public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); + } + + private Fingerprint computeSafetyNumberFingerprint( + final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey + ) { + return Utils.computeSafetyNumber(capabilities.isUuid(), + account.getSelfAddress(), + account.getIdentityKeyPair().getPublicKey(), + theirAddress, + theirIdentityKey); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { + return resolveSignalServiceAddress(resolveRecipient(address)); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { + return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { + final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); + if (address.getUuid().isPresent()) { + return address.toSignalServiceAddress(); + } + + // Address in recipient store doesn't have a uuid, this shouldn't happen + // Try to retrieve the uuid from the server + final var number = address.getNumber().get(); + try { + return resolveSignalServiceAddress(getRegisteredUser(number)); + } catch (IOException e) { + logger.warn("Failed to get uuid for e164 number: {}", number, e); + // Return SignalServiceAddress with unknown UUID + return address.toSignalServiceAddress(); + } + } + + private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { + final var recipientIds = new HashSet(recipients.size()); + for (var number : recipients) { + final var recipientId = resolveRecipient(number); + recipientIds.add(recipientId); + } + return recipientIds; + } + + private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { + if (recipient instanceof RecipientIdentifier.Uuid) { + return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); + } else { + final var number = ((RecipientIdentifier.Number) recipient).number; + return account.getRecipientStore().resolveRecipient(number, () -> { + try { + return getRegisteredUser(number); + } catch (IOException e) { + return null; + } + }); + } + } + + private RecipientId resolveRecipient(SignalServiceAddress address) { + return account.getRecipientStore().resolveRecipient(address); + } + + private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { + return account.getRecipientStore().resolveRecipientTrusted(address); + } + + @Override + public void close() throws IOException { + close(true); + } + + private void close(boolean closeAccount) throws IOException { + executor.shutdown(); + + dependencies.getSignalWebSocket().disconnect(); + + if (closeAccount && account != null) { + account.close(); + } + account = null; + } + +} diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 90dc6c66..226de9be 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -126,9 +126,9 @@ public class ProvisioningManager { profileKey, TrustNewIdentity.ON_FIRST_USE); - Manager m = null; + ManagerImpl m = null; try { - m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); logger.debug("Refreshing pre keys"); try { @@ -178,7 +178,7 @@ public class ProvisioningManager { return false; } - final var m = new Manager(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent); + final var m = new ManagerImpl(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent); try (m) { m.checkAccountState(); } catch (AuthorizationFailedException ignored) { diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 443a7969..978f1fd5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -177,9 +177,9 @@ public class RegistrationManager implements Closeable { //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.finishRegistration(UuidUtil.parseOrNull(response.getUuid()), masterKey, pin); - Manager m = null; + ManagerImpl m = null; try { - m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); account = null; m.refreshPreKeys(); From d72b838560b1a4186ac121c7d605773b49fcdf46 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 18 Sep 2021 10:19:56 +0200 Subject: [PATCH 0832/2005] Refactor Manager interface --- .../org/asamk/signal/manager/Manager.java | 40 +++--- .../org/asamk/signal/manager/ManagerImpl.java | 123 +++++++++++++----- .../signal/manager/RegistrationManager.java | 8 +- .../signal/manager/UserAlreadyExists.java | 10 +- .../org/asamk/signal/manager/api/Device.java | 8 +- .../org/asamk/signal/manager/api/Group.java | 99 ++++++++++++++ .../asamk/signal/manager/api/Identity.java | 65 +++++++++ .../manager/api/RecipientIdentifier.java | 22 ++++ .../storage/recipients/RecipientAddress.java | 10 ++ src/main/java/org/asamk/Signal.java | 4 +- src/main/java/org/asamk/signal/App.java | 2 +- .../asamk/signal/ReceiveMessageHandler.java | 8 +- .../asamk/signal/commands/BlockCommand.java | 2 +- .../asamk/signal/commands/DaemonCommand.java | 2 +- .../signal/commands/JoinGroupCommand.java | 4 +- .../asamk/signal/commands/LinkCommand.java | 4 +- .../signal/commands/ListContactsCommand.java | 11 +- .../signal/commands/ListDevicesCommand.java | 2 +- .../signal/commands/ListGroupsCommand.java | 49 ++++--- .../commands/ListIdentitiesCommand.java | 18 +-- .../signal/commands/QuitGroupCommand.java | 2 +- .../signal/commands/SendReactionCommand.java | 2 +- .../signal/commands/SendReceiptCommand.java | 2 +- .../signal/commands/SendTypingCommand.java | 2 +- .../asamk/signal/commands/TrustCommand.java | 2 +- .../asamk/signal/commands/UnblockCommand.java | 3 +- .../signal/commands/UpdateContactCommand.java | 2 +- .../signal/commands/UpdateGroupCommand.java | 2 +- .../signal/dbus/DbusSignalControlImpl.java | 2 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 68 +++++----- .../org/asamk/signal/json/JsonMention.java | 3 +- .../signal/json/JsonMessageEnvelope.java | 2 +- .../org/asamk/signal/util/CommandUtil.java | 2 +- 33 files changed, 416 insertions(+), 169 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/Group.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/Identity.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index d2eb0f8f..cba438f8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,6 +1,8 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; @@ -17,12 +19,10 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.SignalAccount; -import org.asamk.signal.manager.storage.groups.GroupInfo; -import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; -import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; @@ -51,7 +51,7 @@ import java.util.stream.Collectors; public interface Manager extends Closeable { static Manager init( - String username, + String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent, @@ -59,11 +59,11 @@ public interface Manager extends Closeable { ) throws IOException, NotRegisteredException { var pathConfig = PathConfig.createDefault(settingsPath); - if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { + if (!SignalAccount.userExists(pathConfig.getDataPath(), number)) { throw new NotRegisteredException(); } - var account = SignalAccount.load(pathConfig.getDataPath(), username, true, trustNewIdentity); + var account = SignalAccount.load(pathConfig.getDataPath(), number, true, trustNewIdentity); if (!account.isRegistered()) { throw new NotRegisteredException(); @@ -74,7 +74,7 @@ public interface Manager extends Closeable { return new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); } - static List getAllLocalUsernames(File settingsPath) { + static List getAllLocalNumbers(File settingsPath) { var pathConfig = PathConfig.createDefault(settingsPath); final var dataPath = pathConfig.getDataPath(); final var files = dataPath.listFiles(); @@ -90,11 +90,7 @@ public interface Manager extends Closeable { .collect(Collectors.toList()); } - String getUsername(); - - RecipientId getSelfRecipientId(); - - int getDeviceId(); + String getSelfNumber(); void checkAccountState() throws IOException; @@ -120,9 +116,9 @@ public interface Manager extends Closeable { void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException; - Profile getRecipientProfile(RecipientId recipientId); + Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredUserException; - List getGroups(); + List getGroups(); SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins @@ -221,15 +217,15 @@ public interface Manager extends Closeable { void sendContacts() throws IOException; - List> getContacts(); + List> getContacts(); - String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier); + String getContactOrProfileName(RecipientIdentifier.Single recipient); - GroupInfo getGroup(GroupId groupId); + Group getGroup(GroupId groupId); - List getIdentities(); + List getIdentities(); - List getIdentities(RecipientIdentifier.Single recipient); + List getIdentities(RecipientIdentifier.Single recipient); boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint); @@ -241,14 +237,8 @@ public interface Manager extends Closeable { String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address); - SignalServiceAddress resolveSignalServiceAddress(UUID uuid); - - SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId); - @Override void close() throws IOException; diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index d0fab350..de60fa50 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -18,6 +18,8 @@ package org.asamk.signal.manager; import org.asamk.signal.manager.actions.HandleAction; import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; @@ -52,6 +54,7 @@ import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.messageCache.CachedMessage; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.StickerPackId; @@ -196,7 +199,7 @@ public class ManagerImpl implements Manager { this::resolveSignalServiceAddress, account.getRecipientStore(), this::handleIdentityFailure, - this::getGroup, + this::getGroupInfo, this::refreshRegisteredUser); this.groupHelper = new GroupHelper(account, dependencies, @@ -240,20 +243,10 @@ public class ManagerImpl implements Manager { } @Override - public String getUsername() { + public String getSelfNumber() { return account.getUsername(); } - @Override - public RecipientId getSelfRecipientId() { - return account.getSelfRecipientId(); - } - - @Override - public int getDeviceId() { - return account.getDeviceId(); - } - @Override public void checkAccountState() throws IOException { if (account.getLastReceiveTimestamp() == 0) { @@ -385,7 +378,11 @@ public class ManagerImpl implements Manager { logger.debug("Failed to decrypt device name, maybe plain text?", e); } } - return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen()); + return new Device(d.getId(), + deviceName, + d.getCreated(), + d.getLastSeen(), + d.getId() == account.getDeviceId()); }).collect(Collectors.toList()); } @@ -442,13 +439,48 @@ public class ManagerImpl implements Manager { } @Override - public Profile getRecipientProfile(RecipientId recipientId) { + public Profile getRecipientProfile(RecipientIdentifier.Single recipient) throws UnregisteredUserException { + return profileHelper.getRecipientProfile(resolveRecipient(recipient)); + } + + private Profile getRecipientProfile(RecipientId recipientId) { return profileHelper.getRecipientProfile(recipientId); } @Override - public List getGroups() { - return account.getGroupStore().getGroups(); + public List getGroups() { + return account.getGroupStore().getGroups().stream().map(this::toGroup).collect(Collectors.toList()); + } + + private Group toGroup(final GroupInfo groupInfo) { + if (groupInfo == null) { + return null; + } + + return new Group(groupInfo.getGroupId(), + groupInfo.getTitle(), + groupInfo.getDescription(), + groupInfo.getGroupInviteLink(), + groupInfo.getMembers() + .stream() + .map(account.getRecipientStore()::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.getPendingMembers() + .stream() + .map(account.getRecipientStore()::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.getRequestingMembers() + .stream() + .map(account.getRecipientStore()::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.getAdminMembers() + .stream() + .map(account.getRecipientStore()::resolveRecipientAddress) + .collect(Collectors.toSet()), + groupInfo.isBlocked(), + groupInfo.getMessageExpirationTime(), + groupInfo.isAnnouncementGroup(), + groupInfo.isMember(account.getSelfRecipientId())); } @Override @@ -973,15 +1005,19 @@ public class ManagerImpl implements Manager { } @Override - public List> getContacts() { - return account.getContactStore().getContacts(); + public List> getContacts() { + return account.getContactStore() + .getContacts() + .stream() + .map(p -> new Pair<>(account.getRecipientStore().resolveRecipientAddress(p.first()), p.second())) + .collect(Collectors.toList()); } @Override - public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { + public String getContactOrProfileName(RecipientIdentifier.Single recipient) { final RecipientId recipientId; try { - recipientId = resolveRecipient(recipientIdentifier); + recipientId = resolveRecipient(recipient); } catch (UnregisteredUserException e) { return null; } @@ -1000,24 +1036,46 @@ public class ManagerImpl implements Manager { } @Override - public GroupInfo getGroup(GroupId groupId) { + public Group getGroup(GroupId groupId) { + return toGroup(groupHelper.getGroup(groupId)); + } + + public GroupInfo getGroupInfo(GroupId groupId) { return groupHelper.getGroup(groupId); } @Override - public List getIdentities() { - return account.getIdentityKeyStore().getIdentities(); + public List getIdentities() { + return account.getIdentityKeyStore() + .getIdentities() + .stream() + .map(this::toIdentity) + .collect(Collectors.toList()); + } + + private Identity toIdentity(final IdentityInfo identityInfo) { + if (identityInfo == null) { + return null; + } + + final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId()); + return new Identity(address, + identityInfo.getIdentityKey(), + computeSafetyNumber(address.toSignalServiceAddress(), identityInfo.getIdentityKey()), + computeSafetyNumberForScanning(address.toSignalServiceAddress(), identityInfo.getIdentityKey()), + identityInfo.getTrustLevel(), + identityInfo.getDateAdded()); } @Override - public List getIdentities(RecipientIdentifier.Single recipient) { + public List getIdentities(RecipientIdentifier.Single recipient) { IdentityInfo identity; try { identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); } catch (UnregisteredUserException e) { identity = null; } - return identity == null ? List.of() : List.of(identity); + return identity == null ? List.of() : List.of(toIdentity(identity)); } /** @@ -1144,8 +1202,7 @@ public class ManagerImpl implements Manager { return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); } - @Override - public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + private byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); } @@ -1165,13 +1222,7 @@ public class ManagerImpl implements Manager { return resolveSignalServiceAddress(resolveRecipient(address)); } - @Override - public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { - return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); - } - - @Override - public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { + private SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); if (address.getUuid().isPresent()) { return address.toSignalServiceAddress(); @@ -1180,13 +1231,15 @@ public class ManagerImpl implements Manager { // Address in recipient store doesn't have a uuid, this shouldn't happen // Try to retrieve the uuid from the server final var number = address.getNumber().get(); + final UUID uuid; try { - return resolveSignalServiceAddress(getRegisteredUser(number)); + uuid = getRegisteredUser(number); } catch (IOException e) { logger.warn("Failed to get uuid for e164 number: {}", number, e); // Return SignalServiceAddress with unknown UUID return address.toSignalServiceAddress(); } + return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); } private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 978f1fd5..ff94c19b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -91,18 +91,18 @@ public class RegistrationManager implements Closeable { } public static RegistrationManager init( - String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent + String number, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent ) throws IOException { var pathConfig = PathConfig.createDefault(settingsPath); final var serviceConfiguration = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent); - if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { + if (!SignalAccount.userExists(pathConfig.getDataPath(), number)) { var identityKey = KeyUtils.generateIdentityKeyPair(); var registrationId = KeyHelper.generateRegistrationId(false); var profileKey = KeyUtils.createProfileKey(); var account = SignalAccount.create(pathConfig.getDataPath(), - username, + number, identityKey, registrationId, profileKey, @@ -111,7 +111,7 @@ public class RegistrationManager implements Closeable { return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); } - var account = SignalAccount.load(pathConfig.getDataPath(), username, true, TrustNewIdentity.ON_FIRST_USE); + var account = SignalAccount.load(pathConfig.getDataPath(), number, true, TrustNewIdentity.ON_FIRST_USE); return new RegistrationManager(account, pathConfig, serviceConfiguration, userAgent); } diff --git a/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java b/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java index d506f0c6..905392c5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java +++ b/lib/src/main/java/org/asamk/signal/manager/UserAlreadyExists.java @@ -4,16 +4,16 @@ import java.io.File; public class UserAlreadyExists extends Exception { - private final String username; + private final String number; private final File fileName; - public UserAlreadyExists(String username, File fileName) { - this.username = username; + public UserAlreadyExists(String number, File fileName) { + this.number = number; this.fileName = fileName; } - public String getUsername() { - return username; + public String getNumber() { + return number; } public File getFileName() { diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Device.java b/lib/src/main/java/org/asamk/signal/manager/api/Device.java index 76074cbf..9ee0d36a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Device.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Device.java @@ -6,12 +6,14 @@ public class Device { private final String name; private final long created; private final long lastSeen; + private final boolean thisDevice; - public Device(long id, String name, long created, long lastSeen) { + public Device(long id, String name, long created, long lastSeen, final boolean thisDevice) { this.id = id; this.name = name; this.created = created; this.lastSeen = lastSeen; + this.thisDevice = thisDevice; } public long getId() { @@ -29,4 +31,8 @@ public class Device { public long getLastSeen() { return lastSeen; } + + public boolean isThisDevice() { + return thisDevice; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Group.java b/lib/src/main/java/org/asamk/signal/manager/api/Group.java new file mode 100644 index 00000000..650e10b6 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/Group.java @@ -0,0 +1,99 @@ +package org.asamk.signal.manager.api; + +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; + +import java.util.Set; + +public class Group { + + private final GroupId groupId; + private final String title; + private final String description; + private final GroupInviteLinkUrl groupInviteLinkUrl; + private final Set members; + private final Set pendingMembers; + private final Set requestingMembers; + private final Set adminMembers; + private final boolean isBlocked; + private final int messageExpirationTime; + private final boolean isAnnouncementGroup; + private final boolean isMember; + + public Group( + final GroupId groupId, + final String title, + final String description, + final GroupInviteLinkUrl groupInviteLinkUrl, + final Set members, + final Set pendingMembers, + final Set requestingMembers, + final Set adminMembers, + final boolean isBlocked, + final int messageExpirationTime, + final boolean isAnnouncementGroup, + final boolean isMember + ) { + this.groupId = groupId; + this.title = title; + this.description = description; + this.groupInviteLinkUrl = groupInviteLinkUrl; + this.members = members; + this.pendingMembers = pendingMembers; + this.requestingMembers = requestingMembers; + this.adminMembers = adminMembers; + this.isBlocked = isBlocked; + this.messageExpirationTime = messageExpirationTime; + this.isAnnouncementGroup = isAnnouncementGroup; + this.isMember = isMember; + } + + public GroupId getGroupId() { + return groupId; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public GroupInviteLinkUrl getGroupInviteLinkUrl() { + return groupInviteLinkUrl; + } + + public Set getMembers() { + return members; + } + + public Set getPendingMembers() { + return pendingMembers; + } + + public Set getRequestingMembers() { + return requestingMembers; + } + + public Set getAdminMembers() { + return adminMembers; + } + + public boolean isBlocked() { + return isBlocked; + } + + public int getMessageExpirationTime() { + return messageExpirationTime; + } + + public boolean isAnnouncementGroup() { + return isAnnouncementGroup; + } + + public boolean isMember() { + return isMember; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Identity.java b/lib/src/main/java/org/asamk/signal/manager/api/Identity.java new file mode 100644 index 00000000..4f6f21f6 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/Identity.java @@ -0,0 +1,65 @@ +package org.asamk.signal.manager.api; + +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.whispersystems.libsignal.IdentityKey; + +import java.util.Date; + +public class Identity { + + private final RecipientAddress recipient; + private final IdentityKey identityKey; + private final String safetyNumber; + private final byte[] scannableSafetyNumber; + private final TrustLevel trustLevel; + private final Date dateAdded; + + public Identity( + final RecipientAddress recipient, + final IdentityKey identityKey, + final String safetyNumber, + final byte[] scannableSafetyNumber, + final TrustLevel trustLevel, + final Date dateAdded + ) { + this.recipient = recipient; + this.identityKey = identityKey; + this.safetyNumber = safetyNumber; + this.scannableSafetyNumber = scannableSafetyNumber; + this.trustLevel = trustLevel; + this.dateAdded = dateAdded; + } + + public RecipientAddress getRecipient() { + return recipient; + } + + public IdentityKey getIdentityKey() { + return this.identityKey; + } + + public TrustLevel getTrustLevel() { + return this.trustLevel; + } + + boolean isTrusted() { + return trustLevel == TrustLevel.TRUSTED_UNVERIFIED || trustLevel == TrustLevel.TRUSTED_VERIFIED; + } + + public Date getDateAdded() { + return this.dateAdded; + } + + public byte[] getFingerprint() { + return identityKey.getPublicKey().serialize(); + } + + public String getSafetyNumber() { + return safetyNumber; + } + + public byte[] getScannableSafetyNumber() { + return scannableSafetyNumber; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java index cb0a08bb..be1029e6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -1,6 +1,7 @@ package org.asamk.signal.manager.api; import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -29,6 +30,17 @@ public abstract class RecipientIdentifier { public static Single fromAddress(SignalServiceAddress address) { return new Uuid(address.getUuid()); } + + public static Single fromAddress(RecipientAddress address) { + if (address.getNumber().isPresent()) { + return new Number(address.getNumber().get()); + } else if (address.getUuid().isPresent()) { + return new Uuid(address.getUuid().get()); + } + throw new AssertionError("RecipientAddress without identifier"); + } + + public abstract String getIdentifier(); } public static class Uuid extends Single { @@ -53,6 +65,11 @@ public abstract class RecipientIdentifier { public int hashCode() { return uuid.hashCode(); } + + @Override + public String getIdentifier() { + return uuid.toString(); + } } public static class Number extends Single { @@ -77,6 +94,11 @@ public abstract class RecipientIdentifier { public int hashCode() { return number.hashCode(); } + + @Override + public String getIdentifier() { + return number; + } } public static class Group extends RecipientIdentifier { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java index 88877d83..c0f5b0b8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java @@ -57,6 +57,16 @@ public class RecipientAddress { } } + public String getLegacyIdentifier() { + if (e164.isPresent()) { + return e164.get(); + } else if (uuid.isPresent()) { + return uuid.get().toString(); + } else { + throw new AssertionError("Given the checks in the constructor, this should not be possible."); + } + } + public boolean matches(RecipientAddress other) { return (uuid.isPresent() && other.uuid.isPresent() && uuid.get().equals(other.uuid.get())) || ( e164.isPresent() && other.e164.isPresent() && e164.get().equals(other.e164.get()) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index b19fba8d..2105ca76 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -13,6 +13,8 @@ import java.util.List; */ public interface Signal extends DBusInterface { + String getNumber(); + long sendMessage( String message, List attachments, String recipient ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.UntrustedIdentity; @@ -26,7 +28,7 @@ public interface Signal extends DBusInterface { ) throws Error.Failure, Error.GroupNotFound, Error.UntrustedIdentity; void sendReadReceipt( - String recipient, List targetSentTimestamp + String recipient, List messageIds ) throws Error.Failure, Error.UntrustedIdentity; long sendRemoteDeleteMessage( diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index 4aa510d6..e81b7018 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -161,7 +161,7 @@ public class App { } if (username == null) { - var usernames = Manager.getAllLocalUsernames(dataPath); + var usernames = Manager.getAllLocalNumbers(dataPath); if (command instanceof MultiLocalCommand) { handleMultiLocalCommand((MultiLocalCommand) command, diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index bc9244f8..35790678 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -61,13 +61,13 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { final var recipientName = getLegacyIdentifier(m.resolveSignalServiceAddress(e.getSender())); writer.println( "Use 'signal-cli -u {} listIdentities -n {}', verify the key and run 'signal-cli -u {} trust -v \"FINGER_PRINT\" {}' to mark it as trusted", - m.getUsername(), + m.getSelfNumber(), recipientName, - m.getUsername(), + m.getSelfNumber(), recipientName); writer.println( "If you don't care about security, use 'signal-cli -u {} trust -a {}' to trust it without verification", - m.getUsername(), + m.getSelfNumber(), recipientName); } else { writer.println("Exception: {} ({})", exception.getMessage(), exception.getClass().getSimpleName()); @@ -657,7 +657,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { private void printMention( PlainTextWriter writer, SignalServiceDataMessage.Mention mention ) { - final var address = m.resolveSignalServiceAddress(mention.getUuid()); + final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid())); writer.println("- {}: {} (length: {})", formatContact(address), mention.getStart(), mention.getLength()); } diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 5394022e..516224f5 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -37,7 +37,7 @@ public class BlockCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { final var contacts = ns.getList("recipient"); - for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getUsername())) { + for (var contact : CommandUtil.getSingleRecipientIdentifiers(contacts, m.getSelfNumber())) { try { m.setContactBlocked(contact, true); } catch (NotMasterDeviceException e) { diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 4a322b99..9878de15 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -95,7 +95,7 @@ public class DaemonCommand implements MultiLocalCommand { try (var conn = DBusConnection.getConnection(busType)) { final var signalControl = new DbusSignalControlImpl(c, m -> { try { - final var objectPath = DbusConfig.getObjectPath(m.getUsername()); + final var objectPath = DbusConfig.getObjectPath(m.getSelfNumber()); return run(conn, objectPath, m, outputWriter, ignoreAttachments); } catch (DBusException e) { logger.error("Failed to export object", e); diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index f5585881..1e06ea9c 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -57,14 +57,14 @@ public class JoinGroupCommand implements JsonRpcLocalCommand { var newGroupId = results.first(); if (outputWriter instanceof JsonWriter) { final var writer = (JsonWriter) outputWriter; - if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) { + if (!m.getGroup(newGroupId).isMember()) { writer.write(Map.of("groupId", newGroupId.toBase64(), "onlyRequested", true)); } else { writer.write(Map.of("groupId", newGroupId.toBase64())); } } else { final var writer = (PlainTextWriter) outputWriter; - if (!m.getGroup(newGroupId).isMember(m.getSelfRecipientId())) { + if (!m.getGroup(newGroupId).isMember()) { writer.println("Requested to join group \"{}\"", newGroupId.toBase64()); } else { writer.println("Joined group \"{}\"", newGroupId.toBase64()); diff --git a/src/main/java/org/asamk/signal/commands/LinkCommand.java b/src/main/java/org/asamk/signal/commands/LinkCommand.java index fbc03300..1d697299 100644 --- a/src/main/java/org/asamk/signal/commands/LinkCommand.java +++ b/src/main/java/org/asamk/signal/commands/LinkCommand.java @@ -44,7 +44,7 @@ public class LinkCommand implements ProvisioningCommand { try { writer.println("{}", m.getDeviceLinkUri()); try (var manager = m.finishDeviceLink(deviceName)) { - writer.println("Associated with: {}", manager.getUsername()); + writer.println("Associated with: {}", manager.getSelfNumber()); } } catch (TimeoutException e) { throw new UserErrorException("Link request timed out, please try again."); @@ -52,7 +52,7 @@ public class LinkCommand implements ProvisioningCommand { throw new IOErrorException("Link request error: " + e.getMessage(), e); } catch (UserAlreadyExists e) { throw new UserErrorException("The user " - + e.getUsername() + + e.getNumber() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again."); diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index 5e609a48..b6dfc3ce 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -8,10 +8,9 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.manager.Manager; +import java.util.UUID; import java.util.stream.Collectors; -import static org.asamk.signal.util.Util.getLegacyIdentifier; - public class ListContactsCommand implements JsonRpcLocalCommand { @Override @@ -33,7 +32,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand { for (var c : contacts) { final var contact = c.second(); writer.println("Number: {} Name: {} Blocked: {} Message expiration: {}", - getLegacyIdentifier(m.resolveSignalServiceAddress(c.first())), + c.first().getLegacyIdentifier(), contact.getName(), contact.isBlocked(), contact.getMessageExpirationTime() == 0 @@ -43,10 +42,10 @@ public class ListContactsCommand implements JsonRpcLocalCommand { } else { final var writer = (JsonWriter) outputWriter; final var jsonContacts = contacts.stream().map(contactPair -> { - final var address = m.resolveSignalServiceAddress(contactPair.first()); + final var address = contactPair.first(); final var contact = contactPair.second(); - return new JsonContact(address.getNumber().orNull(), - address.getUuid().toString(), + return new JsonContact(address.getNumber().orElse(null), + address.getUuid().map(UUID::toString).orElse(null), contact.getName(), contact.isBlocked(), contact.getMessageExpirationTime()); diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java index ad0d3531..1de5b842 100644 --- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java @@ -46,7 +46,7 @@ public class ListDevicesCommand implements JsonRpcLocalCommand { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; for (var d : devices) { - writer.println("- Device {}{}:", d.getId(), (d.getId() == m.getDeviceId() ? " (this device)" : "")); + writer.println("- Device {}{}:", d.getId(), (d.isThisDevice() ? " (this device)" : "")); writer.indent(w -> { w.println("Name: {}", d.getName()); w.println("Created: {}", DateUtils.formatTimestamp(d.getCreated())); diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index b53577be..1eda53ce 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -9,13 +9,13 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.storage.groups.GroupInfo; -import org.asamk.signal.manager.storage.recipients.RecipientId; -import org.asamk.signal.util.Util; +import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; public class ListGroupsCommand implements JsonRpcLocalCommand { @@ -35,44 +35,41 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { .help("List the members and group invite links of each group. If output=json, then this is always set"); } - private static Set resolveMembers(Manager m, Set addresses) { - return addresses.stream() - .map(m::resolveSignalServiceAddress) - .map(Util::getLegacyIdentifier) - .collect(Collectors.toSet()); + private static Set resolveMembers(Set addresses) { + return addresses.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toSet()); } - private static Set resolveJsonMembers(Manager m, Set addresses) { + private static Set resolveJsonMembers(Set addresses) { return addresses.stream() - .map(m::resolveSignalServiceAddress) - .map(address -> new JsonGroupMember(address.getNumber().orNull(), address.getUuid().toString())) + .map(address -> new JsonGroupMember(address.getNumber().orElse(null), + address.getUuid().map(UUID::toString).orElse(null))) .collect(Collectors.toSet()); } private static void printGroupPlainText( - PlainTextWriter writer, Manager m, GroupInfo group, boolean detailed + PlainTextWriter writer, Group group, boolean detailed ) { if (detailed) { - final var groupInviteLink = group.getGroupInviteLink(); + final var groupInviteLink = group.getGroupInviteLinkUrl(); writer.println( "Id: {} Name: {} Description: {} Active: {} Blocked: {} Members: {} Pending members: {} Requesting members: {} Admins: {} Message expiration: {} Link: {}", group.getGroupId().toBase64(), group.getTitle(), group.getDescription(), - group.isMember(m.getSelfRecipientId()), + group.isMember(), group.isBlocked(), - resolveMembers(m, group.getMembers()), - resolveMembers(m, group.getPendingMembers()), - resolveMembers(m, group.getRequestingMembers()), - resolveMembers(m, group.getAdminMembers()), + resolveMembers(group.getMembers()), + resolveMembers(group.getPendingMembers()), + resolveMembers(group.getRequestingMembers()), + resolveMembers(group.getAdminMembers()), group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s", groupInviteLink == null ? '-' : groupInviteLink.getUrl()); } else { writer.println("Id: {} Name: {} Active: {} Blocked: {}", group.getGroupId().toBase64(), group.getTitle(), - group.isMember(m.getSelfRecipientId()), + group.isMember(), group.isBlocked()); } } @@ -87,18 +84,18 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { final var jsonWriter = (JsonWriter) outputWriter; var jsonGroups = groups.stream().map(group -> { - final var groupInviteLink = group.getGroupInviteLink(); + final var groupInviteLink = group.getGroupInviteLinkUrl(); return new JsonGroup(group.getGroupId().toBase64(), group.getTitle(), group.getDescription(), - group.isMember(m.getSelfRecipientId()), + group.isMember(), group.isBlocked(), group.getMessageExpirationTime(), - resolveJsonMembers(m, group.getMembers()), - resolveJsonMembers(m, group.getPendingMembers()), - resolveJsonMembers(m, group.getRequestingMembers()), - resolveJsonMembers(m, group.getAdminMembers()), + resolveJsonMembers(group.getMembers()), + resolveJsonMembers(group.getPendingMembers()), + resolveJsonMembers(group.getRequestingMembers()), + resolveJsonMembers(group.getAdminMembers()), groupInviteLink == null ? null : groupInviteLink.getUrl()); }).collect(Collectors.toList()); @@ -107,7 +104,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { final var writer = (PlainTextWriter) outputWriter; boolean detailed = ns.getBoolean("detailed"); for (var group : groups) { - printGroupPlainText(writer, m, group, detailed); + printGroupPlainText(writer, group, detailed); } } } diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index 02cd1d9f..ed2942a5 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -8,7 +8,7 @@ import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.manager.Manager; -import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.manager.api.Identity; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.Hex; import org.asamk.signal.util.Util; @@ -29,9 +29,9 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { return "listIdentities"; } - private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, IdentityInfo theirId) { - final SignalServiceAddress address = m.resolveSignalServiceAddress(theirId.getRecipientId()); - var digits = Util.formatSafetyNumber(m.computeSafetyNumber(address, theirId.getIdentityKey())); + private static void printIdentityFingerprint(PlainTextWriter writer, Manager m, Identity theirId) { + final SignalServiceAddress address = theirId.getRecipient().toSignalServiceAddress(); + var digits = Util.formatSafetyNumber(theirId.getSafetyNumber()); writer.println("{}: {} Added: {} Fingerprint: {} Safety Number: {}", address.getNumber().orNull(), theirId.getTrustLevel(), @@ -52,11 +52,11 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { ) throws CommandException { var number = ns.getString("number"); - List identities; + List identities; if (number == null) { identities = m.getIdentities(); } else { - identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getUsername())); + identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getSelfNumber())); } if (outputWriter instanceof PlainTextWriter) { @@ -67,9 +67,9 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { } else { final var writer = (JsonWriter) outputWriter; final var jsonIdentities = identities.stream().map(id -> { - final var address = m.resolveSignalServiceAddress(id.getRecipientId()); - var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(address, id.getIdentityKey())); - var scannableSafetyNumber = m.computeSafetyNumberForScanning(address, id.getIdentityKey()); + final var address = id.getRecipient().toSignalServiceAddress(); + var safetyNumber = Util.formatSafetyNumber(id.getSafetyNumber()); + var scannableSafetyNumber = id.getScannableSafetyNumber(); return new JsonIdentity(address.getNumber().orNull(), address.getUuid().toString(), Hex.toString(id.getFingerprint()), diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 67a6596b..7635f8ae 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -50,7 +50,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { ) throws CommandException { final var groupId = CommandUtil.getGroupId(ns.getString("group-id")); - var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), m.getUsername()); + var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), m.getSelfNumber()); try { try { diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 338e70ac..f8c3c358 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -72,7 +72,7 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { try { final var results = m.sendMessageReaction(emoji, isRemove, - CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + CommandUtil.getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()), targetTimestamp, recipientIdentifiers); outputResult(outputWriter, results.getTimestamp()); diff --git a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java index 0d5772ec..5dd29682 100644 --- a/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReceiptCommand.java @@ -37,7 +37,7 @@ public class SendReceiptCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { final var recipientString = ns.getString("recipient"); - final var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername()); + final var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getSelfNumber()); final var targetTimestamps = ns.getList("target-timestamp"); final var type = ns.getString("type"); diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index 3a965e47..cfe66770 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -45,7 +45,7 @@ public class SendTypingCommand implements JsonRpcLocalCommand { final var recipientIdentifiers = new HashSet(); if (recipientStrings != null) { - final var localNumber = m.getUsername(); + final var localNumber = m.getSelfNumber(); recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber)); } if (groupIdStrings != null) { diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index aedc2c3e..9e59ad86 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -38,7 +38,7 @@ public class TrustCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { var recipentString = ns.getString("recipient"); - var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getUsername()); + var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getSelfNumber()); if (ns.getBoolean("trust-all-known-keys")) { boolean res = m.trustIdentityAllKeys(recipient); if (!res) { diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 812065bc..7cf209fa 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -36,7 +36,8 @@ public class UnblockCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("recipient"), m.getUsername())) { + for (var contactNumber : CommandUtil.getSingleRecipientIdentifiers(ns.getList("recipient"), + m.getSelfNumber())) { try { m.setContactBlocked(contactNumber, false); } catch (NotMasterDeviceException e) { diff --git a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java index 6c2916eb..46641668 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateContactCommand.java @@ -33,7 +33,7 @@ public class UpdateContactCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { var recipientString = ns.getString("recipient"); - var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getUsername()); + var recipient = CommandUtil.getSingleRecipientIdentifier(recipientString, m.getSelfNumber()); try { var expiration = ns.getInt("expiration"); diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index b0269894..49cd4719 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -116,7 +116,7 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { final var groupIdString = ns.getString("group-id"); var groupId = CommandUtil.getGroupId(groupIdString); - final var localNumber = m.getUsername(); + final var localNumber = m.getSelfNumber(); var groupName = ns.getString("name"); var groupDescription = ns.getString("description"); diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java index 6ec8d964..be628bde 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalControlImpl.java @@ -160,7 +160,7 @@ public class DbusSignalControlImpl implements org.asamk.SignalControl { synchronized (receiveThreads) { return receiveThreads.stream() .map(Pair::first) - .map(Manager::getUsername) + .map(Manager::getSelfNumber) .map(u -> new DBusPath(DbusConfig.getObjectPath(u))) .collect(Collectors.toList()); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index e975a671..c8208774 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -8,6 +8,7 @@ import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.UntrustedIdentityException; import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; @@ -17,9 +18,9 @@ import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.manager.storage.identities.IdentityInfo; +import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.util.ErrorUtils; -import org.asamk.signal.util.Util; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; @@ -45,8 +46,6 @@ import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.asamk.signal.util.Util.getLegacyIdentifier; - public class DbusSignalImpl implements Signal { private final Manager m; @@ -67,6 +66,11 @@ public class DbusSignalImpl implements Signal { return objectPath; } + @Override + public String getNumber() { + return m.getSelfNumber(); + } + @Override public void addDevice(String uri) { try { @@ -123,7 +127,7 @@ public class DbusSignalImpl implements Signal { public long sendMessage(final String message, final List attachments, final List recipients) { try { final var results = m.sendMessage(new Message(message, attachments), - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); @@ -153,7 +157,7 @@ public class DbusSignalImpl implements Signal { ) { try { final var results = m.sendRemoteDeleteMessage(targetSentTimestamp, - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); checkSendMessageResults(results.getTimestamp(), results.getResults()); @@ -205,9 +209,9 @@ public class DbusSignalImpl implements Signal { try { final var results = m.sendMessageReaction(emoji, remove, - getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()), targetSentTimestamp, - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); checkSendMessageResults(results.getTimestamp(), results.getResults()); @@ -227,7 +231,7 @@ public class DbusSignalImpl implements Signal { var recipients = new ArrayList(1); recipients.add(recipient); m.sendTypingMessage(stop ? TypingAction.STOP : TypingAction.START, - getSingleRecipientIdentifiers(recipients, m.getUsername()).stream() + getSingleRecipientIdentifiers(recipients, m.getSelfNumber()).stream() .map(RecipientIdentifier.class::cast) .collect(Collectors.toSet())); } catch (IOException e) { @@ -241,10 +245,10 @@ public class DbusSignalImpl implements Signal { @Override public void sendReadReceipt( - final String recipient, final List timestamps + final String recipient, final List messageIds ) throws Error.Failure, Error.UntrustedIdentity { try { - m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getUsername()), timestamps); + m.sendReadReceipt(getSingleRecipientIdentifier(recipient, m.getSelfNumber()), messageIds); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } catch (UntrustedIdentityException e) { @@ -291,7 +295,7 @@ public class DbusSignalImpl implements Signal { @Override public void sendEndSessionMessage(final List recipients) { try { - final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getUsername())); + final var results = m.sendEndSessionMessage(getSingleRecipientIdentifiers(recipients, m.getSelfNumber())); checkSendMessageResults(results.getTimestamp(), results.getResults()); } catch (IOException e) { throw new Error.Failure(e.getMessage()); @@ -325,7 +329,7 @@ public class DbusSignalImpl implements Signal { try { final var results = m.sendMessageReaction(emoji, remove, - getSingleRecipientIdentifier(targetAuthor, m.getUsername()), + getSingleRecipientIdentifier(targetAuthor, m.getSelfNumber()), targetSentTimestamp, Set.of(new RecipientIdentifier.Group(getGroupId(groupId)))); checkSendMessageResults(results.getTimestamp(), results.getResults()); @@ -341,13 +345,13 @@ public class DbusSignalImpl implements Signal { // the profile name @Override public String getContactName(final String number) { - return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getUsername())); + return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber())); } @Override public void setContactName(final String number, final String name) { try { - m.setContactName(getSingleRecipientIdentifier(number, m.getUsername()), name); + m.setContactName(getSingleRecipientIdentifier(number, m.getSelfNumber()), name); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } catch (UnregisteredUserException e) { @@ -358,7 +362,7 @@ public class DbusSignalImpl implements Signal { @Override public void setExpirationTimer(final String number, final int expiration) { try { - m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getUsername()), expiration); + m.setExpirationTimer(getSingleRecipientIdentifier(number, m.getSelfNumber()), expiration); } catch (IOException e) { throw new Error.Failure(e.getMessage()); } @@ -367,7 +371,7 @@ public class DbusSignalImpl implements Signal { @Override public void setContactBlocked(final String number, final boolean blocked) { try { - m.setContactBlocked(getSingleRecipientIdentifier(number, m.getUsername()), blocked); + m.setContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber()), blocked); } catch (NotMasterDeviceException e) { throw new Error.Failure("This command doesn't work on linked devices."); } catch (IOException e) { @@ -412,11 +416,7 @@ public class DbusSignalImpl implements Signal { if (group == null) { return List.of(); } else { - return group.getMembers() - .stream() - .map(m::resolveSignalServiceAddress) - .map(Util::getLegacyIdentifier) - .collect(Collectors.toList()); + return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList()); } } @@ -432,7 +432,7 @@ public class DbusSignalImpl implements Signal { if (avatar.isEmpty()) { avatar = null; } - final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getUsername()); + final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getSelfNumber()); if (groupId == null) { final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar)); checkSendMessageResults(results.second().getTimestamp(), results.second().getResults()); @@ -573,10 +573,9 @@ public class DbusSignalImpl implements Signal { // all numbers the system knows @Override public List listNumbers() { - return Stream.concat(m.getIdentities().stream().map(IdentityInfo::getRecipientId), + return Stream.concat(m.getIdentities().stream().map(Identity::getRecipient), m.getContacts().stream().map(Pair::first)) - .map(m::resolveSignalServiceAddress) - .map(a -> a.getNumber().orNull()) + .map(a -> a.getNumber().orElse(null)) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); @@ -589,16 +588,19 @@ public class DbusSignalImpl implements Signal { var contacts = m.getContacts(); for (var c : contacts) { if (name.equals(c.second().getName())) { - numbers.add(getLegacyIdentifier(m.resolveSignalServiceAddress(c.first()))); + numbers.add(c.first().getLegacyIdentifier()); } } // Try profiles if no contact name was found for (var identity : m.getIdentities()) { - final var recipientId = identity.getRecipientId(); - final var address = m.resolveSignalServiceAddress(recipientId); - var number = address.getNumber().orNull(); + final var address = identity.getRecipient(); + var number = address.getNumber().orElse(null); if (number != null) { - var profile = m.getRecipientProfile(recipientId); + Profile profile = null; + try { + profile = m.getRecipientProfile(RecipientIdentifier.Single.fromAddress(address)); + } catch (UnregisteredUserException ignored) { + } if (profile != null && profile.getDisplayName().equals(name)) { numbers.add(number); } @@ -639,7 +641,7 @@ public class DbusSignalImpl implements Signal { @Override public boolean isContactBlocked(final String number) { - return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getUsername())); + return m.isContactBlocked(getSingleRecipientIdentifier(number, m.getSelfNumber())); } @Override @@ -658,7 +660,7 @@ public class DbusSignalImpl implements Signal { if (group == null) { return false; } else { - return group.isMember(m.getSelfRecipientId()); + return group.isMember(); } } diff --git a/src/main/java/org/asamk/signal/json/JsonMention.java b/src/main/java/org/asamk/signal/json/JsonMention.java index b24768b7..3c6f2eec 100644 --- a/src/main/java/org/asamk/signal/json/JsonMention.java +++ b/src/main/java/org/asamk/signal/json/JsonMention.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.asamk.signal.manager.Manager; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import static org.asamk.signal.util.Util.getLegacyIdentifier; @@ -26,7 +27,7 @@ public class JsonMention { final int length; JsonMention(SignalServiceDataMessage.Mention mention, Manager m) { - final var address = m.resolveSignalServiceAddress(mention.getUuid()); + final var address = m.resolveSignalServiceAddress(new SignalServiceAddress(mention.getUuid())); this.name = getLegacyIdentifier(address); this.number = address.getNumber().orNull(); this.uuid = address.getUuid().toString(); diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index 7b884b0e..e49e6125 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -86,7 +86,7 @@ public class JsonMessageEnvelope { } String name; try { - name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getUsername())); + name = m.getContactOrProfileName(RecipientIdentifier.Single.fromString(this.source, m.getSelfNumber())); } catch (InvalidNumberException | NullPointerException e) { name = null; } diff --git a/src/main/java/org/asamk/signal/util/CommandUtil.java b/src/main/java/org/asamk/signal/util/CommandUtil.java index 18b38a2a..0a624e6b 100644 --- a/src/main/java/org/asamk/signal/util/CommandUtil.java +++ b/src/main/java/org/asamk/signal/util/CommandUtil.java @@ -28,7 +28,7 @@ public class CommandUtil { recipientIdentifiers.add(RecipientIdentifier.NoteToSelf.INSTANCE); } if (recipientStrings != null) { - final var localNumber = m.getUsername(); + final var localNumber = m.getSelfNumber(); recipientIdentifiers.addAll(CommandUtil.getSingleRecipientIdentifiers(recipientStrings, localNumber)); } if (groupIdStrings != null) { From 593cd7d8ca6e8e0ab654accfd7e3c9d2ee01b001 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 18:51:44 +0200 Subject: [PATCH 0833/2005] Refactor dbus client mode to improve maintainability --- src/main/java/org/asamk/Signal.java | 2 +- src/main/java/org/asamk/signal/App.java | 13 +- .../asamk/signal/commands/DbusCommand.java | 20 - .../signal/commands/RemoteDeleteCommand.java | 46 +- .../asamk/signal/commands/SendCommand.java | 97 +--- .../signal/commands/SendReactionCommand.java | 53 +- .../signal/commands/UpdateGroupCommand.java | 43 +- .../exceptions/UserErrorException.java | 4 + .../asamk/signal/dbus/DbusManagerImpl.java | 487 ++++++++++++++++++ .../org/asamk/signal/dbus/DbusSignalImpl.java | 57 +- 10 files changed, 531 insertions(+), 291 deletions(-) delete mode 100644 src/main/java/org/asamk/signal/commands/DbusCommand.java create mode 100644 src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 2105ca76..cc521f6d 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -13,7 +13,7 @@ import java.util.List; */ public interface Signal extends DBusInterface { - String getNumber(); + String getSelfNumber(); long sendMessage( String message, List attachments, String recipient diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index e81b7018..bffbded5 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -8,7 +8,6 @@ import net.sourceforge.argparse4j.inf.Namespace; import org.asamk.Signal; import org.asamk.signal.commands.Command; import org.asamk.signal.commands.Commands; -import org.asamk.signal.commands.DbusCommand; import org.asamk.signal.commands.ExtendedDbusCommand; import org.asamk.signal.commands.LocalCommand; import org.asamk.signal.commands.MultiLocalCommand; @@ -19,6 +18,7 @@ import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; +import org.asamk.signal.dbus.DbusManagerImpl; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotRegisteredException; import org.asamk.signal.manager.ProvisioningManager; @@ -29,6 +29,7 @@ import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.util.IOUtils; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; @@ -346,8 +347,14 @@ public class App { ) throws CommandException { if (command instanceof ExtendedDbusCommand) { ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn, outputWriter); - } else if (command instanceof DbusCommand) { - ((DbusCommand) command).handleCommand(ns, ts, outputWriter); + } else if (command instanceof LocalCommand) { + try { + ((LocalCommand) command).handleCommand(ns, new DbusManagerImpl(ts), outputWriter); + } catch (UnsupportedOperationException e) { + throw new UserErrorException("Command is not yet implemented via dbus", e); + } catch (DBusExecutionException e) { + throw new UnexpectedErrorException(e.getMessage(), e); + } } else { throw new UserErrorException("Command is not yet implemented via dbus"); } diff --git a/src/main/java/org/asamk/signal/commands/DbusCommand.java b/src/main/java/org/asamk/signal/commands/DbusCommand.java deleted file mode 100644 index 9f676a39..00000000 --- a/src/main/java/org/asamk/signal/commands/DbusCommand.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.asamk.signal.commands; - -import net.sourceforge.argparse4j.inf.Namespace; - -import org.asamk.Signal; -import org.asamk.signal.OutputWriter; -import org.asamk.signal.commands.exceptions.CommandException; -import org.asamk.signal.dbus.DbusSignalImpl; -import org.asamk.signal.manager.Manager; - -public interface DbusCommand extends LocalCommand { - - void handleCommand(Namespace ns, Signal signal, OutputWriter outputWriter) throws CommandException; - - default void handleCommand( - final Namespace ns, final Manager m, final OutputWriter outputWriter - ) throws CommandException { - handleCommand(ns, new DbusSignalImpl(m, null), outputWriter); - } -} diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index 7d7067c4..e515defe 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.Signal; import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; @@ -17,13 +16,11 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; -import org.freedesktop.dbus.errors.UnknownObject; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import java.io.IOException; import java.util.Map; -public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { +public class RemoteDeleteCommand implements JsonRpcLocalCommand { @Override public String getName() { @@ -69,47 +66,6 @@ public class RemoteDeleteCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Signal signal, final OutputWriter outputWriter - ) throws CommandException { - final var recipients = ns.getList("recipient"); - final var groupIdStrings = ns.getList("group-id"); - - final var noRecipients = recipients == null || recipients.isEmpty(); - final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); - if (noRecipients && noGroups) { - throw new UserErrorException("No recipients given"); - } - if (!noRecipients && !noGroups) { - throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); - } - - final long targetTimestamp = ns.getLong("target-timestamp"); - - try { - long timestamp = 0; - if (!noGroups) { - final var groupIds = CommandUtil.getGroupIds(groupIdStrings); - for (final var groupId : groupIds) { - timestamp = signal.sendGroupRemoteDeleteMessage(targetTimestamp, groupId.serialize()); - } - } else { - timestamp = signal.sendRemoteDeleteMessage(targetTimestamp, recipients); - } - outputResult(outputWriter, timestamp); - } catch (UnknownObject e) { - throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); - } catch (Signal.Error.InvalidNumber e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); - } catch (Signal.Error.GroupNotFound e) { - throw new UserErrorException("Failed to send to group: " + e.getMessage()); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")", e); - } - } - private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 1973b1a1..1cd2e674 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -4,13 +4,11 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.Signal; import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; import org.asamk.signal.commands.exceptions.CommandException; import org.asamk.signal.commands.exceptions.UnexpectedErrorException; -import org.asamk.signal.commands.exceptions.UntrustedKeyErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; @@ -22,8 +20,6 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; import org.asamk.signal.util.IOUtils; -import org.freedesktop.dbus.errors.UnknownObject; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +29,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; -public class SendCommand implements DbusCommand, JsonRpcLocalCommand { +public class SendCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(SendCommand.class); @@ -116,97 +112,6 @@ public class SendCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Signal signal, final OutputWriter outputWriter - ) throws CommandException { - final var recipients = ns.getList("recipient"); - final var isEndSession = ns.getBoolean("end-session"); - final var groupIdStrings = ns.getList("group-id"); - final var isNoteToSelf = ns.getBoolean("note-to-self"); - - final var noRecipients = recipients == null || recipients.isEmpty(); - final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); - if ((noRecipients && isEndSession) || (noRecipients && noGroups && !isNoteToSelf)) { - throw new UserErrorException("No recipients given"); - } - if (!noRecipients && !noGroups) { - throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); - } - if (!noRecipients && isNoteToSelf) { - throw new UserErrorException( - "You cannot specify recipients by phone number and note to self at the same time"); - } - - if (isEndSession) { - try { - signal.sendEndSessionMessage(recipients); - return; - } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")", e); - } - } - - var messageText = ns.getString("message"); - if (messageText == null) { - try { - messageText = IOUtils.readAll(System.in, Charset.defaultCharset()); - } catch (IOException e) { - throw new UserErrorException("Failed to read message from stdin: " + e.getMessage()); - } - } - - List attachments = ns.getList("attachment"); - if (attachments == null) { - attachments = List.of(); - } - - if (!noGroups) { - final var groupIds = CommandUtil.getGroupIds(groupIdStrings); - - try { - long timestamp = 0; - for (final var groupId : groupIds) { - timestamp = signal.sendGroupMessage(messageText, attachments, groupId.serialize()); - } - outputResult(outputWriter, timestamp); - return; - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send group message: " + e.getMessage(), e); - } - } - - if (isNoteToSelf) { - try { - var timestamp = signal.sendNoteToSelfMessage(messageText, attachments); - outputResult(outputWriter, timestamp); - return; - } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send note to self message: " + e.getMessage(), e); - } - } - - try { - var timestamp = signal.sendMessage(messageText, attachments, recipients); - outputResult(outputWriter, timestamp); - } catch (UnknownObject e) { - throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); - } catch (Signal.Error.UntrustedIdentity e) { - throw new UntrustedKeyErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")"); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")", e); - } - } - private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index f8c3c358..a1c6c319 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.Signal; import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; @@ -17,13 +16,11 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; -import org.freedesktop.dbus.errors.UnknownObject; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import java.io.IOException; import java.util.Map; -public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { +public class SendReactionCommand implements JsonRpcLocalCommand { @Override public String getName() { @@ -85,54 +82,6 @@ public class SendReactionCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Signal signal, final OutputWriter outputWriter - ) throws CommandException { - final var recipients = ns.getList("recipient"); - final var groupIdStrings = ns.getList("group-id"); - - final var noRecipients = recipients == null || recipients.isEmpty(); - final var noGroups = groupIdStrings == null || groupIdStrings.isEmpty(); - if (noRecipients && noGroups) { - throw new UserErrorException("No recipients given"); - } - if (!noRecipients && !noGroups) { - throw new UserErrorException("You cannot specify recipients by phone number and groups at the same time"); - } - - final var emoji = ns.getString("emoji"); - final var isRemove = ns.getBoolean("remove"); - final var targetAuthor = ns.getString("target-author"); - final var targetTimestamp = ns.getLong("target-timestamp"); - - try { - long timestamp = 0; - if (!noGroups) { - final var groupIds = CommandUtil.getGroupIds(groupIdStrings); - for (final var groupId : groupIds) { - timestamp = signal.sendGroupMessageReaction(emoji, - isRemove, - targetAuthor, - targetTimestamp, - groupId.serialize()); - } - } else { - timestamp = signal.sendMessageReaction(emoji, isRemove, targetAuthor, targetTimestamp, recipients); - } - outputResult(outputWriter, timestamp); - } catch (UnknownObject e) { - throw new UserErrorException("Failed to find dbus object, maybe missing the -u flag: " + e.getMessage()); - } catch (Signal.Error.InvalidNumber e) { - throw new UserErrorException("Invalid number: " + e.getMessage()); - } catch (Signal.Error.GroupNotFound e) { - throw new UserErrorException("Failed to send to group: " + e.getMessage()); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")", e); - } - } - private void outputResult(final OutputWriter outputWriter, final long timestamp) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 49cd4719..4bbaa992 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -4,7 +4,6 @@ import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; -import org.asamk.Signal; import org.asamk.signal.JsonWriter; import org.asamk.signal.OutputWriter; import org.asamk.signal.PlainTextWriter; @@ -21,17 +20,14 @@ import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.util.CommandUtil; import org.asamk.signal.util.ErrorUtils; -import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; -public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { +public class UpdateGroupCommand implements JsonRpcLocalCommand { private final static Logger logger = LoggerFactory.getLogger(UpdateGroupCommand.class); @@ -179,43 +175,6 @@ public class UpdateGroupCommand implements DbusCommand, JsonRpcLocalCommand { } } - @Override - public void handleCommand( - final Namespace ns, final Signal signal, final OutputWriter outputWriter - ) throws CommandException { - var groupId = CommandUtil.getGroupId(ns.getString("group-id")); - - var groupName = ns.getString("name"); - if (groupName == null) { - groupName = ""; - } - - List groupMembers = ns.getList("member"); - if (groupMembers == null) { - groupMembers = new ArrayList<>(); - } - - var groupAvatar = ns.getString("avatar"); - if (groupAvatar == null) { - groupAvatar = ""; - } - - try { - var newGroupId = signal.updateGroup(groupId == null ? new byte[0] : groupId.serialize(), - groupName, - groupMembers, - groupAvatar); - if (groupId == null) { - outputResult(outputWriter, null, GroupId.unknownVersion(newGroupId)); - } - } catch (Signal.Error.AttachmentInvalid e) { - throw new UserErrorException("Failed to add avatar attachment for group\": " + e.getMessage()); - } catch (DBusExecutionException e) { - throw new UnexpectedErrorException("Failed to send message: " + e.getMessage() + " (" + e.getClass() - .getSimpleName() + ")", e); - } - } - private void outputResult(final OutputWriter outputWriter, final Long timestamp, final GroupId groupId) { if (outputWriter instanceof PlainTextWriter) { final var writer = (PlainTextWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java b/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java index 84e957cc..819ce495 100644 --- a/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java +++ b/src/main/java/org/asamk/signal/commands/exceptions/UserErrorException.java @@ -5,4 +5,8 @@ public final class UserErrorException extends CommandException { public UserErrorException(final String message) { super(message); } + + public UserErrorException(final String message, final Throwable cause) { + super(message, cause); + } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java new file mode 100644 index 00000000..b9f5ae11 --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -0,0 +1,487 @@ +package org.asamk.signal.dbus; + +import org.asamk.Signal; +import org.asamk.signal.manager.AttachmentInvalidException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotMasterDeviceException; +import org.asamk.signal.manager.StickerPackInvalidException; +import org.asamk.signal.manager.UntrustedIdentityException; +import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.Group; +import org.asamk.signal.manager.api.Identity; +import org.asamk.signal.manager.api.Message; +import org.asamk.signal.manager.api.RecipientIdentifier; +import org.asamk.signal.manager.api.SendGroupMessageResults; +import org.asamk.signal.manager.api.SendMessageResults; +import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.groups.GroupId; +import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupLinkState; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupPermission; +import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; +import org.asamk.signal.manager.groups.LastGroupAdminException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.Profile; +import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.util.Pair; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * This class implements the Manager interface using the DBus Signal interface, where possible. + * It's used for the signal-cli dbus client mode (--dbus, --dbus-system) + */ +public class DbusManagerImpl implements Manager { + + private final Signal signal; + + public DbusManagerImpl(final Signal signal) { + this.signal = signal; + } + + @Override + public String getSelfNumber() { + return signal.getSelfNumber(); + } + + @Override + public void checkAccountState() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public Map> areUsersRegistered(final Set numbers) throws IOException { + final var numbersList = new ArrayList<>(numbers); + final var registered = signal.isRegistered(numbersList); + + final var result = new HashMap>(); + for (var i = 0; i < numbersList.size(); i++) { + result.put(numbersList.get(i), + new Pair<>(numbersList.get(i), registered.get(i) ? UuidUtil.UNKNOWN_UUID : null)); + } + return result; + } + + @Override + public void updateAccountAttributes(final String deviceName) throws IOException { + if (deviceName != null) { + signal.updateDeviceName(deviceName); + } + } + + @Override + public void setProfile( + final String givenName, + final String familyName, + final String about, + final String aboutEmoji, + final Optional avatar + ) throws IOException { + signal.updateProfile(emptyIfNull(givenName), + emptyIfNull(familyName), + emptyIfNull(about), + emptyIfNull(aboutEmoji), + avatar == null ? "" : avatar.transform(File::getPath).or(""), + avatar != null && !avatar.isPresent()); + } + + @Override + public void unregister() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void deleteAccount() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void submitRateLimitRecaptchaChallenge(final String challenge, final String captcha) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public List getLinkedDevices() throws IOException { + return signal.listDevices() + .stream() + .map(name -> new Device(-1, name, 0, 0, false)) + .collect(Collectors.toList()); + } + + @Override + public void removeLinkedDevices(final int deviceId) throws IOException { + signal.removeDevice(deviceId); + } + + @Override + public void addDeviceLink(final URI linkUri) throws IOException, InvalidKeyException { + signal.addDevice(linkUri.toString()); + } + + @Override + public void setRegistrationLockPin(final Optional pin) throws IOException, UnauthenticatedResponseException { + if (pin.isPresent()) { + signal.setPin(pin.get()); + } else { + signal.removePin(); + } + } + + @Override + public Profile getRecipientProfile(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { + throw new UnsupportedOperationException(); + } + + @Override + public List getGroups() { + final var groupIds = signal.getGroupIds(); + return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList()); + } + + @Override + public SendGroupMessageResults quitGroup( + final GroupId groupId, final Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { + if (groupAdmins.size() > 0) { + throw new UnsupportedOperationException(); + } + signal.quitGroup(groupId.serialize()); + return new SendGroupMessageResults(0, List.of()); + } + + @Override + public void deleteGroup(final GroupId groupId) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public Pair createGroup( + final String name, final Set members, final File avatarFile + ) throws IOException, AttachmentInvalidException { + final var newGroupId = signal.updateGroup(new byte[0], + emptyIfNull(name), + members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), + avatarFile == null ? "" : avatarFile.getPath()); + return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); + } + + @Override + public SendGroupMessageResults updateGroup( + final GroupId groupId, + final String name, + final String description, + final Set members, + final Set removeMembers, + final Set admins, + final Set removeAdmins, + final boolean resetGroupLink, + final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, + final File avatarFile, + final Integer expirationTimer, + final Boolean isAnnouncementGroup + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { + signal.updateGroup(groupId.serialize(), + emptyIfNull(name), + members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), + avatarFile == null ? "" : avatarFile.getPath()); + return new SendGroupMessageResults(0, List.of()); + } + + @Override + public Pair joinGroup(final GroupInviteLinkUrl inviteLinkUrl) throws IOException, GroupLinkNotActiveException { + final var newGroupId = signal.joinGroup(inviteLinkUrl.getUrl()); + return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); + } + + @Override + public void sendTypingMessage( + final TypingAction action, final Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + for (final var recipient : recipients) { + if (recipient instanceof RecipientIdentifier.Single) { + signal.sendTyping(((RecipientIdentifier.Single) recipient).getIdentifier(), + action == TypingAction.STOP); + } else if (recipient instanceof RecipientIdentifier.Group) { + throw new UnsupportedOperationException(); + } + } + } + + @Override + public void sendReadReceipt( + final RecipientIdentifier.Single sender, final List messageIds + ) throws IOException, UntrustedIdentityException { + signal.sendReadReceipt(sender.getIdentifier(), messageIds); + } + + @Override + public void sendViewedReceipt( + final RecipientIdentifier.Single sender, final List messageIds + ) throws IOException, UntrustedIdentityException { + throw new UnsupportedOperationException(); + } + + @Override + public SendMessageResults sendMessage( + final Message message, final Set recipients + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + return handleMessage(recipients, + numbers -> signal.sendMessage(message.getMessageText(), message.getAttachments(), numbers), + () -> signal.sendNoteToSelfMessage(message.getMessageText(), message.getAttachments()), + groupId -> signal.sendGroupMessage(message.getMessageText(), message.getAttachments(), groupId)); + } + + @Override + public SendMessageResults sendRemoteDeleteMessage( + final long targetSentTimestamp, final Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + return handleMessage(recipients, + numbers -> signal.sendRemoteDeleteMessage(targetSentTimestamp, numbers), + () -> signal.sendRemoteDeleteMessage(targetSentTimestamp, signal.getSelfNumber()), + groupId -> signal.sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId)); + } + + @Override + public SendMessageResults sendMessageReaction( + final String emoji, + final boolean remove, + final RecipientIdentifier.Single targetAuthor, + final long targetSentTimestamp, + final Set recipients + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + return handleMessage(recipients, + numbers -> signal.sendMessageReaction(emoji, + remove, + targetAuthor.getIdentifier(), + targetSentTimestamp, + numbers), + () -> signal.sendMessageReaction(emoji, + remove, + targetAuthor.getIdentifier(), + targetSentTimestamp, + signal.getSelfNumber()), + groupId -> signal.sendGroupMessageReaction(emoji, + remove, + targetAuthor.getIdentifier(), + targetSentTimestamp, + groupId)); + } + + @Override + public SendMessageResults sendEndSessionMessage(final Set recipients) throws IOException { + signal.sendEndSessionMessage(recipients.stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + return new SendMessageResults(0, Map.of()); + } + + @Override + public void setContactName( + final RecipientIdentifier.Single recipient, final String name + ) throws NotMasterDeviceException, UnregisteredUserException { + signal.setContactName(recipient.getIdentifier(), name); + } + + @Override + public void setContactBlocked( + final RecipientIdentifier.Single recipient, final boolean blocked + ) throws NotMasterDeviceException, IOException { + signal.setContactBlocked(recipient.getIdentifier(), blocked); + } + + @Override + public void setGroupBlocked( + final GroupId groupId, final boolean blocked + ) throws GroupNotFoundException, IOException { + signal.setGroupBlocked(groupId.serialize(), blocked); + } + + @Override + public void setExpirationTimer( + final RecipientIdentifier.Single recipient, final int messageExpirationTimer + ) throws IOException { + signal.setExpirationTimer(recipient.getIdentifier(), messageExpirationTimer); + } + + @Override + public URI uploadStickerPack(final File path) throws IOException, StickerPackInvalidException { + try { + return new URI(signal.uploadStickerPack(path.getPath())); + } catch (URISyntaxException e) { + throw new AssertionError(e); + } + } + + @Override + public void requestAllSyncData() throws IOException { + signal.sendSyncRequest(); + } + + @Override + public void receiveMessages( + final long timeout, + final TimeUnit unit, + final boolean returnOnTimeout, + final boolean ignoreAttachments, + final ReceiveMessageHandler handler + ) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasCaughtUpWithOldMessages() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { + return signal.isContactBlocked(recipient.getIdentifier()); + } + + @Override + public File getAttachmentFile(final SignalServiceAttachmentRemoteId attachmentId) { + throw new UnsupportedOperationException(); + } + + @Override + public void sendContacts() throws IOException { + signal.sendContacts(); + } + + @Override + public List> getContacts() { + throw new UnsupportedOperationException(); + } + + @Override + public String getContactOrProfileName(final RecipientIdentifier.Single recipient) { + return signal.getContactName(recipient.getIdentifier()); + } + + @Override + public Group getGroup(final GroupId groupId) { + final var id = groupId.serialize(); + return new Group(groupId, + signal.getGroupName(id), + null, + null, + signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()), + Set.of(), + Set.of(), + Set.of(), + signal.isGroupBlocked(id), + 0, + false, + signal.isMember(id)); + } + + @Override + public List getIdentities() { + throw new UnsupportedOperationException(); + } + + @Override + public List getIdentities(final RecipientIdentifier.Single recipient) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean trustIdentityVerified(final RecipientIdentifier.Single recipient, final byte[] fingerprint) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean trustIdentityVerifiedSafetyNumber( + final RecipientIdentifier.Single recipient, final String safetyNumber + ) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean trustIdentityVerifiedSafetyNumber( + final RecipientIdentifier.Single recipient, final byte[] safetyNumber + ) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean trustIdentityAllKeys(final RecipientIdentifier.Single recipient) { + throw new UnsupportedOperationException(); + } + + @Override + public String computeSafetyNumber( + final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey + ) { + throw new UnsupportedOperationException(); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) { + return address; + } + + @Override + public void close() throws IOException { + } + + private SendMessageResults handleMessage( + Set recipients, + Function, Long> recipientsHandler, + Supplier noteToSelfHandler, + Function groupHandler + ) { + long timestamp = 0; + final var singleRecipients = recipients.stream() + .filter(r -> r instanceof RecipientIdentifier.Single) + .map(RecipientIdentifier.Single.class::cast) + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList()); + if (singleRecipients.size() > 0) { + timestamp = recipientsHandler.apply(singleRecipients); + } + + if (recipients.contains(RecipientIdentifier.NoteToSelf.INSTANCE)) { + timestamp = noteToSelfHandler.get(); + } + final var groupRecipients = recipients.stream() + .filter(r -> r instanceof RecipientIdentifier.Group) + .map(RecipientIdentifier.Group.class::cast) + .map(g -> g.groupId) + .collect(Collectors.toList()); + for (final var groupId : groupRecipients) { + timestamp = groupHandler.apply(groupId.serialize()); + } + return new SendMessageResults(timestamp, Map.of()); + } + + private String emptyIfNull(final String string) { + return string == null ? "" : string; + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index c8208774..1a4fdc10 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -67,7 +67,7 @@ public class DbusSignalImpl implements Signal { } @Override - public String getNumber() { + public String getSelfNumber() { return m.getSelfNumber(); } @@ -96,8 +96,6 @@ public class DbusSignalImpl implements Signal { @Override public List listDevices() { List devices; - List results = new ArrayList(); - try { devices = m.getLinkedDevices(); } catch (IOException | Error.Failure e) { @@ -345,7 +343,8 @@ public class DbusSignalImpl implements Signal { // the profile name @Override public String getContactName(final String number) { - return m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber())); + final var name = m.getContactOrProfileName(getSingleRecipientIdentifier(number, m.getSelfNumber())); + return name == null ? "" : name; } @Override @@ -403,7 +402,7 @@ public class DbusSignalImpl implements Signal { @Override public String getGroupName(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); - if (group == null) { + if (group == null || group.getTitle() == null) { return ""; } else { return group.getTitle(); @@ -423,15 +422,9 @@ public class DbusSignalImpl implements Signal { @Override public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) { try { - if (groupId.length == 0) { - groupId = null; - } - if (name.isEmpty()) { - name = null; - } - if (avatar.isEmpty()) { - avatar = null; - } + groupId = nullIfEmpty(groupId); + name = nullIfEmpty(name); + avatar = nullIfEmpty(avatar); final var memberIdentifiers = getSingleRecipientIdentifiers(members, m.getSelfNumber()); if (groupId == null) { final var results = m.createGroup(name, memberIdentifiers, avatar == null ? null : new File(avatar)); @@ -499,17 +492,19 @@ public class DbusSignalImpl implements Signal { @Override public void updateProfile( - final String givenName, - final String familyName, - final String about, - final String aboutEmoji, + String givenName, + String familyName, + String about, + String aboutEmoji, String avatarPath, final boolean removeAvatar ) { try { - if (avatarPath.isEmpty()) { - avatarPath = null; - } + givenName = nullIfEmpty(givenName); + familyName = nullIfEmpty(familyName); + about = nullIfEmpty(about); + aboutEmoji = nullIfEmpty(aboutEmoji); + avatarPath = nullIfEmpty(avatarPath); Optional avatarFile = removeAvatar ? Optional.absent() : avatarPath == null ? null : Optional.of(new File(avatarPath)); @@ -527,17 +522,7 @@ public class DbusSignalImpl implements Signal { String avatarPath, final boolean removeAvatar ) { - try { - if (avatarPath.isEmpty()) { - avatarPath = null; - } - Optional avatarFile = removeAvatar - ? Optional.absent() - : avatarPath == null ? null : Optional.of(new File(avatarPath)); - m.setProfile(name, null, about, aboutEmoji, avatarFile); - } catch (IOException e) { - throw new Error.Failure(e.getMessage()); - } + updateProfile(name, "", about, aboutEmoji, avatarPath, removeAvatar); } @Override @@ -766,4 +751,12 @@ public class DbusSignalImpl implements Signal { throw new Error.InvalidGroupId("Invalid group id: " + e.getMessage()); } } + + private byte[] nullIfEmpty(final byte[] array) { + return array.length == 0 ? null : array; + } + + private String nullIfEmpty(final String name) { + return name.isEmpty() ? null : name; + } } From f44b148946df5822f11a755ce3a4fba2d91d3b68 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 28 Sep 2021 23:48:16 +0200 Subject: [PATCH 0834/2005] Allow message from pending member if it's just a group update Fixes #751 --- .../org/asamk/signal/manager/helper/IncomingMessageHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 45173da4..64e16857 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -416,7 +416,7 @@ public final class IncomingMessageHandler { } final var recipientId = recipientResolver.resolveRecipient(source); - if (!group.isMember(recipientId)) { + if (!group.isMember(recipientId) && !(group.isPendingMember(recipientId) && message.isGroupV2Update())) { return true; } From c9f5550d1821ee99879c75db124baf46642fd846 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 30 Sep 2021 19:33:57 +0200 Subject: [PATCH 0835/2005] Remove workaround for getBoolean from JsonRpcLocalCommand --- src/main/java/org/asamk/signal/App.java | 4 ++-- src/main/java/org/asamk/signal/Main.java | 2 +- .../java/org/asamk/signal/commands/DaemonCommand.java | 8 ++++---- .../asamk/signal/commands/JsonRpcDispatcherCommand.java | 2 +- .../org/asamk/signal/commands/JsonRpcLocalCommand.java | 9 --------- .../org/asamk/signal/commands/ListGroupsCommand.java | 2 +- .../java/org/asamk/signal/commands/QuitGroupCommand.java | 2 +- .../java/org/asamk/signal/commands/ReceiveCommand.java | 2 +- .../java/org/asamk/signal/commands/RegisterCommand.java | 2 +- .../org/asamk/signal/commands/RemoteDeleteCommand.java | 2 +- src/main/java/org/asamk/signal/commands/SendCommand.java | 4 ++-- .../org/asamk/signal/commands/SendReactionCommand.java | 4 ++-- .../org/asamk/signal/commands/SendTypingCommand.java | 2 +- .../java/org/asamk/signal/commands/TrustCommand.java | 2 +- .../org/asamk/signal/commands/UnregisterCommand.java | 2 +- .../org/asamk/signal/commands/UpdateGroupCommand.java | 2 +- .../org/asamk/signal/commands/UpdateProfileCommand.java | 2 +- 17 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index bffbded5..c44c737c 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -117,8 +117,8 @@ public class App { var username = ns.getString("username"); - final var useDbus = ns.getBoolean("dbus"); - final var useDbusSystem = ns.getBoolean("dbus-system"); + final var useDbus = Boolean.TRUE.equals(ns.getBoolean("dbus")); + final var useDbusSystem = Boolean.TRUE.equals(ns.getBoolean("dbus-system")); if (useDbus || useDbusSystem) { // If username is null, it will connect to the default object path initDbusClient(command, username, useDbusSystem, outputWriter); diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index 26079ec6..2a95e6de 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -80,7 +80,7 @@ public class Main { return false; } - return ns.getBoolean("verbose"); + return Boolean.TRUE.equals(ns.getBoolean("verbose")); } private static void configureLogging(final boolean verbose) { diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 9878de15..5045db9a 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -54,10 +54,10 @@ public class DaemonCommand implements MultiLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); + boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); DBusConnection.DBusBusType busType; - if (ns.getBoolean("system")) { + if (Boolean.TRUE.equals(ns.getBoolean("system"))) { busType = DBusConnection.DBusBusType.SYSTEM; } else { busType = DBusConnection.DBusBusType.SESSION; @@ -83,10 +83,10 @@ public class DaemonCommand implements MultiLocalCommand { public void handleCommand( final Namespace ns, final List managers, final SignalCreator c, final OutputWriter outputWriter ) throws CommandException { - boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); + boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); DBusConnection.DBusBusType busType; - if (ns.getBoolean("system")) { + if (Boolean.TRUE.equals(ns.getBoolean("system"))) { busType = DBusConnection.DBusBusType.SYSTEM; } else { busType = DBusConnection.DBusBusType.SESSION; diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java index d0e4dfec..9af67322 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java @@ -65,7 +65,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); + final boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); final var objectMapper = Util.createJsonObjectMapper(); final var jsonRpcSender = new JsonRpcSender((JsonWriter) outputWriter); diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java index 24b45ee8..5b926732 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java @@ -64,14 +64,5 @@ public interface JsonRpcLocalCommand extends JsonRpcCommand> return super.getList(dest + "s"); } - - @Override - public Boolean getBoolean(String dest) { - Boolean maybeGotten = this.get(dest); - if (maybeGotten == null) { - maybeGotten = false; - } - return maybeGotten; - } } } diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index 1eda53ce..fd8c4b92 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -102,7 +102,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { jsonWriter.write(jsonGroups); } else { final var writer = (PlainTextWriter) outputWriter; - boolean detailed = ns.getBoolean("detailed"); + boolean detailed = Boolean.TRUE.equals(ns.getBoolean("detailed")); for (var group : groups) { printGroupPlainText(writer, group, detailed); } diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 7635f8ae..1d6611b5 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -61,7 +61,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { } catch (NotAGroupMemberException e) { logger.info("User is not a group member"); } - if (ns.getBoolean("delete")) { + if (Boolean.TRUE.equals(ns.getBoolean("delete"))) { logger.debug("Deleting group {}", groupId); m.deleteGroup(groupId); } diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 62b3164b..4686f26d 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -147,7 +147,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { returnOnTimeout = false; timeout = 3600; } - boolean ignoreAttachments = ns.getBoolean("ignore-attachments"); + boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); try { final var handler = outputWriter instanceof JsonWriter ? new JsonReceiveMessageHandler(m, (JsonWriter) outputWriter) : new ReceiveMessageHandler(m, (PlainTextWriter) outputWriter); diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index 96530889..af6c06ad 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -31,7 +31,7 @@ public class RegisterCommand implements RegistrationCommand { @Override public void handleCommand(final Namespace ns, final RegistrationManager m) throws CommandException { - final boolean voiceVerification = ns.getBoolean("voice"); + final boolean voiceVerification = Boolean.TRUE.equals(ns.getBoolean("voice")); final var captchaString = ns.getString("captcha"); final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", ""); diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index e515defe..c9eab95c 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -43,7 +43,7 @@ public class RemoteDeleteCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self")); final var recipientStrings = ns.getList("recipient"); final var groupIdStrings = ns.getList("group-id"); diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index 1cd2e674..dba7689f 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -58,7 +58,7 @@ public class SendCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self")); final var recipientStrings = ns.getList("recipient"); final var groupIdStrings = ns.getList("group-id"); @@ -67,7 +67,7 @@ public class SendCommand implements JsonRpcLocalCommand { recipientStrings, groupIdStrings); - final var isEndSession = ns.getBoolean("end-session"); + final var isEndSession = Boolean.TRUE.equals(ns.getBoolean("end-session")); if (isEndSession) { final var singleRecipients = recipientIdentifiers.stream() .filter(r -> r instanceof RecipientIdentifier.Single) diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index a1c6c319..857f603d 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -52,7 +52,7 @@ public class SendReactionCommand implements JsonRpcLocalCommand { public void handleCommand( final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { - final var isNoteToSelf = ns.getBoolean("note-to-self"); + final var isNoteToSelf = Boolean.TRUE.equals(ns.getBoolean("note-to-self")); final var recipientStrings = ns.getList("recipient"); final var groupIdStrings = ns.getList("group-id"); @@ -62,7 +62,7 @@ public class SendReactionCommand implements JsonRpcLocalCommand { groupIdStrings); final var emoji = ns.getString("emoji"); - final var isRemove = ns.getBoolean("remove"); + final var isRemove = Boolean.TRUE.equals(ns.getBoolean("remove")); final var targetAuthor = ns.getString("target-author"); final var targetTimestamp = ns.getLong("target-timestamp"); diff --git a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java index cfe66770..ba062b70 100644 --- a/src/main/java/org/asamk/signal/commands/SendTypingCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendTypingCommand.java @@ -41,7 +41,7 @@ public class SendTypingCommand implements JsonRpcLocalCommand { ) throws CommandException { final var recipientStrings = ns.getList("recipient"); final var groupIdStrings = ns.getList("group-id"); - final var action = ns.getBoolean("stop") ? TypingAction.STOP : TypingAction.START; + final var action = Boolean.TRUE.equals(ns.getBoolean("stop")) ? TypingAction.STOP : TypingAction.START; final var recipientIdentifiers = new HashSet(); if (recipientStrings != null) { diff --git a/src/main/java/org/asamk/signal/commands/TrustCommand.java b/src/main/java/org/asamk/signal/commands/TrustCommand.java index 9e59ad86..77fcc08a 100644 --- a/src/main/java/org/asamk/signal/commands/TrustCommand.java +++ b/src/main/java/org/asamk/signal/commands/TrustCommand.java @@ -39,7 +39,7 @@ public class TrustCommand implements JsonRpcLocalCommand { ) throws CommandException { var recipentString = ns.getString("recipient"); var recipient = CommandUtil.getSingleRecipientIdentifier(recipentString, m.getSelfNumber()); - if (ns.getBoolean("trust-all-known-keys")) { + if (Boolean.TRUE.equals(ns.getBoolean("trust-all-known-keys"))) { boolean res = m.trustIdentityAllKeys(recipient); if (!res) { throw new UserErrorException("Failed to set the trust for this number, make sure the number is correct."); diff --git a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java index 60260046..68a20375 100644 --- a/src/main/java/org/asamk/signal/commands/UnregisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnregisterCommand.java @@ -31,7 +31,7 @@ public class UnregisterCommand implements LocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { try { - if (ns.getBoolean("delete-account")) { + if (Boolean.TRUE.equals(ns.getBoolean("delete-account"))) { m.deleteAccount(); } else { m.unregister(); diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 4bbaa992..68bce2d2 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -121,7 +121,7 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand { var groupAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("admin"), localNumber); var groupRemoveAdmins = CommandUtil.getSingleRecipientIdentifiers(ns.getList("remove-admin"), localNumber); var groupAvatar = ns.getString("avatar"); - var groupResetLink = ns.getBoolean("reset-link"); + var groupResetLink = Boolean.TRUE.equals(ns.getBoolean("reset-link")); var groupLinkState = getGroupLinkState(ns.getString("link")); var groupExpiration = ns.getInt("expiration"); var groupAddMemberPermission = getGroupPermission(ns.getString("set-permission-add-member")); diff --git a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java index f6dcb30e..9890a597 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateProfileCommand.java @@ -42,7 +42,7 @@ public class UpdateProfileCommand implements JsonRpcLocalCommand { var about = ns.getString("about"); var aboutEmoji = ns.getString("about-emoji"); var avatarPath = ns.getString("avatar"); - boolean removeAvatar = ns.getBoolean("remove-avatar"); + boolean removeAvatar = Boolean.TRUE.equals(ns.getBoolean("remove-avatar")); Optional avatarFile = removeAvatar ? Optional.absent() From 6f5e72119e0c996f1efefecda11e33422d44a171 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 29 Sep 2021 19:38:31 +0200 Subject: [PATCH 0836/2005] Implement configuration handling Closes #747 --- .../org/asamk/signal/manager/Manager.java | 7 ++ .../org/asamk/signal/manager/ManagerImpl.java | 25 +++++ .../actions/SendSyncConfigurationAction.java | 20 ++++ .../configuration/ConfigurationStore.java | 93 +++++++++++++++++++ .../helper/IncomingMessageHandler.java | 13 ++- .../signal/manager/helper/SyncHelper.java | 10 ++ .../signal/manager/storage/SignalAccount.java | 26 +++++- man/signal-cli.1.adoc | 17 ++++ .../org/asamk/signal/commands/Commands.java | 1 + .../commands/UpdateConfigurationCommand.java | 55 +++++++++++ .../asamk/signal/dbus/DbusManagerImpl.java | 10 ++ 11 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java create mode 100644 lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java create mode 100644 src/main/java/org/asamk/signal/commands/UpdateConfigurationCommand.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index cba438f8..cd7b0335 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -98,6 +98,13 @@ public interface Manager extends Closeable { void updateAccountAttributes(String deviceName) throws IOException; + void updateConfiguration( + final Boolean readReceipts, + final Boolean unidentifiedDeliveryIndicators, + final Boolean typingIndicators, + final Boolean linkPreviews + ) throws IOException, NotMasterDeviceException; + void setProfile( String givenName, String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException; diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index de60fa50..36c131db 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -320,6 +320,31 @@ public class ManagerImpl implements Manager { account.isDiscoverableByPhoneNumber()); } + @Override + public void updateConfiguration( + final Boolean readReceipts, + final Boolean unidentifiedDeliveryIndicators, + final Boolean typingIndicators, + final Boolean linkPreviews + ) throws IOException, NotMasterDeviceException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + if (readReceipts != null) { + account.getConfigurationStore().setReadReceipts(readReceipts); + } + if (unidentifiedDeliveryIndicators != null) { + account.getConfigurationStore().setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators); + } + if (typingIndicators != null) { + account.getConfigurationStore().setTypingIndicators(typingIndicators); + } + if (linkPreviews != null) { + account.getConfigurationStore().setLinkPreviews(linkPreviews); + } + syncHelper.sendConfigurationMessage(); + } + /** * @param givenName if null, the previous givenName will be kept * @param familyName if null, the previous familyName will be kept diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java new file mode 100644 index 00000000..0e050f0a --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendSyncConfigurationAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.jobs.Context; + +public class SendSyncConfigurationAction implements HandleAction { + + private static final SendSyncConfigurationAction INSTANCE = new SendSyncConfigurationAction(); + + private SendSyncConfigurationAction() { + } + + public static SendSyncConfigurationAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSyncHelper().sendConfigurationMessage(); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java b/lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java new file mode 100644 index 00000000..e7e1b5f5 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/configuration/ConfigurationStore.java @@ -0,0 +1,93 @@ +package org.asamk.signal.manager.configuration; + +public class ConfigurationStore { + + private final Saver saver; + + private Boolean readReceipts; + private Boolean unidentifiedDeliveryIndicators; + private Boolean typingIndicators; + private Boolean linkPreviews; + + public ConfigurationStore(final Saver saver) { + this.saver = saver; + } + + public static ConfigurationStore fromStorage(Storage storage, Saver saver) { + final var store = new ConfigurationStore(saver); + store.readReceipts = storage.readReceipts; + store.unidentifiedDeliveryIndicators = storage.unidentifiedDeliveryIndicators; + store.typingIndicators = storage.typingIndicators; + store.linkPreviews = storage.linkPreviews; + return store; + } + + public Boolean getReadReceipts() { + return readReceipts; + } + + public void setReadReceipts(final boolean readReceipts) { + this.readReceipts = readReceipts; + saver.save(toStorage()); + } + + public Boolean getUnidentifiedDeliveryIndicators() { + return unidentifiedDeliveryIndicators; + } + + public void setUnidentifiedDeliveryIndicators(final boolean unidentifiedDeliveryIndicators) { + this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators; + saver.save(toStorage()); + } + + public Boolean getTypingIndicators() { + return typingIndicators; + } + + public void setTypingIndicators(final boolean typingIndicators) { + this.typingIndicators = typingIndicators; + saver.save(toStorage()); + } + + public Boolean getLinkPreviews() { + return linkPreviews; + } + + public void setLinkPreviews(final boolean linkPreviews) { + this.linkPreviews = linkPreviews; + saver.save(toStorage()); + } + + private Storage toStorage() { + return new Storage(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews); + } + + public static final class Storage { + + public Boolean readReceipts; + public Boolean unidentifiedDeliveryIndicators; + public Boolean typingIndicators; + public Boolean linkPreviews; + + // For deserialization + private Storage() { + } + + public Storage( + final Boolean readReceipts, + final Boolean unidentifiedDeliveryIndicators, + final Boolean typingIndicators, + final Boolean linkPreviews + ) { + this.readReceipts = readReceipts; + this.unidentifiedDeliveryIndicators = unidentifiedDeliveryIndicators; + this.typingIndicators = typingIndicators; + this.linkPreviews = linkPreviews; + } + } + + public interface Saver { + + void save(Storage storage); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 64e16857..dead91b0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -15,6 +15,7 @@ import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendReceiptAction; import org.asamk.signal.manager.actions.SendRetryMessageRequestAction; import org.asamk.signal.manager.actions.SendSyncBlockedListAction; +import org.asamk.signal.manager.actions.SendSyncConfigurationAction; import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction; import org.asamk.signal.manager.actions.SendSyncKeysAction; @@ -271,7 +272,9 @@ public final class IncomingMessageHandler { if (rm.isKeysRequest()) { actions.add(SendSyncKeysAction.create()); } - // TODO Handle rm.isConfigurationRequest(); + if (rm.isConfigurationRequest()) { + actions.add(SendSyncConfigurationAction.create()); + } } if (syncMessage.getGroups().isPresent()) { logger.warn("Received a group v1 sync message, that can't be handled anymore, ignoring."); @@ -353,7 +356,13 @@ public final class IncomingMessageHandler { } } if (syncMessage.getConfiguration().isPresent()) { - // TODO + final var configurationMessage = syncMessage.getConfiguration().get(); + account.getConfigurationStore().setReadReceipts(configurationMessage.getReadReceipts().orNull()); + account.getConfigurationStore().setLinkPreviews(configurationMessage.getLinkPreviews().orNull()); + account.getConfigurationStore().setTypingIndicators(configurationMessage.getTypingIndicators().orNull()); + account.getConfigurationStore() + .setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators() + .orNull()); } return actions; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 6db1ca7d..e3fc7fc2 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -14,6 +14,7 @@ import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; +import org.whispersystems.signalservice.api.messages.multidevice.ConfigurationMessage; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; @@ -221,6 +222,15 @@ public class SyncHelper { sendHelper.sendSyncMessage(SignalServiceSyncMessage.forKeys(keysMessage)); } + public void sendConfigurationMessage() throws IOException { + final var config = account.getConfigurationStore(); + var configurationMessage = new ConfigurationMessage(Optional.fromNullable(config.getReadReceipts()), + Optional.fromNullable(config.getUnidentifiedDeliveryIndicators()), + Optional.fromNullable(config.getTypingIndicators()), + Optional.fromNullable(config.getLinkPreviews())); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forConfiguration(configurationMessage)); + } + public void handleSyncDeviceContacts(final InputStream input) throws IOException { final var s = new DeviceContactsInputStream(input); DeviceContact c; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index fd4ec597..9c51017c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.configuration.ConfigurationStore; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.storage.contacts.ContactsStore; import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore; @@ -103,6 +104,8 @@ public class SignalAccount implements Closeable { private RecipientStore recipientStore; private StickerStore stickerStore; private StickerStore.Storage stickerStoreStorage; + private ConfigurationStore configurationStore; + private ConfigurationStore.Storage configurationStoreStorage; private MessageCache messageCache; @@ -159,6 +162,7 @@ public class SignalAccount implements Closeable { account.recipientStore, account::saveGroupStore); account.stickerStore = new StickerStore(account::saveStickerStore); + account.configurationStore = new ConfigurationStore(account::saveConfigurationStore); account.registered = false; @@ -267,6 +271,7 @@ public class SignalAccount implements Closeable { account.recipientStore, account::saveGroupStore); account.stickerStore = new StickerStore(account::saveStickerStore); + account.configurationStore = new ConfigurationStore(account::saveConfigurationStore); account.recipientStore.resolveRecipientTrusted(account.getSelfAddress()); account.migrateLegacyConfigs(); @@ -491,6 +496,15 @@ public class SignalAccount implements Closeable { stickerStore = new StickerStore(this::saveStickerStore); } + if (rootNode.hasNonNull("configurationStore")) { + configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"), + ConfigurationStore.Storage.class); + configurationStore = ConfigurationStore.fromStorage(configurationStoreStorage, + this::saveConfigurationStore); + } else { + configurationStore = new ConfigurationStore(this::saveConfigurationStore); + } + migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig; if (migratedLegacyConfig) { @@ -677,6 +691,11 @@ public class SignalAccount implements Closeable { save(); } + private void saveConfigurationStore(ConfigurationStore.Storage storage) { + this.configurationStoreStorage = storage; + save(); + } + private void save() { synchronized (fileChannel) { var rootNode = jsonProcessor.createObjectNode(); @@ -707,7 +726,8 @@ public class SignalAccount implements Closeable { profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize())) .put("registered", registered) .putPOJO("groupStore", groupStoreStorage) - .putPOJO("stickerStore", stickerStoreStorage); + .putPOJO("stickerStore", stickerStoreStorage) + .putPOJO("configurationStore", configurationStoreStorage); try { try (var output = new ByteArrayOutputStream()) { // Write to memory first to prevent corrupting the file in case of serialization errors @@ -797,6 +817,10 @@ public class SignalAccount implements Closeable { return senderKeyStore; } + public ConfigurationStore getConfigurationStore() { + return configurationStore; + } + public MessageCache getMessageCache() { return messageCache; } diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 573ade7c..9829fe00 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -113,6 +113,23 @@ Can fix problems with receiving messages. *-n* NAME, *--device-name* NAME:: Set a new device name for the main or linked device +=== updateConfiguration + +Update signal configs and sync them to linked devices. +This command only works on the main devices. + +*--read-receipts* {true,false}:: +Indicates if Signal should send read receipts. + +*--unidentified-delivery-indicators* {true,false}:: +Indicates if Signal should show unidentified delivery indicators. + +*--typing-indicators* {true,false}:: +Indicates if Signal should send/show typing indicators. + +*--link-previews* {true,false}:: +Indicates if Signal should generate link previews. + === setPin Set a registration lock pin, to prevent others from registering this number. diff --git a/src/main/java/org/asamk/signal/commands/Commands.java b/src/main/java/org/asamk/signal/commands/Commands.java index 5d637eee..1d6dd26d 100644 --- a/src/main/java/org/asamk/signal/commands/Commands.java +++ b/src/main/java/org/asamk/signal/commands/Commands.java @@ -39,6 +39,7 @@ public class Commands { addCommand(new UnblockCommand()); addCommand(new UnregisterCommand()); addCommand(new UpdateAccountCommand()); + addCommand(new UpdateConfigurationCommand()); addCommand(new UpdateContactCommand()); addCommand(new UpdateGroupCommand()); addCommand(new UpdateProfileCommand()); diff --git a/src/main/java/org/asamk/signal/commands/UpdateConfigurationCommand.java b/src/main/java/org/asamk/signal/commands/UpdateConfigurationCommand.java new file mode 100644 index 00000000..9ca126d0 --- /dev/null +++ b/src/main/java/org/asamk/signal/commands/UpdateConfigurationCommand.java @@ -0,0 +1,55 @@ +package org.asamk.signal.commands; + +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import org.asamk.signal.OutputWriter; +import org.asamk.signal.commands.exceptions.CommandException; +import org.asamk.signal.commands.exceptions.IOErrorException; +import org.asamk.signal.commands.exceptions.UserErrorException; +import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.NotMasterDeviceException; + +import java.io.IOException; + +public class UpdateConfigurationCommand implements JsonRpcLocalCommand { + + @Override + public String getName() { + return "updateConfiguration"; + } + + @Override + public void attachToSubparser(final Subparser subparser) { + subparser.help("Update signal configs and sync them to linked devices."); + subparser.addArgument("--read-receipts") + .type(Boolean.class) + .help("Indicates if Signal should send read receipts."); + subparser.addArgument("--unidentified-delivery-indicators") + .type(Boolean.class) + .help("Indicates if Signal should show unidentified delivery indicators."); + subparser.addArgument("--typing-indicators") + .type(Boolean.class) + .help("Indicates if Signal should send/show typing indicators."); + subparser.addArgument("--link-previews") + .type(Boolean.class) + .help("Indicates if Signal should generate link previews."); + } + + @Override + public void handleCommand( + final Namespace ns, final Manager m, final OutputWriter outputWriter + ) throws CommandException { + final var readReceipts = ns.getBoolean("read-receipts"); + final var unidentifiedDeliveryIndicators = ns.getBoolean("unidentified-delivery-indicators"); + final var typingIndicators = ns.getBoolean("typing-indicators"); + final var linkPreviews = ns.getBoolean("link-previews"); + try { + m.updateConfiguration(readReceipts, unidentifiedDeliveryIndicators, typingIndicators, linkPreviews); + } catch (IOException e) { + throw new IOErrorException("UpdateAccount error: " + e.getMessage(), e); + } catch (NotMasterDeviceException e) { + throw new UserErrorException("This command doesn't work on linked devices."); + } + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index b9f5ae11..ea776797 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -93,6 +93,16 @@ public class DbusManagerImpl implements Manager { } } + @Override + public void updateConfiguration( + final Boolean readReceipts, + final Boolean unidentifiedDeliveryIndicators, + final Boolean typingIndicators, + final Boolean linkPreviews + ) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public void setProfile( final String givenName, From 9839be48f3ff80456ddec3ad28cf0b583bf80226 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 1 Oct 2021 17:52:33 +0200 Subject: [PATCH 0837/2005] Extract configurationStore variable --- .../java/org/asamk/signal/manager/ManagerImpl.java | 10 ++++++---- .../manager/helper/IncomingMessageHandler.java | 12 ++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 36c131db..86ec34c1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -330,17 +330,19 @@ public class ManagerImpl implements Manager { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } + + final var configurationStore = account.getConfigurationStore(); if (readReceipts != null) { - account.getConfigurationStore().setReadReceipts(readReceipts); + configurationStore.setReadReceipts(readReceipts); } if (unidentifiedDeliveryIndicators != null) { - account.getConfigurationStore().setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators); + configurationStore.setUnidentifiedDeliveryIndicators(unidentifiedDeliveryIndicators); } if (typingIndicators != null) { - account.getConfigurationStore().setTypingIndicators(typingIndicators); + configurationStore.setTypingIndicators(typingIndicators); } if (linkPreviews != null) { - account.getConfigurationStore().setLinkPreviews(linkPreviews); + configurationStore.setLinkPreviews(linkPreviews); } syncHelper.sendConfigurationMessage(); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index dead91b0..16f47d3c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -357,12 +357,12 @@ public final class IncomingMessageHandler { } if (syncMessage.getConfiguration().isPresent()) { final var configurationMessage = syncMessage.getConfiguration().get(); - account.getConfigurationStore().setReadReceipts(configurationMessage.getReadReceipts().orNull()); - account.getConfigurationStore().setLinkPreviews(configurationMessage.getLinkPreviews().orNull()); - account.getConfigurationStore().setTypingIndicators(configurationMessage.getTypingIndicators().orNull()); - account.getConfigurationStore() - .setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators() - .orNull()); + final var configurationStore = account.getConfigurationStore(); + configurationStore.setReadReceipts(configurationMessage.getReadReceipts().orNull()); + configurationStore.setLinkPreviews(configurationMessage.getLinkPreviews().orNull()); + configurationStore.setTypingIndicators(configurationMessage.getTypingIndicators().orNull()); + configurationStore.setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators() + .orNull()); } return actions; } From 1548ce9c795662a0dcd6666415c2ecc0a5a88852 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 2 Oct 2021 17:16:08 +0200 Subject: [PATCH 0838/2005] Add helper classes for exporting dbus properties --- .../dbus/DbusInterfacePropertiesHandler.java | 46 +++++++++++++ .../org/asamk/signal/dbus/DbusProperties.java | 66 +++++++++++++++++++ .../org/asamk/signal/dbus/DbusProperty.java | 35 ++++++++++ 3 files changed, 147 insertions(+) create mode 100644 src/main/java/org/asamk/signal/dbus/DbusInterfacePropertiesHandler.java create mode 100644 src/main/java/org/asamk/signal/dbus/DbusProperties.java create mode 100644 src/main/java/org/asamk/signal/dbus/DbusProperty.java diff --git a/src/main/java/org/asamk/signal/dbus/DbusInterfacePropertiesHandler.java b/src/main/java/org/asamk/signal/dbus/DbusInterfacePropertiesHandler.java new file mode 100644 index 00000000..d3c2ca83 --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusInterfacePropertiesHandler.java @@ -0,0 +1,46 @@ +package org.asamk.signal.dbus; + +import org.asamk.Signal; + +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class DbusInterfacePropertiesHandler { + + private final String interfaceName; + private final List> properties; + + public DbusInterfacePropertiesHandler( + final String interfaceName, final List> properties + ) { + this.interfaceName = interfaceName; + this.properties = properties; + } + + public String getInterfaceName() { + return interfaceName; + } + + @SuppressWarnings("unchecked") + private DbusProperty findProperty(String propertyName) { + final var property = properties.stream().filter(p -> p.getName().equals(propertyName)).findFirst(); + if (property.isEmpty()) { + throw new Signal.Error.Failure("Property not found"); + } + return (DbusProperty) property.get(); + } + + Consumer getSetter(String propertyName) { + return this.findProperty(propertyName).getSetter(); + } + + Supplier getGetter(String propertyName) { + return this.findProperty(propertyName).getGetter(); + } + + Collection> getProperties() { + return properties; + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusProperties.java b/src/main/java/org/asamk/signal/dbus/DbusProperties.java new file mode 100644 index 00000000..37cc35e3 --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusProperties.java @@ -0,0 +1,66 @@ +package org.asamk.signal.dbus; + +import org.asamk.Signal; +import org.freedesktop.dbus.interfaces.Properties; +import org.freedesktop.dbus.types.Variant; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public abstract class DbusProperties implements Properties { + + private final List handlers = new ArrayList<>(); + + protected void addPropertiesHandler(DbusInterfacePropertiesHandler handler) { + this.handlers.add(handler); + } + + DbusInterfacePropertiesHandler getHandler(String interfaceName) { + final var handler = getHandlerOptional(interfaceName); + if (handler.isEmpty()) { + throw new Signal.Error.Failure("Property not found"); + } + return handler.get(); + } + + private java.util.Optional getHandlerOptional(final String interfaceName) { + return handlers.stream().filter(h -> h.getInterfaceName().equals(interfaceName)).findFirst(); + } + + @Override + @SuppressWarnings("unchecked") + public A Get(final String interface_name, final String property_name) { + final var handler = getHandler(interface_name); + final var getter = handler.getGetter(property_name); + if (getter == null) { + throw new Signal.Error.Failure("Property not found"); + } + return (A) getter.get(); + } + + @Override + public void Set(final String interface_name, final String property_name, final A value) { + final var handler = getHandler(interface_name); + final var setter = handler.getSetter(property_name); + if (setter == null) { + throw new Signal.Error.Failure("Property not found"); + } + setter.accept(value); + } + + @Override + public Map> GetAll(final String interface_name) { + final var handler = getHandlerOptional(interface_name); + if (handler.isEmpty()) { + return Map.of(); + } + + return handler.get() + .getProperties() + .stream() + .filter(p -> p.getGetter() != null) + .collect(Collectors.toMap(DbusProperty::getName, p -> new Variant<>(p.getGetter().get()))); + } +} diff --git a/src/main/java/org/asamk/signal/dbus/DbusProperty.java b/src/main/java/org/asamk/signal/dbus/DbusProperty.java new file mode 100644 index 00000000..e0557786 --- /dev/null +++ b/src/main/java/org/asamk/signal/dbus/DbusProperty.java @@ -0,0 +1,35 @@ +package org.asamk.signal.dbus; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +public class DbusProperty { + + private final String name; + private final Supplier getter; + private final Consumer setter; + + public DbusProperty(final String name, final Supplier getter, final Consumer setter) { + this.name = name; + this.getter = getter; + this.setter = setter; + } + + public DbusProperty(final String name, final Supplier getter) { + this.name = name; + this.getter = getter; + this.setter = null; + } + + public String getName() { + return name; + } + + public Consumer getSetter() { + return setter; + } + + public Supplier getGetter() { + return getter; + } +} From 778adacb80bae7d6ecc1d70fa87f9217c7bc1c71 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 2 Oct 2021 18:04:30 +0200 Subject: [PATCH 0839/2005] Refactor dbus linked devices interface Export a separate dbus object for each device --- .../org/asamk/signal/manager/Manager.java | 2 +- .../org/asamk/signal/manager/ManagerImpl.java | 2 +- src/main/java/org/asamk/Signal.java | 18 ++- src/main/java/org/asamk/signal/App.java | 2 +- .../asamk/signal/commands/DaemonCommand.java | 10 +- .../signal/commands/RemoveDeviceCommand.java | 4 +- .../asamk/signal/dbus/DbusManagerImpl.java | 38 ++++-- .../org/asamk/signal/dbus/DbusSignalImpl.java | 119 ++++++++++++++---- 8 files changed, 157 insertions(+), 38 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index cd7b0335..7a421966 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -117,7 +117,7 @@ public interface Manager extends Closeable { List getLinkedDevices() throws IOException; - void removeLinkedDevices(int deviceId) throws IOException; + void removeLinkedDevices(long deviceId) throws IOException; void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException; diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 86ec34c1..6a039c69 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -414,7 +414,7 @@ public class ManagerImpl implements Manager { } @Override - public void removeLinkedDevices(int deviceId) throws IOException { + public void removeLinkedDevices(long deviceId) throws IOException { dependencies.getAccountManager().removeDevice(deviceId); var devices = dependencies.getAccountManager().getDevices(); account.setMultiDevice(devices.size() > 1); diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index cc521f6d..b8800085 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,8 +1,11 @@ package org.asamk; +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.annotations.DBusProperty; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; +import org.freedesktop.dbus.interfaces.Properties; import org.freedesktop.dbus.messages.DBusSignal; import java.util.List; @@ -97,11 +100,11 @@ public interface Signal extends DBusInterface { void addDevice(String uri) throws Error.InvalidUri; - void removeDevice(int deviceId) throws Error.Failure; + DBusPath getDevice(long deviceId); - List listDevices() throws Error.Failure; + List listDevices() throws Error.Failure; - void updateDeviceName(String deviceName) throws Error.Failure; + DBusPath getThisDevice(); void updateProfile( String givenName, @@ -255,6 +258,15 @@ public interface Signal extends DBusInterface { } } + @DBusProperty(name = "Id", type = Integer.class, access = DBusProperty.Access.READ) + @DBusProperty(name = "Name", type = String.class) + @DBusProperty(name = "Created", type = String.class, access = DBusProperty.Access.READ) + @DBusProperty(name = "LastSeen", type = String.class, access = DBusProperty.Access.READ) + interface Device extends DBusInterface, Properties { + + void removeDevice() throws Error.Failure; + } + interface Error { class AttachmentInvalid extends DBusExecutionException { diff --git a/src/main/java/org/asamk/signal/App.java b/src/main/java/org/asamk/signal/App.java index c44c737c..3d35ff8f 100644 --- a/src/main/java/org/asamk/signal/App.java +++ b/src/main/java/org/asamk/signal/App.java @@ -349,7 +349,7 @@ public class App { ((ExtendedDbusCommand) command).handleCommand(ns, ts, dBusConn, outputWriter); } else if (command instanceof LocalCommand) { try { - ((LocalCommand) command).handleCommand(ns, new DbusManagerImpl(ts), outputWriter); + ((LocalCommand) command).handleCommand(ns, new DbusManagerImpl(ts, dBusConn), outputWriter); } catch (UnsupportedOperationException e) { throw new UserErrorException("Command is not yet implemented via dbus", e); } catch (DBusExecutionException e) { diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 5045db9a..02063b87 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -120,7 +120,10 @@ public class DaemonCommand implements MultiLocalCommand { private Thread run( DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter, boolean ignoreAttachments ) throws DBusException { - conn.exportObject(new DbusSignalImpl(m, objectPath)); + final var signal = new DbusSignalImpl(m, conn, objectPath); + conn.exportObject(signal); + final var initThread = new Thread(signal::initObjects); + initThread.start(); logger.info("Exported dbus object: " + objectPath); @@ -136,6 +139,11 @@ public class DaemonCommand implements MultiLocalCommand { logger.warn("Receiving messages failed, retrying", e); } } + try { + initThread.join(); + } catch (InterruptedException ignored) { + } + signal.close(); }); thread.start(); diff --git a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java index d67cc5ea..4fcad79d 100644 --- a/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoveDeviceCommand.java @@ -21,7 +21,7 @@ public class RemoveDeviceCommand implements JsonRpcLocalCommand { public void attachToSubparser(final Subparser subparser) { subparser.help("Remove a linked device."); subparser.addArgument("-d", "--device-id", "--deviceId") - .type(int.class) + .type(long.class) .required(true) .help("Specify the device you want to remove. Use listDevices to see the deviceIds."); } @@ -31,7 +31,7 @@ public class RemoveDeviceCommand implements JsonRpcLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { try { - int deviceId = ns.getInt("device-id"); + final var deviceId = ns.getLong("device-id"); m.removeLinkedDevices(deviceId); } catch (IOException e) { throw new IOErrorException("Error while removing device: " + e.getMessage(), e); diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index ea776797..3124a5b0 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -1,6 +1,7 @@ package org.asamk.signal.dbus; import org.asamk.Signal; +import org.asamk.signal.DbusConfig; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -25,6 +26,10 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.exceptions.DBusException; +import org.freedesktop.dbus.interfaces.DBusInterface; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; @@ -58,9 +63,11 @@ import java.util.stream.Collectors; public class DbusManagerImpl implements Manager { private final Signal signal; + private final DBusConnection connection; - public DbusManagerImpl(final Signal signal) { + public DbusManagerImpl(final Signal signal, DBusConnection connection) { this.signal = signal; + this.connection = connection; } @Override @@ -89,7 +96,8 @@ public class DbusManagerImpl implements Manager { @Override public void updateAccountAttributes(final String deviceName) throws IOException { if (deviceName != null) { - signal.updateDeviceName(deviceName); + final var devicePath = signal.getThisDevice(); + getRemoteObject(devicePath, Signal.Device.class).Set("org.asamk.Signal.Device", "Name", deviceName); } } @@ -136,15 +144,21 @@ public class DbusManagerImpl implements Manager { @Override public List getLinkedDevices() throws IOException { - return signal.listDevices() - .stream() - .map(name -> new Device(-1, name, 0, 0, false)) - .collect(Collectors.toList()); + final var thisDevice = signal.getThisDevice(); + return signal.listDevices().stream().map(devicePath -> { + final var device = getRemoteObject(devicePath, Signal.Device.class).GetAll("org.asamk.Signal.Device"); + return new Device((long) device.get("Id").getValue(), + (String) device.get("Name").getValue(), + (long) device.get("Created").getValue(), + (long) device.get("LastSeen").getValue(), + thisDevice.equals(devicePath)); + }).collect(Collectors.toList()); } @Override - public void removeLinkedDevices(final int deviceId) throws IOException { - signal.removeDevice(deviceId); + public void removeLinkedDevices(final long deviceId) throws IOException { + final var devicePath = signal.getDevice(deviceId); + getRemoteObject(devicePath, Signal.Device.class).removeDevice(); } @Override @@ -494,4 +508,12 @@ public class DbusManagerImpl implements Manager { private String emptyIfNull(final String string) { return string == null ? "" : string; } + + private T getRemoteObject(final DBusPath devicePath, final Class type) { + try { + return connection.getRemoteObject(DbusConfig.getBusname(), devicePath.getPath(), type); + } catch (DBusException e) { + throw new AssertionError(e); + } + } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 1a4fdc10..ab9c89b2 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -7,7 +7,6 @@ import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; import org.asamk.signal.manager.StickerPackInvalidException; import org.asamk.signal.manager.UntrustedIdentityException; -import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -21,6 +20,9 @@ import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.util.ErrorUtils; +import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.connections.impl.DBusConnection; +import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; @@ -49,16 +51,24 @@ import java.util.stream.Stream; public class DbusSignalImpl implements Signal { private final Manager m; + private final DBusConnection connection; private final String objectPath; - public DbusSignalImpl(final Manager m, final String objectPath) { + private DBusPath thisDevice; + private final List devices = new ArrayList<>(); + + public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) { this.m = m; + this.connection = connection; this.objectPath = objectPath; } - @Override - public boolean isRemote() { - return false; + public void initObjects() { + updateDevices(); + } + + public void close() { + unExportDevices(); } @Override @@ -85,33 +95,51 @@ public class DbusSignalImpl implements Signal { } @Override - public void removeDevice(int deviceId) { - try { - m.removeLinkedDevices(deviceId); - } catch (IOException e) { - throw new Error.Failure(e.getClass().getSimpleName() + ": Error while removing device: " + e.getMessage()); - } + public DBusPath getDevice(long deviceId) { + updateDevices(); + return new DBusPath(getDeviceObjectPath(objectPath, deviceId)); } @Override - public List listDevices() { - List devices; + public List listDevices() { + updateDevices(); + return this.devices; + } + + private void updateDevices() { + List linkedDevices; try { - devices = m.getLinkedDevices(); + linkedDevices = m.getLinkedDevices(); } catch (IOException | Error.Failure e) { throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); } - return devices.stream().map(d -> d.getName() == null ? "" : d.getName()).collect(Collectors.toList()); + unExportDevices(); + + linkedDevices.forEach(d -> { + final var object = new DbusSignalDeviceImpl(d); + final var deviceObjectPath = object.getObjectPath(); + try { + connection.exportObject(object); + } catch (DBusException e) { + e.printStackTrace(); + } + if (d.isThisDevice()) { + thisDevice = new DBusPath(deviceObjectPath); + } + this.devices.add(new DBusPath(deviceObjectPath)); + }); + } + + private void unExportDevices() { + this.devices.stream().map(DBusPath::getPath).forEach(connection::unExportObject); + this.devices.clear(); } @Override - public void updateDeviceName(String deviceName) { - try { - m.updateAccountAttributes(deviceName); - } catch (IOException | Signal.Error.Failure e) { - throw new Error.Failure("UpdateAccount error: " + e.getMessage()); - } + public DBusPath getThisDevice() { + updateDevices(); + return thisDevice; } @Override @@ -759,4 +787,53 @@ public class DbusSignalImpl implements Signal { private String nullIfEmpty(final String name) { return name.isEmpty() ? null : name; } + + private static String getDeviceObjectPath(String basePath, long deviceId) { + return basePath + "/Devices/" + deviceId; + } + + public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device { + + private final org.asamk.signal.manager.api.Device device; + + public DbusSignalDeviceImpl(final org.asamk.signal.manager.api.Device device) { + super(); + super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Device", + List.of(new DbusProperty<>("Id", device::getId), + new DbusProperty<>("Name", + () -> device.getName() == null ? "" : device.getName(), + this::setDeviceName), + new DbusProperty<>("Created", device::getCreated), + new DbusProperty<>("LastSeen", device::getLastSeen)))); + this.device = device; + } + + @Override + public String getObjectPath() { + return getDeviceObjectPath(objectPath, device.getId()); + } + + @Override + public void removeDevice() throws Error.Failure { + try { + m.removeLinkedDevices(device.getId()); + updateDevices(); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + + private void setDeviceName(String name) { + if (!device.isThisDevice()) { + throw new Error.Failure("Only the name of this device can be changed"); + } + try { + m.updateAccountAttributes(name); + // update device list + updateDevices(); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + } } From 8b83992e95dbd0f888ec71ee751613a77ec00820 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 2 Oct 2021 18:40:36 +0200 Subject: [PATCH 0840/2005] Don't repeatedly try to refetch group info if permission was denied i.e. if the user is no longer a member of that group --- .../signal/manager/helper/GroupHelper.java | 23 ++++++++--- .../signal/manager/helper/GroupV2Helper.java | 10 ++++- .../manager/storage/groups/GroupInfoV2.java | 20 +++++++++- .../manager/storage/groups/GroupStore.java | 38 ++++--------------- 4 files changed, 54 insertions(+), 37 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 3ddd6edd..62f4f111 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -145,7 +145,10 @@ public class GroupHelper { groupMasterKey); } if (group == null) { - group = groupV2Helper.getDecryptedGroup(groupSecretParams); + try { + group = groupV2Helper.getDecryptedGroup(groupSecretParams); + } catch (NotAGroupMemberException ignored) { + } } if (group != null) { storeProfileKeysFromMembers(group); @@ -348,10 +351,20 @@ public class GroupHelper { private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { final var group = account.getGroupStore().getGroup(groupId); - if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) { - final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); - ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), recipientResolver); - account.getGroupStore().updateGroup(group); + if (group instanceof GroupInfoV2) { + final var groupInfoV2 = (GroupInfoV2) group; + if (forceUpdate || (!groupInfoV2.isPermissionDenied() && groupInfoV2.getGroup() == null)) { + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); + DecryptedGroup decryptedGroup; + try { + decryptedGroup = groupV2Helper.getDecryptedGroup(groupSecretParams); + } catch (NotAGroupMemberException e) { + groupInfoV2.setPermissionDenied(true); + decryptedGroup = null; + } + groupInfoV2.setGroup(decryptedGroup, recipientResolver); + account.getGroupStore().updateGroup(group); + } } return group; } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 3187fca1..746af2f9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -6,6 +6,7 @@ import org.asamk.signal.manager.groups.GroupLinkPassword; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; @@ -35,6 +36,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException; import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.File; @@ -78,10 +80,16 @@ public class GroupV2Helper { this.addressResolver = addressResolver; } - public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) { + public DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException { try { final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); return groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString); + } catch (NonSuccessfulResponseCodeException e) { + if (e.getCode() == 403) { + throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null); + } + logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); + return null; } catch (IOException | VerificationFailedException | InvalidGroupStateException e) { logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage()); return null; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index f86dcb04..a06b83df 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -22,16 +22,23 @@ public class GroupInfoV2 extends GroupInfo { private boolean blocked; private DecryptedGroup group; // stored as a file with hexadecimal groupId as name private RecipientResolver recipientResolver; + private boolean permissionDenied; public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey) { this.groupId = groupId; this.masterKey = masterKey; } - public GroupInfoV2(final GroupIdV2 groupId, final GroupMasterKey masterKey, final boolean blocked) { + public GroupInfoV2( + final GroupIdV2 groupId, + final GroupMasterKey masterKey, + final boolean blocked, + final boolean permissionDenied + ) { this.groupId = groupId; this.masterKey = masterKey; this.blocked = blocked; + this.permissionDenied = permissionDenied; } @Override @@ -44,6 +51,9 @@ public class GroupInfoV2 extends GroupInfo { } public void setGroup(final DecryptedGroup group, final RecipientResolver recipientResolver) { + if (group != null) { + this.permissionDenied = false; + } this.group = group; this.recipientResolver = recipientResolver; } @@ -151,4 +161,12 @@ public class GroupInfoV2 extends GroupInfo { public boolean isAnnouncementGroup() { return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED; } + + public void setPermissionDenied(final boolean permissionDenied) { + this.permissionDenied = permissionDenied; + } + + public boolean isPermissionDenied() { + return permissionDenied; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java index 4adc413a..fe8f85a6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -104,7 +104,7 @@ public class GroupStore { throw new AssertionError("Invalid master key for group " + groupId.toBase64()); } - return new GroupInfoV2(groupId, masterKey, g2.blocked); + return new GroupInfoV2(groupId, masterKey, g2.blocked, g2.permissionDenied); }).collect(Collectors.toMap(GroupInfo::getGroupId, g -> g)); return new GroupStore(groupCachePath, groups, recipientResolver, saver); @@ -268,13 +268,13 @@ public class GroupStore { final var g2 = (GroupInfoV2) g; return new Storage.GroupV2(g2.getGroupId().toBase64(), Base64.getEncoder().encodeToString(g2.getMasterKey().serialize()), - g2.isBlocked()); + g2.isBlocked(), + g2.isPermissionDenied()); }).collect(Collectors.toList())); } public static class Storage { - // @JsonSerialize(using = GroupsSerializer.class) @JsonDeserialize(using = GroupsDeserializer.class) public List groups; @@ -408,46 +408,24 @@ public class GroupStore { public String groupId; public String masterKey; public boolean blocked; + public boolean permissionDenied; // For deserialization private GroupV2() { } - public GroupV2(final String groupId, final String masterKey, final boolean blocked) { + public GroupV2( + final String groupId, final String masterKey, final boolean blocked, final boolean permissionDenied + ) { this.groupId = groupId; this.masterKey = masterKey; this.blocked = blocked; + this.permissionDenied = permissionDenied; } } } - // private static class GroupsSerializer extends JsonSerializer> { -// -// @Override -// public void serialize( -// final List groups, final JsonGenerator jgen, final SerializerProvider provider -// ) throws IOException { -// jgen.writeStartArray(groups.size()); -// for (var group : groups) { -// if (group instanceof GroupInfoV1) { -// jgen.writeObject(group); -// } else if (group instanceof GroupInfoV2) { -// final var groupV2 = (GroupInfoV2) group; -// jgen.writeStartObject(); -// jgen.writeStringField("groupId", groupV2.getGroupId().toBase64()); -// jgen.writeStringField("masterKey", -// Base64.getEncoder().encodeToString(groupV2.getMasterKey().serialize())); -// jgen.writeBooleanField("blocked", groupV2.isBlocked()); -// jgen.writeEndObject(); -// } else { -// throw new AssertionError("Unknown group version"); -// } -// } -// jgen.writeEndArray(); -// } -// } -// private static class GroupsDeserializer extends JsonDeserializer> { @Override From 76ceac4d543d0682335758c77971f1c174107e63 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Oct 2021 13:14:43 +0200 Subject: [PATCH 0841/2005] Read configurations from storage --- .../java/org/asamk/signal/manager/helper/StorageHelper.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index 4caab519..63e6ca59 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -188,6 +188,12 @@ public class StorageHelper { return; } + account.getConfigurationStore().setReadReceipts(accountRecord.isReadReceiptsEnabled()); + account.getConfigurationStore().setTypingIndicators(accountRecord.isTypingIndicatorsEnabled()); + account.getConfigurationStore() + .setUnidentifiedDeliveryIndicators(accountRecord.isSealedSenderIndicatorsEnabled()); + account.getConfigurationStore().setLinkPreviews(accountRecord.isLinkPreviewsEnabled()); + if (accountRecord.getProfileKey().isPresent()) { try { account.setProfileKey(new ProfileKey(accountRecord.getProfileKey().get())); From 0709c0caf8d66b346eb82c8ecfb6f91049993f11 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Oct 2021 13:12:48 +0200 Subject: [PATCH 0842/2005] Update libsignal-service-java --- lib/build.gradle.kts | 2 +- .../java/org/asamk/signal/manager/config/LiveConfig.java | 3 +++ .../org/asamk/signal/manager/config/SandboxConfig.java | 3 +++ .../org/asamk/signal/manager/config/ServiceConfig.java | 8 +++++++- .../org/asamk/signal/manager/helper/ProfileHelper.java | 4 +++- .../org/asamk/signal/manager/helper/StorageHelper.java | 4 ++++ 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index dc6c910e..6e528805 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -14,7 +14,7 @@ repositories { } dependencies { - api("com.github.turasa:signal-service-java:2.15.3_unofficial_27") + api("com.github.turasa:signal-service-java:2.15.3_unofficial_28") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.slf4j:slf4j-api:1.7.30") diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index 7762a4cb..177f6697 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -7,6 +7,7 @@ import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalProxy; @@ -38,6 +39,7 @@ class LiveConfig { private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api.directory.signal.org"; private final static String SIGNAL_KEY_BACKUP_URL = "https://api.backup.signal.org"; private final static String STORAGE_URL = "https://storage.signal.org"; + private final static String SIGNAL_CDSH_URL = ""; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); private final static Optional dns = Optional.absent(); @@ -58,6 +60,7 @@ class LiveConfig { TRUST_STORE)}, new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, + new SignalCdshUrl[]{new SignalCdshUrl(SIGNAL_CDSH_URL, TRUST_STORE)}, interceptors, dns, proxy, diff --git a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java index bedec52c..d643f10a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java @@ -7,6 +7,7 @@ import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.TrustStore; import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl; +import org.whispersystems.signalservice.internal.configuration.SignalCdshUrl; import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl; import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl; import org.whispersystems.signalservice.internal.configuration.SignalProxy; @@ -38,6 +39,7 @@ class SandboxConfig { private final static String SIGNAL_CONTACT_DISCOVERY_URL = "https://api-staging.directory.signal.org"; private final static String SIGNAL_KEY_BACKUP_URL = "https://api-staging.backup.signal.org"; private final static String STORAGE_URL = "https://storage-staging.signal.org"; + private final static String SIGNAL_CDSH_URL = "https://cdsh.staging.signal.org"; private final static TrustStore TRUST_STORE = new WhisperTrustStore(); private final static Optional dns = Optional.absent(); @@ -58,6 +60,7 @@ class SandboxConfig { TRUST_STORE)}, new SignalKeyBackupServiceUrl[]{new SignalKeyBackupServiceUrl(SIGNAL_KEY_BACKUP_URL, TRUST_STORE)}, new SignalStorageUrl[]{new SignalStorageUrl(STORAGE_URL, TRUST_STORE)}, + new SignalCdshUrl[]{new SignalCdshUrl(SIGNAL_CDSH_URL, TRUST_STORE)}, interceptors, dns, proxy, diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index 3677bba1..a9a08d93 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -39,7 +39,13 @@ public class ServiceConfig { logger.warn("Failed to call libzkgroup: {}", e.getMessage()); zkGroupAvailable = false; } - capabilities = new AccountAttributes.Capabilities(false, zkGroupAvailable, false, zkGroupAvailable, true, true); + capabilities = new AccountAttributes.Capabilities(false, + zkGroupAvailable, + false, + zkGroupAvailable, + true, + true, + false); try { TrustStore contactTrustStore = new IasTrustStore(); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index d4f8ae5d..46c83e9d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -32,6 +32,7 @@ import java.nio.file.Files; import java.util.Base64; import java.util.Date; import java.util.HashSet; +import java.util.List; import java.util.Set; import io.reactivex.rxjava3.core.Single; @@ -136,7 +137,8 @@ public final class ProfileHelper { newProfile.getAbout() == null ? "" : newProfile.getAbout(), newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), Optional.absent(), - streamDetails); + streamDetails, + List.of(/* TODO */)); } if (avatar != null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index 63e6ca59..f76c95fb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -188,6 +188,10 @@ public class StorageHelper { return; } + if (!accountRecord.getE164().equals(account.getUsername())) { + // TODO implement changed number handling + } + account.getConfigurationStore().setReadReceipts(accountRecord.isReadReceiptsEnabled()); account.getConfigurationStore().setTypingIndicators(accountRecord.isTypingIndicatorsEnabled()); account.getConfigurationStore() From 26594dd0eed44225d7d4a17571597a81e4e3b58a Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Oct 2021 16:17:58 +0200 Subject: [PATCH 0843/2005] Retrieve self profile from storage --- .../org/asamk/signal/manager/ManagerImpl.java | 2 +- .../signal/manager/RegistrationManager.java | 6 +- .../signal/manager/helper/ProfileHelper.java | 68 ++++++++++++++----- .../signal/manager/helper/StorageHelper.java | 23 ++++++- .../signal/manager/storage/SignalAccount.java | 1 + .../manager/storage/recipients/Profile.java | 16 +++++ .../storage/recipients/RecipientStore.java | 5 ++ .../signal/manager/util/ProfileUtils.java | 1 + 8 files changed, 99 insertions(+), 23 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 6a039c69..0fd1eb33 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -209,7 +209,7 @@ public class ManagerImpl implements Manager { avatarStore, this::resolveSignalServiceAddress, account.getRecipientStore()); - this.storageHelper = new StorageHelper(account, dependencies, groupHelper); + this.storageHelper = new StorageHelper(account, dependencies, groupHelper, profileHelper); this.contactHelper = new ContactHelper(account); this.syncHelper = new SyncHelper(account, attachmentHelper, diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index ff94c19b..c42782f7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -183,15 +183,15 @@ public class RegistrationManager implements Closeable { account = null; m.refreshPreKeys(); + if (response.isStorageCapable()) { + m.retrieveRemoteStorage(); + } // Set an initial empty profile so user can be added to groups try { m.setProfile(null, null, null, null, null); } catch (NoClassDefFoundError e) { logger.warn("Failed to set default profile: {}", e.getMessage()); } - if (response.isStorageCapable()) { - m.retrieveRemoteStorage(); - } final var result = m; m = null; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 46c83e9d..e24d41fa 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -33,6 +33,7 @@ import java.util.Base64; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import io.reactivex.rxjava3.core.Single; @@ -110,6 +111,17 @@ public final class ProfileHelper { */ public void setProfile( String givenName, final String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException { + setProfile(true, givenName, familyName, about, aboutEmoji, avatar); + } + + public void setProfile( + boolean uploadProfile, + String givenName, + final String familyName, + String about, + String aboutEmoji, + Optional avatar ) throws IOException { var profile = getRecipientProfile(account.getSelfRecipientId()); var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); @@ -127,18 +139,22 @@ public final class ProfileHelper { } var newProfile = builder.build(); - try (final var streamDetails = avatar == null - ? avatarStore.retrieveProfileAvatar(account.getSelfAddress()) - : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { - dependencies.getAccountManager() - .setVersionedProfile(account.getUuid(), - account.getProfileKey(), - newProfile.getInternalServiceName(), - newProfile.getAbout() == null ? "" : newProfile.getAbout(), - newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), - Optional.absent(), - streamDetails, - List.of(/* TODO */)); + if (uploadProfile) { + try (final var streamDetails = avatar == null + ? avatarStore.retrieveProfileAvatar(account.getSelfAddress()) + : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { + final var avatarPath = dependencies.getAccountManager() + .setVersionedProfile(account.getUuid(), + account.getProfileKey(), + newProfile.getInternalServiceName(), + newProfile.getAbout() == null ? "" : newProfile.getAbout(), + newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), + Optional.absent(), + streamDetails, + List.of(/* TODO */)); + builder.withAvatarUrlPath(avatarPath.orNull()); + newProfile = builder.build(); + } } if (avatar != null) { @@ -197,6 +213,7 @@ public final class ProfileHelper { null, null, null, + null, ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null), ProfileUtils.getCapabilities(encryptedProfile)); } @@ -242,15 +259,23 @@ public final class ProfileHelper { private Profile decryptProfileAndDownloadAvatar( final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile ) { - if (encryptedProfile.getAvatar() != null) { - downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId), - encryptedProfile.getAvatar(), - profileKey); - } + final var avatarPath = encryptedProfile.getAvatar(); + downloadProfileAvatar(recipientId, avatarPath, profileKey); return ProfileUtils.decryptProfile(profileKey, encryptedProfile); } + public void downloadProfileAvatar( + final RecipientId recipientId, final String avatarPath, final ProfileKey profileKey + ) { + var profile = account.getProfileStore().getProfile(recipientId); + if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) { + downloadProfileAvatar(addressResolver.resolveSignalServiceAddress(recipientId), avatarPath, profileKey); + var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); + account.getProfileStore().storeProfile(recipientId, builder.withAvatarUrlPath(avatarPath).build()); + } + } + private ProfileAndCredential retrieveProfileSync( RecipientId recipientId, SignalServiceProfile.RequestType requestType ) throws IOException { @@ -310,6 +335,15 @@ public final class ProfileHelper { private void downloadProfileAvatar( SignalServiceAddress address, String avatarPath, ProfileKey profileKey ) { + if (avatarPath == null) { + try { + avatarStore.deleteProfileAvatar(address); + } catch (IOException e) { + logger.warn("Failed to delete local profile avatar, ignoring: {}", e.getMessage()); + } + return; + } + try { avatarStore.storeProfileAvatar(address, outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream)); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index f76c95fb..b68e65b4 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -32,13 +32,18 @@ public class StorageHelper { private final SignalAccount account; private final SignalDependencies dependencies; private final GroupHelper groupHelper; + private final ProfileHelper profileHelper; public StorageHelper( - final SignalAccount account, final SignalDependencies dependencies, final GroupHelper groupHelper + final SignalAccount account, + final SignalDependencies dependencies, + final GroupHelper groupHelper, + final ProfileHelper profileHelper ) { this.account = account; this.dependencies = dependencies; this.groupHelper = groupHelper; + this.profileHelper = profileHelper; } public void readDataFromStorage() throws IOException { @@ -199,12 +204,26 @@ public class StorageHelper { account.getConfigurationStore().setLinkPreviews(accountRecord.isLinkPreviewsEnabled()); if (accountRecord.getProfileKey().isPresent()) { + ProfileKey profileKey; try { - account.setProfileKey(new ProfileKey(accountRecord.getProfileKey().get())); + profileKey = new ProfileKey(accountRecord.getProfileKey().get()); } catch (InvalidInputException e) { logger.warn("Received invalid profile key from storage"); + profileKey = null; + } + if (profileKey != null) { + account.setProfileKey(profileKey); + final var avatarPath = accountRecord.getAvatarUrlPath().orNull(); + profileHelper.downloadProfileAvatar(account.getSelfRecipientId(), avatarPath, profileKey); } } + + profileHelper.setProfile(false, + accountRecord.getGivenName().orNull(), + accountRecord.getFamilyName().orNull(), + null, + null, + null); } private SignalStorageRecord getSignalStorageRecord(final StorageId accountId) throws IOException { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 9c51017c..5bb9fdeb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -631,6 +631,7 @@ public class SignalAccount implements Closeable { profile.getFamilyName(), profile.getAbout(), profile.getAboutEmoji(), + null, profile.isUnrestrictedUnidentifiedAccess() ? Profile.UnidentifiedAccessMode.UNRESTRICTED : profile.getUnidentifiedAccess() != null diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java index d61a81b5..c6ba5c92 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java @@ -17,6 +17,8 @@ public class Profile { private final String aboutEmoji; + private final String avatarUrlPath; + private final UnidentifiedAccessMode unidentifiedAccessMode; private final Set capabilities; @@ -27,6 +29,7 @@ public class Profile { final String familyName, final String about, final String aboutEmoji, + final String avatarUrlPath, final UnidentifiedAccessMode unidentifiedAccessMode, final Set capabilities ) { @@ -35,6 +38,7 @@ public class Profile { this.familyName = familyName; this.about = about; this.aboutEmoji = aboutEmoji; + this.avatarUrlPath = avatarUrlPath; this.unidentifiedAccessMode = unidentifiedAccessMode; this.capabilities = capabilities; } @@ -45,6 +49,7 @@ public class Profile { familyName = builder.familyName; about = builder.about; aboutEmoji = builder.aboutEmoji; + avatarUrlPath = builder.avatarUrlPath; unidentifiedAccessMode = builder.unidentifiedAccessMode; capabilities = builder.capabilities; } @@ -60,6 +65,7 @@ public class Profile { builder.familyName = copy.getFamilyName(); builder.about = copy.getAbout(); builder.aboutEmoji = copy.getAboutEmoji(); + builder.avatarUrlPath = copy.getAvatarUrlPath(); builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode(); builder.capabilities = copy.getCapabilities(); return builder; @@ -107,6 +113,10 @@ public class Profile { return aboutEmoji; } + public String getAvatarUrlPath() { + return avatarUrlPath; + } + public UnidentifiedAccessMode getUnidentifiedAccessMode() { return unidentifiedAccessMode; } @@ -152,6 +162,7 @@ public class Profile { private String familyName; private String about; private String aboutEmoji; + private String avatarUrlPath; private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; private Set capabilities = Collections.emptySet(); private long lastUpdateTimestamp = 0; @@ -179,6 +190,11 @@ public class Profile { return this; } + public Builder withAvatarUrlPath(final String val) { + avatarUrlPath = val; + return this; + } + public Builder withUnidentifiedAccessMode(final UnidentifiedAccessMode val) { unidentifiedAccessMode = val; return this; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index bace6a6b..16302692 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -89,6 +89,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile r.profile.familyName, r.profile.about, r.profile.aboutEmoji, + r.profile.avatarUrlPath, Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode), r.profile.capabilities.stream() .map(Profile.Capability::valueOfOrNull) @@ -445,6 +446,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile recipient.getProfile().getFamilyName(), recipient.getProfile().getAbout(), recipient.getProfile().getAboutEmoji(), + recipient.getProfile().getAvatarUrlPath(), recipient.getProfile().getUnidentifiedAccessMode().name(), recipient.getProfile() .getCapabilities() @@ -558,6 +560,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile public String familyName; public String about; public String aboutEmoji; + public String avatarUrlPath; public String unidentifiedAccessMode; public Set capabilities; @@ -571,6 +574,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile final String familyName, final String about, final String aboutEmoji, + final String avatarUrlPath, final String unidentifiedAccessMode, final Set capabilities ) { @@ -579,6 +583,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile this.familyName = familyName; this.about = about; this.aboutEmoji = aboutEmoji; + this.avatarUrlPath = avatarUrlPath; this.unidentifiedAccessMode = unidentifiedAccessMode; this.capabilities = capabilities; } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java index 7ceb07f6..c1b1183c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java @@ -27,6 +27,7 @@ public class ProfileUtils { nameParts.second(), about, aboutEmoji, + encryptedProfile.getAvatar(), getUnidentifiedAccessMode(encryptedProfile, profileCipher), getCapabilities(encryptedProfile)); } catch (InvalidCiphertextException e) { From d4838bd646c736a5fe1d40b45aa12be239b01ed3 Mon Sep 17 00:00:00 2001 From: John Freed Date: Thu, 7 Oct 2021 07:46:15 +0200 Subject: [PATCH 0844/2005] implement DBus submitRateLimitChallenge method (#763) update documentation --- man/signal-cli-dbus.5.adoc | 8 +++++++- src/main/java/org/asamk/Signal.java | 4 ++++ .../java/org/asamk/signal/dbus/DbusSignalImpl.java | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index e7cd083f..594c2941 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -363,8 +363,14 @@ uploadStickerPack(stickerPackPath) -> url:: Exception: Failure -== Signals +submitRateLimitChallenge(challenge, captcha) -> <>:: +* challenge : The challenge token taken from the proof required error. +* captcha : The captcha token from the solved captcha on the Signal website.. +Can be used to lift some rate-limits by solving a captcha. +Exception: IOErrorException + +== Signalss SyncMessageReceived (timestamp, sender, destination, groupId,message, attachments):: The sync message is received when the user sends a message from a linked device. diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index b8800085..a7832714 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,5 +1,7 @@ package org.asamk; +import org.asamk.signal.commands.exceptions.IOErrorException; + import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.annotations.DBusProperty; import org.freedesktop.dbus.exceptions.DBusException; @@ -141,6 +143,8 @@ public interface Signal extends DBusInterface { String uploadStickerPack(String stickerPackPath) throws Error.Failure; + void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException; + class MessageReceived extends DBusSignal { private final long timestamp; diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index ab9c89b2..698ce7c1 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -1,7 +1,9 @@ package org.asamk.signal.dbus; import org.asamk.Signal; +import org.asamk.Signal.Error; import org.asamk.signal.BaseConfig; +import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.NotMasterDeviceException; @@ -81,6 +83,18 @@ public class DbusSignalImpl implements Signal { return m.getSelfNumber(); } + @Override + public void submitRateLimitChallenge(String challenge, String captchaString) throws IOErrorException { + final var captcha = captchaString == null ? null : captchaString.replace("signalcaptcha://", ""); + + try { + m.submitRateLimitRecaptchaChallenge(challenge, captcha); + } catch (IOException e) { + throw new IOErrorException("Submit challenge error: " + e.getMessage(), e); + } + + } + @Override public void addDevice(String uri) { try { From 7829a8d631c22a6c06f4ce176d8bccabd65f9de9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 3 Oct 2021 17:41:31 +0200 Subject: [PATCH 0845/2005] Fix device id type --- src/main/java/org/asamk/Signal.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index a7832714..1d8d04a3 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -262,7 +262,7 @@ public interface Signal extends DBusInterface { } } - @DBusProperty(name = "Id", type = Integer.class, access = DBusProperty.Access.READ) + @DBusProperty(name = "Id", type = Long.class, access = DBusProperty.Access.READ) @DBusProperty(name = "Name", type = String.class) @DBusProperty(name = "Created", type = String.class, access = DBusProperty.Access.READ) @DBusProperty(name = "LastSeen", type = String.class, access = DBusProperty.Access.READ) From c56a8df9b286eee786c7e81764c5394eced51295 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Oct 2021 20:51:33 +0200 Subject: [PATCH 0846/2005] Return struct instead of object path directly for dbus list devices --- src/main/java/org/asamk/Signal.java | 34 ++++++++- .../asamk/signal/dbus/DbusManagerImpl.java | 7 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 76 ++++++++++--------- 3 files changed, 77 insertions(+), 40 deletions(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 1d8d04a3..2f81c196 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -3,7 +3,9 @@ package org.asamk; import org.asamk.signal.commands.exceptions.IOErrorException; import org.freedesktop.dbus.DBusPath; +import org.freedesktop.dbus.Struct; import org.freedesktop.dbus.annotations.DBusProperty; +import org.freedesktop.dbus.annotations.Position; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; import org.freedesktop.dbus.interfaces.DBusInterface; @@ -104,7 +106,7 @@ public interface Signal extends DBusInterface { DBusPath getDevice(long deviceId); - List listDevices() throws Error.Failure; + List listDevices() throws Error.Failure; DBusPath getThisDevice(); @@ -262,6 +264,36 @@ public interface Signal extends DBusInterface { } } + class StructDevice extends Struct { + + @Position(0) + DBusPath objectPath; + + @Position(1) + Long id; + + @Position(2) + String name; + + public StructDevice(final DBusPath objectPath, final Long id, final String name) { + this.objectPath = objectPath; + this.id = id; + this.name = name; + } + + public DBusPath getObjectPath() { + return objectPath; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } + @DBusProperty(name = "Id", type = Long.class, access = DBusProperty.Access.READ) @DBusProperty(name = "Name", type = String.class) @DBusProperty(name = "Created", type = String.class, access = DBusProperty.Access.READ) diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 3124a5b0..53148c01 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -145,13 +145,14 @@ public class DbusManagerImpl implements Manager { @Override public List getLinkedDevices() throws IOException { final var thisDevice = signal.getThisDevice(); - return signal.listDevices().stream().map(devicePath -> { - final var device = getRemoteObject(devicePath, Signal.Device.class).GetAll("org.asamk.Signal.Device"); + return signal.listDevices().stream().map(d -> { + final var device = getRemoteObject(d.getObjectPath(), + Signal.Device.class).GetAll("org.asamk.Signal.Device"); return new Device((long) device.get("Id").getValue(), (String) device.get("Name").getValue(), (long) device.get("Created").getValue(), (long) device.get("LastSeen").getValue(), - thisDevice.equals(devicePath)); + thisDevice.equals(d.getObjectPath())); }).collect(Collectors.toList()); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 698ce7c1..d0e33a40 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -57,7 +57,7 @@ public class DbusSignalImpl implements Signal { private final String objectPath; private DBusPath thisDevice; - private final List devices = new ArrayList<>(); + private final List devices = new ArrayList<>(); public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) { this.m = m; @@ -115,41 +115,11 @@ public class DbusSignalImpl implements Signal { } @Override - public List listDevices() { + public List listDevices() { updateDevices(); return this.devices; } - private void updateDevices() { - List linkedDevices; - try { - linkedDevices = m.getLinkedDevices(); - } catch (IOException | Error.Failure e) { - throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); - } - - unExportDevices(); - - linkedDevices.forEach(d -> { - final var object = new DbusSignalDeviceImpl(d); - final var deviceObjectPath = object.getObjectPath(); - try { - connection.exportObject(object); - } catch (DBusException e) { - e.printStackTrace(); - } - if (d.isThisDevice()) { - thisDevice = new DBusPath(deviceObjectPath); - } - this.devices.add(new DBusPath(deviceObjectPath)); - }); - } - - private void unExportDevices() { - this.devices.stream().map(DBusPath::getPath).forEach(connection::unExportObject); - this.devices.clear(); - } - @Override public DBusPath getThisDevice() { updateDevices(); @@ -802,21 +772,55 @@ public class DbusSignalImpl implements Signal { return name.isEmpty() ? null : name; } + private String emptyIfNull(final String string) { + return string == null ? "" : string; + } + private static String getDeviceObjectPath(String basePath, long deviceId) { return basePath + "/Devices/" + deviceId; } + private void updateDevices() { + List linkedDevices; + try { + linkedDevices = m.getLinkedDevices(); + } catch (IOException e) { + throw new Error.Failure("Failed to get linked devices: " + e.getMessage()); + } + + unExportDevices(); + + linkedDevices.forEach(d -> { + final var object = new DbusSignalDeviceImpl(d); + final var deviceObjectPath = object.getObjectPath(); + try { + connection.exportObject(object); + } catch (DBusException e) { + e.printStackTrace(); + } + if (d.isThisDevice()) { + thisDevice = new DBusPath(deviceObjectPath); + } + this.devices.add(new StructDevice(new DBusPath(deviceObjectPath), d.getId(), emptyIfNull(d.getName()))); + }); + } + + private void unExportDevices() { + this.devices.stream() + .map(StructDevice::getObjectPath) + .map(DBusPath::getPath) + .forEach(connection::unExportObject); + this.devices.clear(); + } + public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device { private final org.asamk.signal.manager.api.Device device; public DbusSignalDeviceImpl(final org.asamk.signal.manager.api.Device device) { - super(); super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Device", List.of(new DbusProperty<>("Id", device::getId), - new DbusProperty<>("Name", - () -> device.getName() == null ? "" : device.getName(), - this::setDeviceName), + new DbusProperty<>("Name", () -> emptyIfNull(device.getName()), this::setDeviceName), new DbusProperty<>("Created", device::getCreated), new DbusProperty<>("LastSeen", device::getLastSeen)))); this.device = device; From 179855272a9eab56bfd8514ec71e684b541feb88 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Oct 2021 20:52:10 +0200 Subject: [PATCH 0847/2005] Fix dbus properties GetAll method for variants --- src/main/java/org/asamk/signal/dbus/DbusProperties.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/dbus/DbusProperties.java b/src/main/java/org/asamk/signal/dbus/DbusProperties.java index 37cc35e3..bbe01d6b 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusProperties.java +++ b/src/main/java/org/asamk/signal/dbus/DbusProperties.java @@ -51,6 +51,7 @@ public abstract class DbusProperties implements Properties { } @Override + @SuppressWarnings("unchecked") public Map> GetAll(final String interface_name) { final var handler = getHandlerOptional(interface_name); if (handler.isEmpty()) { @@ -61,6 +62,9 @@ public abstract class DbusProperties implements Properties { .getProperties() .stream() .filter(p -> p.getGetter() != null) - .collect(Collectors.toMap(DbusProperty::getName, p -> new Variant<>(p.getGetter().get()))); + .collect(Collectors.toMap(DbusProperty::getName, p -> { + final Object o = p.getGetter().get(); + return o instanceof Variant ? (Variant) o : new Variant<>(o); + })); } } From cadcc6c8ef13b679d6a542b0712aad202d4a64a7 Mon Sep 17 00:00:00 2001 From: John Freed Date: Sat, 9 Oct 2021 13:04:07 +0200 Subject: [PATCH 0848/2005] update docs for DBus listDevices method (#768) --- man/signal-cli-dbus.5.adoc | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 594c2941..8168c421 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -29,15 +29,18 @@ method(arg1, arg2, ...) -> return Where is according to DBus specification: -* : String -* : Byte Array -* : Array of Byte Arrays -* : String Array -* : Array of signed 64 bit integer -* : Boolean (0|1) -* : Signed 64 bit integer +* : Array of ... (comma-separated list) +* (...) : Struct (cannot be sent via `dbus-send`) +* : Boolean (false|true) (boolean:) +* : Signed 32-bit (int) integer (int32:) +* : DBusPath object (objpath:) +* : String (string:) +* : Signed 64-bit (long) integer (int64:) +* : Unsigned 8-bit (byte) integer (byte:) * <> : no return value +The final parenthetical value (such as "boolean:") is the type indicator used by `dbus-send`. + Exceptions are the names of the Java Exceptions returned in the body field. They typically contain an additional message with details. All Exceptions begin with "org.asamk.Signal.Error." which is omitted here for better readability. Phone numbers always have the format + @@ -340,8 +343,11 @@ addDevice(deviceUri) -> <>:: Exception: InvalidUri -listDevices() -> devices:: -* devices : String array of linked devices +listDevices() -> devices:: +* devices : Array of structs (objectPath, id, name) +** objectPath : DBusPath representing the device's object path +** id : Long representing the deviceId +** name : String representing the device's name Exception: Failure @@ -370,7 +376,7 @@ Can be used to lift some rate-limits by solving a captcha. Exception: IOErrorException -== Signalss +== Signals SyncMessageReceived (timestamp, sender, destination, groupId,message, attachments):: The sync message is received when the user sends a message from a linked device. From b5d4a5000b1c28ca7674de5f313c876c0f29a763 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 9 Oct 2021 17:04:01 +0200 Subject: [PATCH 0849/2005] Add DeviceNotFound Error --- src/main/java/org/asamk/Signal.java | 7 +++++++ src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index 2f81c196..bf8265ff 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -326,6 +326,13 @@ public interface Signal extends DBusInterface { } } + class DeviceNotFound extends DBusExecutionException { + + public DeviceNotFound(final String message) { + super(message); + } + } + class GroupNotFound extends DBusExecutionException { public GroupNotFound(final String message) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index d0e33a40..ab19f0ce 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -111,7 +111,11 @@ public class DbusSignalImpl implements Signal { @Override public DBusPath getDevice(long deviceId) { updateDevices(); - return new DBusPath(getDeviceObjectPath(objectPath, deviceId)); + final var deviceOptional = devices.stream().filter(g -> g.getId().equals(deviceId)).findFirst(); + if (deviceOptional.isEmpty()) { + throw new Error.DeviceNotFound("Device not found"); + } + return deviceOptional.get().getObjectPath(); } @Override From 997b4f0c3fe371efe08e92ed4678835bc62538bf Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Oct 2021 21:18:14 +0200 Subject: [PATCH 0850/2005] Implement new dbus group interface --- .../org/asamk/signal/manager/Manager.java | 18 +- .../org/asamk/signal/manager/ManagerImpl.java | 53 ++-- .../org/asamk/signal/manager/api/Group.java | 45 +++- .../asamk/signal/manager/api/UpdateGroup.java | 203 ++++++++++++++ .../signal/manager/helper/GroupHelper.java | 4 +- .../signal/manager/helper/SendHelper.java | 2 +- .../manager/storage/groups/GroupInfo.java | 9 +- .../manager/storage/groups/GroupInfoV1.java | 18 +- .../manager/storage/groups/GroupInfoV2.java | 38 ++- src/main/java/org/asamk/Signal.java | 89 ++++++- .../signal/commands/ListGroupsCommand.java | 16 +- .../signal/commands/UpdateGroupCommand.java | 33 +-- .../asamk/signal/dbus/DbusManagerImpl.java | 154 ++++++++--- .../org/asamk/signal/dbus/DbusProperty.java | 6 + .../org/asamk/signal/dbus/DbusSignalImpl.java | 251 ++++++++++++++++-- 15 files changed, 803 insertions(+), 136 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 7a421966..f529b408 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -8,13 +8,12 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; -import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; -import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -138,20 +137,7 @@ public interface Manager extends Closeable { ) throws IOException, AttachmentInvalidException; SendGroupMessageResults updateGroup( - GroupId groupId, - String name, - String description, - Set members, - Set removeMembers, - Set admins, - Set removeAdmins, - boolean resetGroupLink, - GroupLinkState groupLinkState, - GroupPermission addMemberPermission, - GroupPermission editDetailsPermission, - File avatarFile, - Integer expirationTimer, - Boolean isAnnouncementGroup + final GroupId groupId, final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException; Pair joinGroup( diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 0fd1eb33..8ccab36b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -25,13 +25,12 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; -import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; -import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -505,9 +504,12 @@ public class ManagerImpl implements Manager { .map(account.getRecipientStore()::resolveRecipientAddress) .collect(Collectors.toSet()), groupInfo.isBlocked(), - groupInfo.getMessageExpirationTime(), - groupInfo.isAnnouncementGroup(), - groupInfo.isMember(account.getSelfRecipientId())); + groupInfo.getMessageExpirationTimer(), + groupInfo.getPermissionAddMember(), + groupInfo.getPermissionEditDetails(), + groupInfo.getPermissionSendMessage(), + groupInfo.isMember(account.getSelfRecipientId()), + groupInfo.isAdmin(account.getSelfRecipientId())); } @Override @@ -532,35 +534,22 @@ public class ManagerImpl implements Manager { @Override public SendGroupMessageResults updateGroup( - GroupId groupId, - String name, - String description, - Set members, - Set removeMembers, - Set admins, - Set removeAdmins, - boolean resetGroupLink, - GroupLinkState groupLinkState, - GroupPermission addMemberPermission, - GroupPermission editDetailsPermission, - File avatarFile, - Integer expirationTimer, - Boolean isAnnouncementGroup + final GroupId groupId, final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { return groupHelper.updateGroup(groupId, - name, - description, - members == null ? null : resolveRecipients(members), - removeMembers == null ? null : resolveRecipients(removeMembers), - admins == null ? null : resolveRecipients(admins), - removeAdmins == null ? null : resolveRecipients(removeAdmins), - resetGroupLink, - groupLinkState, - addMemberPermission, - editDetailsPermission, - avatarFile, - expirationTimer, - isAnnouncementGroup); + updateGroup.getName(), + updateGroup.getDescription(), + updateGroup.getMembers() == null ? null : resolveRecipients(updateGroup.getMembers()), + updateGroup.getRemoveMembers() == null ? null : resolveRecipients(updateGroup.getRemoveMembers()), + updateGroup.getAdmins() == null ? null : resolveRecipients(updateGroup.getAdmins()), + updateGroup.getRemoveAdmins() == null ? null : resolveRecipients(updateGroup.getRemoveAdmins()), + updateGroup.isResetGroupLink(), + updateGroup.getGroupLinkState(), + updateGroup.getAddMemberPermission(), + updateGroup.getEditDetailsPermission(), + updateGroup.getAvatarFile(), + updateGroup.getExpirationTimer(), + updateGroup.getIsAnnouncementGroup()); } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/api/Group.java b/lib/src/main/java/org/asamk/signal/manager/api/Group.java index 650e10b6..4787ef95 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/Group.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/Group.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager.api; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import java.util.Set; @@ -17,9 +18,13 @@ public class Group { private final Set requestingMembers; private final Set adminMembers; private final boolean isBlocked; - private final int messageExpirationTime; - private final boolean isAnnouncementGroup; + private final int messageExpirationTimer; + + private final GroupPermission permissionAddMember; + private final GroupPermission permissionEditDetails; + private final GroupPermission permissionSendMessage; private final boolean isMember; + private final boolean isAdmin; public Group( final GroupId groupId, @@ -31,9 +36,12 @@ public class Group { final Set requestingMembers, final Set adminMembers, final boolean isBlocked, - final int messageExpirationTime, - final boolean isAnnouncementGroup, - final boolean isMember + final int messageExpirationTimer, + final GroupPermission permissionAddMember, + final GroupPermission permissionEditDetails, + final GroupPermission permissionSendMessage, + final boolean isMember, + final boolean isAdmin ) { this.groupId = groupId; this.title = title; @@ -44,9 +52,12 @@ public class Group { this.requestingMembers = requestingMembers; this.adminMembers = adminMembers; this.isBlocked = isBlocked; - this.messageExpirationTime = messageExpirationTime; - this.isAnnouncementGroup = isAnnouncementGroup; + this.messageExpirationTimer = messageExpirationTimer; + this.permissionAddMember = permissionAddMember; + this.permissionEditDetails = permissionEditDetails; + this.permissionSendMessage = permissionSendMessage; this.isMember = isMember; + this.isAdmin = isAdmin; } public GroupId getGroupId() { @@ -85,15 +96,27 @@ public class Group { return isBlocked; } - public int getMessageExpirationTime() { - return messageExpirationTime; + public int getMessageExpirationTimer() { + return messageExpirationTimer; } - public boolean isAnnouncementGroup() { - return isAnnouncementGroup; + public GroupPermission getPermissionAddMember() { + return permissionAddMember; + } + + public GroupPermission getPermissionEditDetails() { + return permissionEditDetails; + } + + public GroupPermission getPermissionSendMessage() { + return permissionSendMessage; } public boolean isMember() { return isMember; } + + public boolean isAdmin() { + return isAdmin; + } } diff --git a/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java b/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java new file mode 100644 index 00000000..b5877ae5 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/api/UpdateGroup.java @@ -0,0 +1,203 @@ +package org.asamk.signal.manager.api; + +import org.asamk.signal.manager.groups.GroupLinkState; +import org.asamk.signal.manager.groups.GroupPermission; + +import java.io.File; +import java.util.Set; + +public class UpdateGroup { + + private final String name; + private final String description; + private final Set members; + private final Set removeMembers; + private final Set admins; + private final Set removeAdmins; + private final boolean resetGroupLink; + private final GroupLinkState groupLinkState; + private final GroupPermission addMemberPermission; + private final GroupPermission editDetailsPermission; + private final File avatarFile; + private final Integer expirationTimer; + private final Boolean isAnnouncementGroup; + + private UpdateGroup(final Builder builder) { + name = builder.name; + description = builder.description; + members = builder.members; + removeMembers = builder.removeMembers; + admins = builder.admins; + removeAdmins = builder.removeAdmins; + resetGroupLink = builder.resetGroupLink; + groupLinkState = builder.groupLinkState; + addMemberPermission = builder.addMemberPermission; + editDetailsPermission = builder.editDetailsPermission; + avatarFile = builder.avatarFile; + expirationTimer = builder.expirationTimer; + isAnnouncementGroup = builder.isAnnouncementGroup; + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static Builder newBuilder(final UpdateGroup copy) { + Builder builder = new Builder(); + builder.name = copy.getName(); + builder.description = copy.getDescription(); + builder.members = copy.getMembers(); + builder.removeMembers = copy.getRemoveMembers(); + builder.admins = copy.getAdmins(); + builder.removeAdmins = copy.getRemoveAdmins(); + builder.resetGroupLink = copy.isResetGroupLink(); + builder.groupLinkState = copy.getGroupLinkState(); + builder.addMemberPermission = copy.getAddMemberPermission(); + builder.editDetailsPermission = copy.getEditDetailsPermission(); + builder.avatarFile = copy.getAvatarFile(); + builder.expirationTimer = copy.getExpirationTimer(); + builder.isAnnouncementGroup = copy.getIsAnnouncementGroup(); + return builder; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Set getMembers() { + return members; + } + + public Set getRemoveMembers() { + return removeMembers; + } + + public Set getAdmins() { + return admins; + } + + public Set getRemoveAdmins() { + return removeAdmins; + } + + public boolean isResetGroupLink() { + return resetGroupLink; + } + + public GroupLinkState getGroupLinkState() { + return groupLinkState; + } + + public GroupPermission getAddMemberPermission() { + return addMemberPermission; + } + + public GroupPermission getEditDetailsPermission() { + return editDetailsPermission; + } + + public File getAvatarFile() { + return avatarFile; + } + + public Integer getExpirationTimer() { + return expirationTimer; + } + + public Boolean getIsAnnouncementGroup() { + return isAnnouncementGroup; + } + + public static final class Builder { + + private String name; + private String description; + private Set members; + private Set removeMembers; + private Set admins; + private Set removeAdmins; + private boolean resetGroupLink; + private GroupLinkState groupLinkState; + private GroupPermission addMemberPermission; + private GroupPermission editDetailsPermission; + private File avatarFile; + private Integer expirationTimer; + private Boolean isAnnouncementGroup; + + private Builder() { + } + + public Builder withName(final String val) { + name = val; + return this; + } + + public Builder withDescription(final String val) { + description = val; + return this; + } + + public Builder withMembers(final Set val) { + members = val; + return this; + } + + public Builder withRemoveMembers(final Set val) { + removeMembers = val; + return this; + } + + public Builder withAdmins(final Set val) { + admins = val; + return this; + } + + public Builder withRemoveAdmins(final Set val) { + removeAdmins = val; + return this; + } + + public Builder withResetGroupLink(final boolean val) { + resetGroupLink = val; + return this; + } + + public Builder withGroupLinkState(final GroupLinkState val) { + groupLinkState = val; + return this; + } + + public Builder withAddMemberPermission(final GroupPermission val) { + addMemberPermission = val; + return this; + } + + public Builder withEditDetailsPermission(final GroupPermission val) { + editDetailsPermission = val; + return this; + } + + public Builder withAvatarFile(final File val) { + avatarFile = val; + return this; + } + + public Builder withExpirationTimer(final Integer val) { + expirationTimer = val; + return this; + } + + public Builder withIsAnnouncementGroup(final Boolean val) { + isAnnouncementGroup = val; + return this; + } + + public UpdateGroup build() { + return new UpdateGroup(this); + } + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index 62f4f111..ee2e9416 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -639,7 +639,7 @@ public class GroupHelper { return SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + .withExpiration(g.getMessageExpirationTimer()); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { @@ -648,7 +648,7 @@ public class GroupHelper { .withSignedGroupChange(signedGroupChange); return SignalServiceDataMessage.newBuilder() .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + .withExpiration(g.getMessageExpirationTimer()); } private SendGroupMessageResults sendUpdateGroupV2Message( diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index c0953f1f..6c0fb2e9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -100,7 +100,7 @@ public class SendHelper { final SignalServiceDataMessage.Builder messageBuilder, final GroupInfo g ) throws IOException, GroupSendingNotAllowedException { GroupUtils.setGroupContext(messageBuilder, g); - messageBuilder.withExpiration(g.getMessageExpirationTime()); + messageBuilder.withExpiration(g.getMessageExpirationTimer()); final var message = messageBuilder.build(); final var recipients = g.getMembersWithout(account.getSelfRecipientId()); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index 60efc84b..b4f4e63a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.groups; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.storage.recipients.RecipientId; import java.util.Set; @@ -38,10 +39,16 @@ public abstract class GroupInfo { public abstract void setBlocked(boolean blocked); - public abstract int getMessageExpirationTime(); + public abstract int getMessageExpirationTimer(); public abstract boolean isAnnouncementGroup(); + public abstract GroupPermission getPermissionAddMember(); + + public abstract GroupPermission getPermissionEditDetails(); + + public abstract GroupPermission getPermissionSendMessage(); + public Set getMembersWithout(RecipientId recipientId) { return getMembers().stream().filter(member -> !member.equals(recipientId)).collect(Collectors.toSet()); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index 49c9a504..dbd2dcbb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -3,6 +3,7 @@ package org.asamk.signal.manager.storage.groups; import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.storage.recipients.RecipientId; @@ -85,7 +86,7 @@ public class GroupInfoV1 extends GroupInfo { } @Override - public int getMessageExpirationTime() { + public int getMessageExpirationTimer() { return messageExpirationTime; } @@ -94,6 +95,21 @@ public class GroupInfoV1 extends GroupInfo { return false; } + @Override + public GroupPermission getPermissionAddMember() { + return GroupPermission.EVERY_MEMBER; + } + + @Override + public GroupPermission getPermissionEditDetails() { + return GroupPermission.EVERY_MEMBER; + } + + @Override + public GroupPermission getPermissionSendMessage() { + return GroupPermission.EVERY_MEMBER; + } + public void addMembers(Collection members) { this.members.addAll(members); } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index a06b83df..34db2a28 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.groups; import org.asamk.signal.manager.groups.GroupIdV2; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.signal.storageservice.protos.groups.AccessControl; @@ -151,7 +152,7 @@ public class GroupInfoV2 extends GroupInfo { } @Override - public int getMessageExpirationTime() { + public int getMessageExpirationTimer() { return this.group != null && this.group.hasDisappearingMessagesTimer() ? this.group.getDisappearingMessagesTimer().getDuration() : 0; @@ -162,6 +163,23 @@ public class GroupInfoV2 extends GroupInfo { return this.group != null && this.group.getIsAnnouncementGroup() == EnabledState.ENABLED; } + @Override + public GroupPermission getPermissionAddMember() { + final var accessControl = getAccessControl(); + return accessControl == null ? GroupPermission.EVERY_MEMBER : toGroupPermission(accessControl.getMembers()); + } + + @Override + public GroupPermission getPermissionEditDetails() { + final var accessControl = getAccessControl(); + return accessControl == null ? GroupPermission.EVERY_MEMBER : toGroupPermission(accessControl.getAttributes()); + } + + @Override + public GroupPermission getPermissionSendMessage() { + return isAnnouncementGroup() ? GroupPermission.ONLY_ADMINS : GroupPermission.EVERY_MEMBER; + } + public void setPermissionDenied(final boolean permissionDenied) { this.permissionDenied = permissionDenied; } @@ -169,4 +187,22 @@ public class GroupInfoV2 extends GroupInfo { public boolean isPermissionDenied() { return permissionDenied; } + + private AccessControl getAccessControl() { + if (this.group == null || !this.group.hasAccessControl()) { + return null; + } + + return this.group.getAccessControl(); + } + + private static GroupPermission toGroupPermission(final AccessControl.AccessRequired permission) { + switch (permission) { + case ADMINISTRATOR: + return GroupPermission.ONLY_ADMINS; + case MEMBER: + default: + return GroupPermission.EVERY_MEMBER; + } + } } diff --git a/src/main/java/org/asamk/Signal.java b/src/main/java/org/asamk/Signal.java index bf8265ff..349671b3 100644 --- a/src/main/java/org/asamk/Signal.java +++ b/src/main/java/org/asamk/Signal.java @@ -1,7 +1,6 @@ package org.asamk; import org.asamk.signal.commands.exceptions.IOErrorException; - import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.Struct; import org.freedesktop.dbus.annotations.DBusProperty; @@ -84,14 +83,27 @@ public interface Signal extends DBusInterface { void setContactBlocked(String number, boolean blocked) throws Error.InvalidNumber; + @Deprecated void setGroupBlocked(byte[] groupId, boolean blocked) throws Error.GroupNotFound, Error.InvalidGroupId; + @Deprecated List getGroupIds(); + DBusPath getGroup(byte[] groupId); + + List listGroups(); + + @Deprecated String getGroupName(byte[] groupId) throws Error.InvalidGroupId; + @Deprecated List getGroupMembers(byte[] groupId) throws Error.InvalidGroupId; + byte[] createGroup( + String name, List members, String avatar + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber; + + @Deprecated byte[] updateGroup( byte[] groupId, String name, List members, String avatar ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber, Error.GroupNotFound, Error.InvalidGroupId; @@ -133,12 +145,15 @@ public interface Signal extends DBusInterface { List getContactNumber(final String name) throws Error.Failure; + @Deprecated void quitGroup(final byte[] groupId) throws Error.GroupNotFound, Error.Failure, Error.InvalidGroupId; boolean isContactBlocked(final String number) throws Error.InvalidNumber; + @Deprecated boolean isGroupBlocked(final byte[] groupId) throws Error.InvalidGroupId; + @Deprecated boolean isMember(final byte[] groupId) throws Error.InvalidGroupId; byte[] joinGroup(final String groupLink) throws Error.Failure; @@ -303,6 +318,71 @@ public interface Signal extends DBusInterface { void removeDevice() throws Error.Failure; } + class StructGroup extends Struct { + + @Position(0) + DBusPath objectPath; + + @Position(1) + byte[] id; + + @Position(2) + String name; + + public StructGroup(final DBusPath objectPath, final byte[] id, final String name) { + this.objectPath = objectPath; + this.id = id; + this.name = name; + } + + public DBusPath getObjectPath() { + return objectPath; + } + + public byte[] getId() { + return id; + } + + public String getName() { + return name; + } + } + + @DBusProperty(name = "Id", type = Byte[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "Name", type = String.class) + @DBusProperty(name = "Description", type = String.class) + @DBusProperty(name = "Avatar", type = String.class, access = DBusProperty.Access.WRITE) + @DBusProperty(name = "IsBlocked", type = Boolean.class) + @DBusProperty(name = "IsMember", type = Boolean.class, access = DBusProperty.Access.READ) + @DBusProperty(name = "IsAdmin", type = Boolean.class, access = DBusProperty.Access.READ) + @DBusProperty(name = "MessageExpirationTimer", type = Integer.class) + @DBusProperty(name = "Members", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "PendingMembers", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "RequestingMembers", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "Admins", type = String[].class, access = DBusProperty.Access.READ) + @DBusProperty(name = "PermissionAddMember", type = String.class) + @DBusProperty(name = "PermissionEditDetails", type = String.class) + @DBusProperty(name = "PermissionSendMessage", type = String.class) + @DBusProperty(name = "GroupInviteLink", type = String.class, access = DBusProperty.Access.READ) + interface Group extends DBusInterface, Properties { + + void quitGroup() throws Error.Failure, Error.LastGroupAdmin; + + void addMembers(List recipients) throws Error.Failure; + + void removeMembers(List recipients) throws Error.Failure; + + void addAdmins(List recipients) throws Error.Failure; + + void removeAdmins(List recipients) throws Error.Failure; + + void resetLink() throws Error.Failure; + + void disableLink() throws Error.Failure; + + void enableLink(boolean requiresApproval) throws Error.Failure; + } + interface Error { class AttachmentInvalid extends DBusExecutionException { @@ -347,6 +427,13 @@ public interface Signal extends DBusInterface { } } + class LastGroupAdmin extends DBusExecutionException { + + public LastGroupAdmin(final String message) { + super(message); + } + } + class InvalidNumber extends DBusExecutionException { public InvalidNumber(final String message) { diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index fd8c4b92..b2182429 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -63,7 +63,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { resolveMembers(group.getPendingMembers()), resolveMembers(group.getRequestingMembers()), resolveMembers(group.getAdminMembers()), - group.getMessageExpirationTime() == 0 ? "disabled" : group.getMessageExpirationTime() + "s", + group.getMessageExpirationTimer() == 0 ? "disabled" : group.getMessageExpirationTimer() + "s", groupInviteLink == null ? '-' : groupInviteLink.getUrl()); } else { writer.println("Id: {} Name: {} Active: {} Blocked: {}", @@ -91,11 +91,14 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { group.getDescription(), group.isMember(), group.isBlocked(), - group.getMessageExpirationTime(), + group.getMessageExpirationTimer(), resolveJsonMembers(group.getMembers()), resolveJsonMembers(group.getPendingMembers()), resolveJsonMembers(group.getRequestingMembers()), resolveJsonMembers(group.getAdminMembers()), + group.getPermissionAddMember().name(), + group.getPermissionEditDetails().name(), + group.getPermissionSendMessage().name(), groupInviteLink == null ? null : groupInviteLink.getUrl()); }).collect(Collectors.toList()); @@ -122,6 +125,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { public final Set pendingMembers; public final Set requestingMembers; public final Set admins; + public final String permissionAddMember; + public final String permissionEditDetails; + public final String permissionSendMessage; public final String groupInviteLink; public JsonGroup( @@ -135,6 +141,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { Set pendingMembers, Set requestingMembers, Set admins, + final String permissionAddMember, + final String permissionEditDetails, + final String permissionSendMessage, String groupInviteLink ) { this.id = id; @@ -148,6 +157,9 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { this.pendingMembers = pendingMembers; this.requestingMembers = requestingMembers; this.admins = admins; + this.permissionAddMember = permissionAddMember; + this.permissionEditDetails = permissionEditDetails; + this.permissionSendMessage = permissionSendMessage; this.groupInviteLink = groupInviteLink; } } diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index 68bce2d2..b63a7160 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -12,6 +12,7 @@ import org.asamk.signal.commands.exceptions.UnexpectedErrorException; import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.manager.AttachmentInvalidException; import org.asamk.signal.manager.Manager; +import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; @@ -145,21 +146,23 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand { } var results = m.updateGroup(groupId, - groupName, - groupDescription, - groupMembers, - groupRemoveMembers, - groupAdmins, - groupRemoveAdmins, - groupResetLink, - groupLinkState, - groupAddMemberPermission, - groupEditDetailsPermission, - groupAvatar == null ? null : new File(groupAvatar), - groupExpiration, - groupSendMessagesPermission == null - ? null - : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS); + UpdateGroup.newBuilder() + .withName(groupName) + .withDescription(groupDescription) + .withMembers(groupMembers) + .withRemoveMembers(groupRemoveMembers) + .withAdmins(groupAdmins) + .withRemoveAdmins(groupRemoveAdmins) + .withResetGroupLink(groupResetLink) + .withGroupLinkState(groupLinkState) + .withAddMemberPermission(groupAddMemberPermission) + .withEditDetailsPermission(groupEditDetailsPermission) + .withAvatarFile(groupAvatar == null ? null : new File(groupAvatar)) + .withExpirationTimer(groupExpiration) + .withIsAnnouncementGroup(groupSendMessagesPermission == null + ? null + : groupSendMessagesPermission == GroupPermission.ONLY_ADMINS) + .build()); if (results != null) { timestamp = results.getTimestamp(); ErrorUtils.handleSendMessageResults(results.getResults()); diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 53148c01..6e655bdc 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -15,9 +15,9 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; -import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; @@ -183,8 +183,8 @@ public class DbusManagerImpl implements Manager { @Override public List getGroups() { - final var groupIds = signal.getGroupIds(); - return groupIds.stream().map(id -> getGroup(GroupId.unknownVersion(id))).collect(Collectors.toList()); + final var groups = signal.listGroups(); + return groups.stream().map(Signal.StructGroup::getObjectPath).map(this::getGroup).collect(Collectors.toList()); } @Override @@ -194,7 +194,8 @@ public class DbusManagerImpl implements Manager { if (groupAdmins.size() > 0) { throw new UnsupportedOperationException(); } - signal.quitGroup(groupId.serialize()); + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + group.quitGroup(); return new SendGroupMessageResults(0, List.of()); } @@ -207,8 +208,7 @@ public class DbusManagerImpl implements Manager { public Pair createGroup( final String name, final Set members, final File avatarFile ) throws IOException, AttachmentInvalidException { - final var newGroupId = signal.updateGroup(new byte[0], - emptyIfNull(name), + final var newGroupId = signal.createGroup(emptyIfNull(name), members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), avatarFile == null ? "" : avatarFile.getPath()); return new Pair<>(GroupId.unknownVersion(newGroupId), new SendGroupMessageResults(0, List.of())); @@ -216,25 +216,76 @@ public class DbusManagerImpl implements Manager { @Override public SendGroupMessageResults updateGroup( - final GroupId groupId, - final String name, - final String description, - final Set members, - final Set removeMembers, - final Set admins, - final Set removeAdmins, - final boolean resetGroupLink, - final GroupLinkState groupLinkState, - final GroupPermission addMemberPermission, - final GroupPermission editDetailsPermission, - final File avatarFile, - final Integer expirationTimer, - final Boolean isAnnouncementGroup + final GroupId groupId, final UpdateGroup updateGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { - signal.updateGroup(groupId.serialize(), - emptyIfNull(name), - members.stream().map(RecipientIdentifier.Single::getIdentifier).collect(Collectors.toList()), - avatarFile == null ? "" : avatarFile.getPath()); + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + if (updateGroup.getName() != null) { + group.Set("org.asamk.Signal.Group", "Name", updateGroup.getName()); + } + if (updateGroup.getDescription() != null) { + group.Set("org.asamk.Signal.Group", "Description", updateGroup.getDescription()); + } + if (updateGroup.getAvatarFile() != null) { + group.Set("org.asamk.Signal.Group", + "Avatar", + updateGroup.getAvatarFile() == null ? "" : updateGroup.getAvatarFile().getPath()); + } + if (updateGroup.getExpirationTimer() != null) { + group.Set("org.asamk.Signal.Group", "MessageExpirationTimer", updateGroup.getExpirationTimer()); + } + if (updateGroup.getAddMemberPermission() != null) { + group.Set("org.asamk.Signal.Group", "PermissionAddMember", updateGroup.getAddMemberPermission().name()); + } + if (updateGroup.getEditDetailsPermission() != null) { + group.Set("org.asamk.Signal.Group", "PermissionEditDetails", updateGroup.getEditDetailsPermission().name()); + } + if (updateGroup.getIsAnnouncementGroup() != null) { + group.Set("org.asamk.Signal.Group", + "PermissionSendMessage", + updateGroup.getIsAnnouncementGroup() + ? GroupPermission.ONLY_ADMINS.name() + : GroupPermission.EVERY_MEMBER.name()); + } + if (updateGroup.getMembers() != null) { + group.addMembers(updateGroup.getMembers() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getRemoveMembers() != null) { + group.removeMembers(updateGroup.getRemoveMembers() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getAdmins() != null) { + group.addAdmins(updateGroup.getAdmins() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.getRemoveAdmins() != null) { + group.removeAdmins(updateGroup.getRemoveAdmins() + .stream() + .map(RecipientIdentifier.Single::getIdentifier) + .collect(Collectors.toList())); + } + if (updateGroup.isResetGroupLink()) { + group.resetLink(); + } + if (updateGroup.getGroupLinkState() != null) { + switch (updateGroup.getGroupLinkState()) { + case DISABLED: + group.disableLink(); + break; + case ENABLED: + group.enableLink(false); + break; + case ENABLED_WITH_APPROVAL: + group.enableLink(true); + break; + } + } return new SendGroupMessageResults(0, List.of()); } @@ -344,7 +395,12 @@ public class DbusManagerImpl implements Manager { public void setGroupBlocked( final GroupId groupId, final boolean blocked ) throws GroupNotFoundException, IOException { - signal.setGroupBlocked(groupId.serialize(), blocked); + setGroupProperty(groupId, "IsBlocked", blocked); + } + + private void setGroupProperty(final GroupId groupId, final String propertyName, final boolean blocked) { + final var group = getRemoteObject(signal.getGroup(groupId.serialize()), Signal.Group.class); + group.Set("org.asamk.Signal.Group", propertyName, blocked); } @Override @@ -411,19 +467,41 @@ public class DbusManagerImpl implements Manager { @Override public Group getGroup(final GroupId groupId) { - final var id = groupId.serialize(); - return new Group(groupId, - signal.getGroupName(id), - null, - null, - signal.getGroupMembers(id).stream().map(m -> new RecipientAddress(null, m)).collect(Collectors.toSet()), - Set.of(), - Set.of(), - Set.of(), - signal.isGroupBlocked(id), - 0, - false, - signal.isMember(id)); + final var groupPath = signal.getGroup(groupId.serialize()); + return getGroup(groupPath); + } + + @SuppressWarnings("unchecked") + private Group getGroup(final DBusPath groupPath) { + final var group = getRemoteObject(groupPath, Signal.Group.class).GetAll("org.asamk.Signal.Group"); + final var id = (byte[]) group.get("Id").getValue(); + try { + return new Group(GroupId.unknownVersion(id), + (String) group.get("Name").getValue(), + (String) group.get("Description").getValue(), + GroupInviteLinkUrl.fromUri((String) group.get("GroupInviteLink").getValue()), + ((List) group.get("Members").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("PendingMembers").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("RequestingMembers").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + ((List) group.get("Admins").getValue()).stream() + .map(m -> new RecipientAddress(null, m)) + .collect(Collectors.toSet()), + (boolean) group.get("IsBlocked").getValue(), + (int) group.get("MessageExpirationTimer").getValue(), + GroupPermission.valueOf((String) group.get("PermissionAddMember").getValue()), + GroupPermission.valueOf((String) group.get("PermissionEditDetails").getValue()), + GroupPermission.valueOf((String) group.get("PermissionSendMessage").getValue()), + (boolean) group.get("IsMember").getValue(), + (boolean) group.get("IsAdmin").getValue()); + } catch (GroupInviteLinkUrl.InvalidGroupLinkException | GroupInviteLinkUrl.UnknownGroupLinkVersionException e) { + throw new AssertionError(e); + } } @Override diff --git a/src/main/java/org/asamk/signal/dbus/DbusProperty.java b/src/main/java/org/asamk/signal/dbus/DbusProperty.java index e0557786..5042458e 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusProperty.java +++ b/src/main/java/org/asamk/signal/dbus/DbusProperty.java @@ -21,6 +21,12 @@ public class DbusProperty { this.setter = null; } + public DbusProperty(final String name, final Consumer setter) { + this.name = name; + this.getter = null; + this.setter = setter; + } + public String getName() { return name; } diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index ab19f0ce..56ecdc55 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -1,7 +1,6 @@ package org.asamk.signal.dbus; import org.asamk.Signal; -import org.asamk.Signal.Error; import org.asamk.signal.BaseConfig; import org.asamk.signal.commands.exceptions.IOErrorException; import org.asamk.signal.manager.AttachmentInvalidException; @@ -13,9 +12,12 @@ import org.asamk.signal.manager.api.Identity; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.TypingAction; +import org.asamk.signal.manager.api.UpdateGroup; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; @@ -26,6 +28,7 @@ import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.freedesktop.dbus.types.Variant; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -40,6 +43,8 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -58,6 +63,7 @@ public class DbusSignalImpl implements Signal { private DBusPath thisDevice; private final List devices = new ArrayList<>(); + private final List groups = new ArrayList<>(); public DbusSignalImpl(final Manager m, DBusConnection connection, final String objectPath) { this.m = m; @@ -67,6 +73,7 @@ public class DbusSignalImpl implements Signal { public void initObjects() { updateDevices(); + updateGroups(); } public void close() { @@ -415,6 +422,22 @@ public class DbusSignalImpl implements Signal { return ids; } + @Override + public DBusPath getGroup(final byte[] groupId) { + updateGroups(); + final var groupOptional = groups.stream().filter(g -> Arrays.equals(g.getId(), groupId)).findFirst(); + if (groupOptional.isEmpty()) { + throw new Error.GroupNotFound("Group not found"); + } + return groupOptional.get().getObjectPath(); + } + + @Override + public List listGroups() { + updateGroups(); + return groups; + } + @Override public String getGroupName(final byte[] groupId) { var group = m.getGroup(getGroupId(groupId)); @@ -431,10 +454,18 @@ public class DbusSignalImpl implements Signal { if (group == null) { return List.of(); } else { - return group.getMembers().stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList()); + final var members = group.getMembers(); + return getRecipientStrings(members); } } + @Override + public byte[] createGroup( + final String name, final List members, final String avatar + ) throws Error.AttachmentInvalid, Error.Failure, Error.InvalidNumber { + return updateGroup(new byte[0], name, members, avatar); + } + @Override public byte[] updateGroup(byte[] groupId, String name, List members, String avatar) { try { @@ -448,19 +479,11 @@ public class DbusSignalImpl implements Signal { return results.first().serialize(); } else { final var results = m.updateGroup(getGroupId(groupId), - name, - null, - memberIdentifiers, - null, - null, - null, - false, - null, - null, - null, - avatar == null ? null : new File(avatar), - null, - null); + UpdateGroup.newBuilder() + .withName(name) + .withMembers(memberIdentifiers) + .withAvatarFile(avatar == null ? null : new File(avatar)) + .build()); if (results != null) { checkSendMessageResults(results.getTimestamp(), results.getResults()); } @@ -740,6 +763,10 @@ public class DbusSignalImpl implements Signal { throw new Error.Failure(message.toString()); } + private static List getRecipientStrings(final Set members) { + return members.stream().map(RecipientAddress::getLegacyIdentifier).collect(Collectors.toList()); + } + private static Set getSingleRecipientIdentifiers( final Collection recipientStrings, final String localNumber ) throws DBusExecutionException { @@ -817,6 +844,38 @@ public class DbusSignalImpl implements Signal { this.devices.clear(); } + private static String getGroupObjectPath(String basePath, byte[] groupId) { + return basePath + "/Groups/" + Base64.getEncoder() + .encodeToString(groupId) + .replace("+", "_") + .replace("/", "_") + .replace("=", "_"); + } + + private void updateGroups() { + List groups; + groups = m.getGroups(); + + unExportGroups(); + + groups.forEach(g -> { + final var object = new DbusSignalGroupImpl(g.getGroupId()); + try { + connection.exportObject(object); + } catch (DBusException e) { + e.printStackTrace(); + } + this.groups.add(new StructGroup(new DBusPath(object.getObjectPath()), + g.getGroupId().serialize(), + emptyIfNull(g.getTitle()))); + }); + } + + private void unExportGroups() { + this.groups.stream().map(StructGroup::getObjectPath).map(DBusPath::getPath).forEach(connection::unExportObject); + this.groups.clear(); + } + public class DbusSignalDeviceImpl extends DbusProperties implements Signal.Device { private final org.asamk.signal.manager.api.Device device; @@ -858,4 +917,166 @@ public class DbusSignalImpl implements Signal { } } } + + public class DbusSignalGroupImpl extends DbusProperties implements Signal.Group { + + private final GroupId groupId; + + public DbusSignalGroupImpl(final GroupId groupId) { + this.groupId = groupId; + super.addPropertiesHandler(new DbusInterfacePropertiesHandler("org.asamk.Signal.Group", + List.of(new DbusProperty<>("Id", groupId::serialize), + new DbusProperty<>("Name", () -> emptyIfNull(getGroup().getTitle()), this::setGroupName), + new DbusProperty<>("Description", + () -> emptyIfNull(getGroup().getDescription()), + this::setGroupDescription), + new DbusProperty<>("Avatar", this::setGroupAvatar), + new DbusProperty<>("IsBlocked", () -> getGroup().isBlocked(), this::setIsBlocked), + new DbusProperty<>("IsMember", () -> getGroup().isMember()), + new DbusProperty<>("IsAdmin", () -> getGroup().isAdmin()), + new DbusProperty<>("MessageExpirationTimer", + () -> getGroup().getMessageExpirationTimer(), + this::setMessageExpirationTime), + new DbusProperty<>("Members", + () -> new Variant<>(getRecipientStrings(getGroup().getMembers()), "as")), + new DbusProperty<>("PendingMembers", + () -> new Variant<>(getRecipientStrings(getGroup().getPendingMembers()), "as")), + new DbusProperty<>("RequestingMembers", + () -> new Variant<>(getRecipientStrings(getGroup().getRequestingMembers()), "as")), + new DbusProperty<>("Admins", + () -> new Variant<>(getRecipientStrings(getGroup().getAdminMembers()), "as")), + new DbusProperty<>("PermissionAddMember", + () -> getGroup().getPermissionAddMember().name(), + this::setGroupPermissionAddMember), + new DbusProperty<>("PermissionEditDetails", + () -> getGroup().getPermissionEditDetails().name(), + this::setGroupPermissionEditDetails), + new DbusProperty<>("PermissionSendMessage", + () -> getGroup().getPermissionSendMessage().name(), + this::setGroupPermissionSendMessage), + new DbusProperty<>("GroupInviteLink", () -> { + final var groupInviteLinkUrl = getGroup().getGroupInviteLinkUrl(); + return groupInviteLinkUrl == null ? "" : groupInviteLinkUrl.getUrl(); + })))); + } + + @Override + public String getObjectPath() { + return getGroupObjectPath(objectPath, groupId.serialize()); + } + + @Override + public void quitGroup() throws Error.Failure { + try { + m.quitGroup(groupId, Set.of()); + } catch (GroupNotFoundException | NotAGroupMemberException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (LastGroupAdminException e) { + throw new Error.LastGroupAdmin(e.getMessage()); + } + } + + @Override + public void addMembers(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withMembers(memberIdentifiers).build()); + } + + @Override + public void removeMembers(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withRemoveMembers(memberIdentifiers).build()); + } + + @Override + public void addAdmins(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withAdmins(memberIdentifiers).build()); + } + + @Override + public void removeAdmins(final List recipients) throws Error.Failure { + final var memberIdentifiers = getSingleRecipientIdentifiers(recipients, m.getSelfNumber()); + updateGroup(UpdateGroup.newBuilder().withRemoveAdmins(memberIdentifiers).build()); + } + + @Override + public void resetLink() throws Error.Failure { + updateGroup(UpdateGroup.newBuilder().withResetGroupLink(true).build()); + } + + @Override + public void disableLink() throws Error.Failure { + updateGroup(UpdateGroup.newBuilder().withGroupLinkState(GroupLinkState.DISABLED).build()); + } + + @Override + public void enableLink(final boolean requiresApproval) throws Error.Failure { + updateGroup(UpdateGroup.newBuilder() + .withGroupLinkState(requiresApproval + ? GroupLinkState.ENABLED_WITH_APPROVAL + : GroupLinkState.ENABLED) + .build()); + } + + private org.asamk.signal.manager.api.Group getGroup() { + return m.getGroup(groupId); + } + + private void setGroupName(final String name) { + updateGroup(UpdateGroup.newBuilder().withName(name).build()); + } + + private void setGroupDescription(final String description) { + updateGroup(UpdateGroup.newBuilder().withDescription(description).build()); + } + + private void setGroupAvatar(final String avatar) { + updateGroup(UpdateGroup.newBuilder().withAvatarFile(new File(avatar)).build()); + } + + private void setMessageExpirationTime(final int expirationTime) { + updateGroup(UpdateGroup.newBuilder().withExpirationTimer(expirationTime).build()); + } + + private void setGroupPermissionAddMember(final String permission) { + updateGroup(UpdateGroup.newBuilder().withAddMemberPermission(GroupPermission.valueOf(permission)).build()); + } + + private void setGroupPermissionEditDetails(final String permission) { + updateGroup(UpdateGroup.newBuilder() + .withEditDetailsPermission(GroupPermission.valueOf(permission)) + .build()); + } + + private void setGroupPermissionSendMessage(final String permission) { + updateGroup(UpdateGroup.newBuilder() + .withIsAnnouncementGroup(GroupPermission.valueOf(permission) == GroupPermission.ONLY_ADMINS) + .build()); + } + + private void setIsBlocked(final boolean isBlocked) { + try { + m.setGroupBlocked(groupId, isBlocked); + } catch (GroupNotFoundException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } + } + + private void updateGroup(final UpdateGroup updateGroup) { + try { + m.updateGroup(groupId, updateGroup); + } catch (IOException e) { + throw new Error.Failure(e.getMessage()); + } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { + throw new Error.GroupNotFound(e.getMessage()); + } catch (AttachmentInvalidException e) { + throw new Error.AttachmentInvalid(e.getMessage()); + } + } + } } From 6501ffcdacc2ada63f30a00299d437d5aeff1293 Mon Sep 17 00:00:00 2001 From: John Freed Date: Tue, 21 Sep 2021 16:42:51 +0200 Subject: [PATCH 0851/2005] Update documentation --- man/signal-cli-dbus.5.adoc | 148 ++++++++++++++++++++++--------------- 1 file changed, 89 insertions(+), 59 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 8168c421..3d977de6 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -48,9 +48,9 @@ Phone numbers always have the format + == Methods === Control methods -These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`). -Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to -`/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_). +These methods are available if the daemon is started anonymously (without an explicit `-u USERNAME`). +Requests are sent to `/org/asamk/Signal`; requests related to individual accounts are sent to +`/org/asamk/Signal/_441234567890` where the + dialing code is replaced by an underscore (_). Only `version()` is activated in single-user mode; the rest are disabled. link() -> deviceLinkUri:: @@ -63,7 +63,7 @@ can be captured by a Signal smartphone client. For example: `dbus-send --session --dest=org.asamk.Signal --type=method_call --print-reply /org/asamk/Signal org.asamk.Signal.link string:"My secondary client"|tr '\n' '\0'|sed 's/.*string //g'|sed 's/\"//g'|qrencode -s10 -tANSI256` -Exception: Failure +Exceptions: Failure listAccounts() -> accountList:: * accountList : Array of all attached accounts in DBus object path form @@ -89,20 +89,28 @@ verify(number, verificationCode) -> <>:: Command fails if PIN was set after previous registration; use verifyWithPin instead. -Exception: Failure, InvalidNumber +Exceptions: Failure, InvalidNumber verifyWithPin(number, verificationCode, pin) -> <>:: * number : Phone number * verificationCode : Code received from Signal after successful registration request * pin : PIN you set with setPin command after verifying previous registration -Exception: Failure, InvalidNumber +Exceptions: Failure, InvalidNumber version() -> version:: * version : Version string of signal-cli Exceptions: None +=== Device methods +Requests for these methods are sent to a specific device (main or linked); the list is available +from the listDevices() method (see below under "Other methods"). + +removeDevice() -> <>:: + +Exceptions: Failure + === Other methods updateGroup(groupId, newName, members, avatar) -> groupId:: @@ -130,11 +138,11 @@ setExpirationTimer(number, expiration) -> <>:: * number : Phone number of recipient * expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration. -Exceptions: Failure +Exceptions: Failure, InvalidNumber setContactBlocked(number, block) -> <>:: * number : Phone number affected by method -* block : 0=remove block , 1=blocked +* block : false=remove block, true=blocked Messages from blocked numbers will no longer be forwarded via DBus. @@ -142,11 +150,11 @@ Exceptions: InvalidNumber setGroupBlocked(groupId, block) -> <>:: * groupId : Byte array representing the internal group identifier -* block : 0=remove block , 1=blocked +* block : false=remove block , true=blocked Messages from blocked groups will no longer be forwarded via DBus. -Exceptions: GroupNotFound +Exceptions: GroupNotFound, InvalidGroupId joinGroup(inviteURI) -> <>:: * inviteURI : String starting with https://signal.group which is generated when you share a group link via Signal App @@ -158,10 +166,11 @@ quitGroup(groupId) -> <>:: Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember() -Exceptions: GroupNotFound, Failure +Exceptions: GroupNotFound, Failure, InvalidGroupId -isMember(groupId) -> active:: -* groupId : Byte array representing the internal group identifier +isMember(groupId) -> isMember:: +* groupId : Byte array representing the internal group identifier +* isMember : true=you are a group member; false=you are not a group member Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) @@ -174,9 +183,9 @@ sendGroupMessage(message, attachments, groupId) -> timestamp:: * message : Text to send (can be UTF8) * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) * groupId : Byte array representing the internal group identifier -* timestamp : Can be used to identify the corresponding signal reply +* timestamp : Long, can be used to identify the corresponding Signal reply -Exceptions: GroupNotFound, Failure, AttachmentInvalid +Exceptions: GroupNotFound, Failure, AttachmentInvalid, InvalidGroupId sendContacts() -> <>:: @@ -188,12 +197,12 @@ sendSyncRequest() -> <>:: Sends a synchronization request to the primary device (for group, contacts, ...). Only works if sent from a secondary device. -Exception: Failure +Exceptions: Failure sendNoteToSelfMessage(message, attachments) -> timestamp:: * message : Text to send (can be UTF8) * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) -* timestamp : Can be used to identify the corresponding signal reply +* timestamp : Long, can be used to identify the corresponding Signal reply Exceptions: Failure, AttachmentInvalid @@ -202,8 +211,8 @@ sendMessage(message, attachments, recipients) -> timestamp:: * message : Text to send (can be UTF8) * attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) * recipient : Phone number of a single recipient -* recipients : Array of phone numbers -* timestamp : Can be used to identify the corresponding signal reply +* recipients : String array of phone numbers +* timestamp : Long, can be used to identify the corresponding Signal reply Depending on the type of the recipient field this sends a message to one or multiple recipients. @@ -215,10 +224,9 @@ sendTyping(recipient, stop) -> <>:: Exceptions: Failure, GroupNotFound, UntrustedIdentity - -sendReadReceipt(recipient, targetSentTimestamp) -> <>:: +sendReadReceipt(recipient, targetSentTimestamps) -> <>:: * recipient : Phone number of a single recipient -* targetSentTimestamp : Array of Longs to identify the corresponding signal messages +* targetSentTimestamps : Array of Longs to identify the corresponding Signal messages Exceptions: Failure, UntrustedIdentity @@ -227,10 +235,10 @@ sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimesta * remove : Boolean, whether a previously sent reaction (emoji) should be removed * targetAuthor : String with the phone number of the author of the message to which to react * targetSentTimestamp : Long representing timestamp of the message to which to react -* groupId : Byte array with base64 encoded group identifier +* groupId : Byte array representing the internal group identifier * timestamp : Long, can be used to identify the corresponding signal reply -Exceptions: Failure, InvalidNumber, GroupNotFound +Exceptions: Failure, InvalidNumber, GroupNotFound, InvalidGroupId sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipient) -> timestamp:: sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients) -> timestamp:: @@ -240,7 +248,7 @@ sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp * targetSentTimestamp : Long representing timestamp of the message to which to react * recipient : String with the phone number of a single recipient * recipients : Array of strings with phone numbers, should there be more recipients -* timestamp : Long, can be used to identify the corresponding signal reply +* timestamp : Long, can be used to identify the corresponding Signal reply Depending on the type of the recipient(s) field this sends a reaction to one or multiple recipients. @@ -251,7 +259,7 @@ sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId) -> timestamp, recipient) -> timestamp:: sendRemoteDeleteMessage(targetSentTimestamp, recipients) -> timestamp:: @@ -268,66 +276,87 @@ getContactName(number) -> name:: * number : Phone number * name : Contact's name in local storage (from the master device for a linked account, or the one set with setContactName); if not set, contact's profile name is used +Exceptions: None + setContactName(number,name<>) -> <>:: * number : Phone number * name : Name to be set in contacts (in local storage with signal-cli) +Exceptions: InvalidNumber, Failure + getGroupIds() -> groupList:: groupList : Array of Byte arrays representing the internal group identifiers All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() +Exceptions: None + getGroupName(groupId) -> groupName:: -groupName : The display name of the group -groupId : Byte array representing the internal group identifier +* groupId : Byte array representing the internal group identifier +* groupName : The display name of the group Exceptions: None, if the group name is not found an empty string is returned getGroupMembers(groupId) -> members:: -members : String array with the phone numbers of all active members of a group -groupId : Byte array representing the internal group identifier +* groupId : Byte array representing the internal group identifier +* members : String array with the phone numbers of all active members of a group Exceptions: None, if the group name is not found an empty array is returned listNumbers() -> numbers:: -numbers : String array of all known numbers +* numbers : String array of all known numbers This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages) +Exceptions: None + getContactNumber(name) -> numbers:: * numbers : Array of phone number * name : Contact or profile name ("firstname lastname") Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set. -isContactBlocked(number) -> state:: -* number : Phone number -* state : 1=blocked, 0=not blocked +Exceptions: None -Exceptions: None, for unknown numbers 0 (false) is returned +isContactBlocked(number) -> blocked:: +* number : Phone number +* blocked : true=blocked, false=not blocked -isGroupBlocked(groupId) -> state:: -* groupId : Byte array representing the internal group identifier -* state : 1=blocked, 0=not blocked +For unknown numbers false is returned but no exception is raised. -Exceptions: None, for unknown groups 0 (false) is returned +Exceptions: InvalidPhoneNumber + +isGroupBlocked(groupId) -> isGroupBlocked:: +* groupId : Byte array representing the internal group identifier +* isGroupBlocked : true=group is blocked; false=group is not blocked + +Dbus will not forward messages from a group when you have blocked it. + +Exceptions: InvalidGroupId, Failure removePin() -> <>:: Removes registration PIN protection. -Exception: Failure +Exceptions: Failure setPin(pin) -> <>:: * pin : PIN you set after registration (resets after 7 days of inactivity) Sets a registration lock PIN, to prevent others from registering your number. -Exception: Failure +Exceptions: Failure version() -> version:: * version : Version string of signal-cli +Exceptions: None + +getSelfNumber() -> number:: +* number : Your phone number + +Exceptions: None + isRegistered() -> result:: isRegistered(number) -> result:: isRegistered(numbers) -> results:: @@ -336,12 +365,12 @@ isRegistered(numbers) -> results:: * result : true=number is registered, false=number is not registered * results : Boolean array of results -Exception: InvalidNumber for an incorrectly formatted phone number. For unknown numbers, false is returned, but no exception is raised. If no number is given, returns whether you are registered (presumably true). +For unknown numbers, false is returned, but no exception is raised. If no number is given, returns true (indicating that you are registered). + +Exceptions: InvalidNumber addDevice(deviceUri) -> <>:: -* deviceUri : URI in the form of tsdevice:/?uuid=... Normally received from Signal desktop or smartphone app - -Exception: InvalidUri +* deviceUri : URI in the form of tsdevice:/?uuid=... Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. listDevices() -> devices:: * devices : Array of structs (objectPath, id, name) @@ -349,25 +378,19 @@ listDevices() -> devices:: ** id : Long representing the deviceId ** name : String representing the device's name -Exception: Failure +Exceptions: InvalidUri -removeDevice(deviceId) -> <>:: -* deviceId : Device ID to remove, obtained from listDevices() command +getDevice(deviceId) -> devicePath:: +* deviceId : Long representing a (potential) deviceId +* devicePath : DBusPath object for the device -Exception: Failure - -updateDeviceName(deviceName) -> <>:: -* deviceName : New name - -Set a new name for this device (main or linked). - -Exception: Failure +Exceptions: DeviceNotFound uploadStickerPack(stickerPackPath) -> url:: * stickerPackPath : Path to the manifest.json file or a zip file in the same directory * url : URL of sticker pack after successful upload -Exception: Failure +Exceptions: Failure submitRateLimitChallenge(challenge, captcha) -> <>:: * challenge : The challenge token taken from the proof required error. @@ -377,7 +400,14 @@ Can be used to lift some rate-limits by solving a captcha. Exception: IOErrorException == Signals -SyncMessageReceived (timestamp, sender, destination, groupId,message, attachments):: +SyncMessageReceived (timestamp, sender, destination, groupId, message, attachments):: +* timestamp : Integer value that can be used to associate this e.g. with a sendMessage() +* sender : Phone number of the sender +* destination : DBus code for destination +* groupId : Byte array representing the internal group identifier (empty when private message) +* message : Message text +* attachments : String array of filenames in the signal-cli storage (~/.local/share/signal-cli/attachments/) + The sync message is received when the user sends a message from a linked device. ReceiptReceived (timestamp, sender):: @@ -391,7 +421,7 @@ MessageReceived(timestamp, sender, groupId, message, attachments Date: Sat, 9 Oct 2021 11:55:33 -0400 Subject: [PATCH 0852/2005] Update to new provisioning URL scheme (#762) --- lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java b/lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java index 1f9d10ff..3ba1ef20 100644 --- a/lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/DeviceLinkInfo.java @@ -65,7 +65,7 @@ public class DeviceLinkInfo { public URI createDeviceLinkUri() { final var deviceKeyString = Base64.getEncoder().encodeToString(deviceKey.serialize()).replace("=", ""); try { - return new URI("tsdevice:/?uuid=" + return new URI("sgnl://linkdevice?uuid=" + URLEncoder.encode(deviceIdentifier, StandardCharsets.UTF_8) + "&pub_key=" + URLEncoder.encode(deviceKeyString, StandardCharsets.UTF_8)); From abd0e718141bd1d4802f8801e4ae801eaa8efdc7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 9 Oct 2021 17:57:32 +0200 Subject: [PATCH 0853/2005] Update documentation --- man/signal-cli-dbus.5.adoc | 4 ++-- man/signal-cli.1.adoc | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 3d977de6..c5c27fa9 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -58,7 +58,7 @@ link(newDeviceName) -> deviceLinkUri:: * newDeviceName : Name to give new device (defaults to "cli" if no name is given) * deviceLinkUri : URI of newly linked device -Returns a URI of the form "tsdevice:/?uuid=...". This can be piped to a QR encoder to create a display that +Returns a URI of the form "sgnl://linkdevice/?uuid=...". This can be piped to a QR encoder to create a display that can be captured by a Signal smartphone client. For example: `dbus-send --session --dest=org.asamk.Signal --type=method_call --print-reply /org/asamk/Signal org.asamk.Signal.link string:"My secondary client"|tr '\n' '\0'|sed 's/.*string //g'|sed 's/\"//g'|qrencode -s10 -tANSI256` @@ -370,7 +370,7 @@ For unknown numbers, false is returned, but no exception is raised. If no number Exceptions: InvalidNumber addDevice(deviceUri) -> <>:: -* deviceUri : URI in the form of tsdevice:/?uuid=... Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. +* deviceUri : URI in the form of "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. listDevices() -> devices:: * devices : Array of structs (objectPath, id, name) diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index 9829fe00..f2a1a960 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -144,7 +144,7 @@ Remove the registration lock pin. === link Link to an existing device, instead of registering a new number. -This shows a "tsdevice:/…" URI. If you want to connect to another signal-cli instance, you can just use this URI. +This shows a "sgnl://linkdevice/?uuid=..." URI. If you want to connect to another signal-cli instance, you can just use this URI. If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app. *-n* NAME, *--name* NAME:: @@ -158,7 +158,8 @@ Only works, if this is the master device. *--uri* URI:: Specify the uri contained in the QR code shown by the new device. -You will need the full uri enclosed in quotation marks, such as "tsdevice:/?uuid=....." +You will need the full URI such as "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") +Make sure to enclose it in quotation marks for shells. === listDevices From 07742843df5633c800f8bc9106717adc2a0a93a8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Oct 2021 13:30:47 +0200 Subject: [PATCH 0854/2005] Update gradle wrapper --- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 269 ++++++++++++++--------- 3 files changed, 160 insertions(+), 111 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 18435 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 B8dv}T delta 17845 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 X49Luk)baa85H9$5YCsTPT`SVRWMtMW diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f371643e..ffed3a25 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c..1b6c7873 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" From bfddb40d73848a767f3a2dfb7a93d7e50172781c Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Oct 2021 13:31:34 +0200 Subject: [PATCH 0855/2005] Reconnect websockets after errors --- .../org/asamk/signal/manager/SignalWebSocketHealthMonitor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java index 24a673ff..556e227d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java @@ -101,6 +101,7 @@ public final class SignalWebSocketHealthMonitor implements HealthMonitor { if (healthState.mismatchErrorTracker.addSample(System.currentTimeMillis())) { logger.warn("Received too many mismatch device errors, forcing new websockets."); signalWebSocket.forceNewWebSockets(); + signalWebSocket.connect(); } } } @@ -146,6 +147,7 @@ public final class SignalWebSocketHealthMonitor implements HealthMonitor { + " needed by: " + keepAliveRequiredSinceTime); signalWebSocket.forceNewWebSockets(); + signalWebSocket.connect(); } else { signalWebSocket.sendKeepAlive(); } From 09730b474b11df36d68081ce2937dbf38224c0da Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 10 Oct 2021 13:31:44 +0200 Subject: [PATCH 0856/2005] Update libsignal-service-java --- lib/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 6e528805..e4e7dc20 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -14,7 +14,7 @@ repositories { } dependencies { - api("com.github.turasa:signal-service-java:2.15.3_unofficial_28") + api("com.github.turasa:signal-service-java:2.15.3_unofficial_29") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.slf4j:slf4j-api:1.7.30") From a95886c4911f8538b53042149312727fb4a3839a Mon Sep 17 00:00:00 2001 From: John Freed Date: Mon, 11 Oct 2021 16:53:24 +0200 Subject: [PATCH 0857/2005] update DBus documentation (#773) --- man/signal-cli-dbus.5.adoc | 513 +++++++++++++++++++++++-------------- 1 file changed, 325 insertions(+), 188 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index c5c27fa9..f38afc93 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -29,7 +29,7 @@ method(arg1, arg2, ...) -> return Where is according to DBus specification: -* : Array of ... (comma-separated list) +* : Array of ... (comma-separated list) (array:) * (...) : Struct (cannot be sent via `dbus-send`) * : Boolean (false|true) (boolean:) * : Signed 32-bit (int) integer (int32:) @@ -103,50 +103,191 @@ version() -> version:: Exceptions: None -=== Device methods -Requests for these methods are sent to a specific device (main or linked); the list is available -from the listDevices() method (see below under "Other methods"). +=== Group control methods +The following methods listen to the recipient's object path, which is constructed as follows: +"/org/asamk/Signal/" + DBusNumber +* DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) -removeDevice() -> <>:: +createGroup(groupName, members, avatar) -> groupId:: +* groupName : String representing the display name of the group +* members : String array of new members to be invited to group +* avatar : Filename of avatar picture to be set for group (empty if none) +* groupId : Byte array representing the internal group identifier + +Exceptions: AttachmentInvalid, Failure, InvalidNumber; + +getGroup(groupId) -> objectPath:: +* groupId : Byte array representing the internal group identifier +* objectPath : DBusPath for the group + +getGroupMembers(groupId) -> members:: +* groupId : Byte array representing the internal group identifier +* members : String array with the phone numbers of all active members of a group + +Exceptions: None, if the group name is not found an empty array is returned + +joinGroup(inviteURI) -> <>:: +* inviteURI : String starting with https://signal.group/# + +If the link requires admin approval, this adds you to the requesting members list. Otherwise, this adds you to the pending members list. Exceptions: Failure -=== Other methods +listGroups() -> groups:: +* groups : Array of Structs(objectPath, groupId, groupName) +** objectPath : DBusPath +** groupId : Byte array representing the internal group identifier +** groupName : String representing the display name of the group -updateGroup(groupId, newName, members, avatar) -> groupId:: -* groupId : Byte array representing the internal group identifier -* newName : New name of group (empty if unchanged) -* members : String array of new members to be invited to group -* avatar : Filename of avatar picture to be set for group (empty if none) +sendGroupMessage(message, attachments, groupId) -> timestamp:: +* message : Text to send (can be UTF8) +* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) +* groupId : Byte array representing the internal group identifier +* timestamp : Long, can be used to identify the corresponding Signal reply -Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound +Exceptions: GroupNotFound, Failure, AttachmentInvalid, InvalidGroupId -updateProfile(name, about, aboutEmoji , avatar, remove) -> <>:: -updateProfile(givenName, familyName, about, aboutEmoji , avatar, remove) -> <>:: -* name : Name for your own profile (empty if unchanged) -* givenName : Given name for your own profile (empty if unchanged) -* familyName : Family name for your own profile (empty if unchanged) -* about : About message for profile (empty if unchanged) -* aboutEmoji : Emoji for profile (empty if unchanged) -* avatar : Filename of avatar picture for profile (empty if unchanged) -* remove : Set to true if the existing avatar picture should be removed +sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, groupId) -> timestamp:: +* emoji : Unicode grapheme cluster of the emoji +* remove : Boolean, whether a previously sent reaction (emoji) should be removed +* targetAuthor : String with the phone number of the author of the message to which to react +* targetSentTimestamp : Long representing timestamp of the message to which to react +* groupId : Byte array representing the internal group identifier +* timestamp : Long, can be used to identify the corresponding signal reply + +Exceptions: Failure, InvalidNumber, GroupNotFound, InvalidGroupId + +sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId) -> timestamp:: +* targetSentTimestamp : Long representing timestamp of the message to delete +* groupId : Byte array with base64 encoded group identifier +* timestamp : Long, can be used to identify the corresponding signal reply + +Exceptions: Failure, GroupNotFound, InvalidGroupId + +=== Group methods +The following methods listen to the group's object path, which can be obtained from the listGroups() method and is constructed as follows: +"/org/asamk/Signal/" + DBusNumber + "/Groups/" + DBusGroupId +* DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) +* DBusGroupId : groupId in base64 format, with underscore (_) replacing plus (+), equals (=), or slash (/) + +Groups have the following (case-sensitive) properties: +* Id (read-only) : Byte array representing the internal group identifier +* Name : Display name of the group +* Description : Description of the group +* Avatar (write-only) : Filename of the avatar +* IsBlocked : true=member will not receive group messages; false=not blocked +* IsMember (read-only) : always true (object path exists only for group members) +* IsAdmin (read-only) : true=member has admin privileges; false=not admin +* MessageExpirationTimer : int32 representing message expiration time for group +* Members (read-only) : String array of group members' phone numbers +* PendingMembers (read-only) : String array of pending members' phone numbers +* RequestingMembers (read-only) : String array of requesting members' phone numbers +* Admins (read-only) : String array of admins' phone numbers +* PermissionAddMember : String representing who has permission to add members +** ONLY_ADMINS, EVERY_MEMBER +* PermissionEditDetails : String representing who may edit group details +** ONLY_ADMINS, EVERY_MEMBER +* PermissionSendMessage : String representing who post messages to group +** ONLY_ADMINS, EVERY_MEMBER (note that ONLY_ADMINS is equivalent to IsAnnouncementGroup) +* GroupInviteLink (read-only) : String of the invitation link (starts with https://signal.group/#) + +To get a property, use (replacing `--session` with `--system` if needed): +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Get string:org.asamk.Signal.Group string:$PROPERTY_NAME` + +To set a property, use: +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Set string:org.asamk.Signal.Group string:$PROPERTY_NAME variant:$PROPERTY_TYPE:$PROPERTY_VALUE` + +To get all properties, use: +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.GetAll string:org.asamk.Signal.Group` + +addAdmins(recipients) -> <>:: +* recipients : String array of phone numbers + +Grant admit privileges to recipients. Exceptions: Failure +addMembers(recipients) -> <>:: +* recipients : String array of phone numbers -setExpirationTimer(number, expiration) -> <>:: -* number : Phone number of recipient -* expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration. +Add recipients to group if they are pending members; otherwise add recipients to list of requesting members. -Exceptions: Failure, InvalidNumber +Exceptions: Failure -setContactBlocked(number, block) -> <>:: -* number : Phone number affected by method -* block : false=remove block, true=blocked +disableLink() -> <>:: -Messages from blocked numbers will no longer be forwarded via DBus. +Disables the group's invitation link. -Exceptions: InvalidNumber +Exceptions: Failure + +enableLink(requiresApproval) -> <>:: +* requiresApproval : true=add numbers using the link to the requesting members list + +Enables the group's invitation link. + +Exceptions: Failure + +quitGroup() -> <>:: +Exceptions: Failure, LastGroupAdmin + +removeAdmins(recipients) -> <>:: +* recipients : String array of phone numbers + +Remove admin privileges from recipients. + +Exceptions: Failure + +removeMembers(recipients) -> <>:: +* recipients : String array of phone numbers + +Remove recipients from group. + +Exceptions: Failure + +resetLink() -> <>:: + +Resets the group's invitation link to a new random URL starting with https://signal.group/# + +Exceptions: Failure + +=== Deprecated group control methods +The following deprecated methods listen to the recipient's object path, which is constructed as follows: +"/org/asamk/Signal/" + DBusNumber +* DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) + +getGroupIds() -> groupList:: +groupList : Array of Byte arrays representing the internal group identifiers + +All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() + +Exceptions: None + +getGroupName(groupId) -> groupName:: +* groupId : Byte array representing the internal group identifier +* groupName : The display name of the group + +Exceptions: None, if the group name is not found an empty string is returned + +isGroupBlocked(groupId) -> isGroupBlocked:: +* groupId : Byte array representing the internal group identifier +* isGroupBlocked : true=group is blocked; false=group is not blocked + +Dbus will not forward messages from a group when you have blocked it. + +Exceptions: InvalidGroupId, Failure + +isMember(groupId) -> isMember:: +* groupId : Byte array representing the internal group identifier +* isMember : true=you are a group member; false=you are not a group member + +Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) + +quitGroup(groupId) -> <>:: +* groupId : Byte array representing the internal group identifier + +Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember() + +Exceptions: GroupNotFound, Failure, InvalidGroupId setGroupBlocked(groupId, block) -> <>:: * groupId : Byte array representing the internal group identifier @@ -156,36 +297,35 @@ Messages from blocked groups will no longer be forwarded via DBus. Exceptions: GroupNotFound, InvalidGroupId -joinGroup(inviteURI) -> <>:: -* inviteURI : String starting with https://signal.group which is generated when you share a group link via Signal App +updateGroup(groupId, newName, members, avatar) -> groupId:: +* groupId : Byte array representing the internal group identifier +* newName : New name of group (empty if unchanged) +* members : String array of new members to be invited to group +* avatar : Filename of avatar picture to be set for group (empty if none) -Exceptions: Failure +Exceptions: AttachmentInvalid, Failure, InvalidNumber, GroupNotFound -quitGroup(groupId) -> <>:: -* groupId : Byte array representing the internal group identifier +=== Device control methods +The following methods listen to the recipient's object path, which is constructed as follows: +"/org/asamk/Signal/" + DBusNumber +* DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) -Note that quitting a group will not remove the group from the getGroupIds command, but set it inactive which can be tested with isMember() +addDevice(deviceUri) -> <>:: +* deviceUri : URI in the form of "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. -Exceptions: GroupNotFound, Failure, InvalidGroupId +getDevice(deviceId) -> devicePath:: +* deviceId : Long representing a deviceId +* devicePath : DBusPath object for the device -isMember(groupId) -> isMember:: -* groupId : Byte array representing the internal group identifier -* isMember : true=you are a group member; false=you are not a group member +Exceptions: DeviceNotFound -Note that this method does not raise an Exception for a non-existing/unknown group but will simply return 0 (false) +listDevices() -> devices:: +* devices : Array of structs (objectPath, id, name) +** objectPath : DBusPath representing the device's object path +** id : Long representing the deviceId +** name : String representing the device's name -sendEndSessionMessage(recipients) -> <>:: -* recipients : Array of phone numbers - -Exceptions: Failure, InvalidNumber, UntrustedIdentity - -sendGroupMessage(message, attachments, groupId) -> timestamp:: -* message : Text to send (can be UTF8) -* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) -* groupId : Byte array representing the internal group identifier -* timestamp : Long, can be used to identify the corresponding Signal reply - -Exceptions: GroupNotFound, Failure, AttachmentInvalid, InvalidGroupId +Exceptions: InvalidUri sendContacts() -> <>:: @@ -199,12 +339,89 @@ Sends a synchronization request to the primary device (for group, contacts, ...) Exceptions: Failure -sendNoteToSelfMessage(message, attachments) -> timestamp:: -* message : Text to send (can be UTF8) -* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) -* timestamp : Long, can be used to identify the corresponding Signal reply +=== Device methods and properties +The following methods listen to the device's object path, which is constructed as follows: +"/org/asamk/Signal/" + DBusNumber + "/Devices/" + deviceId +* DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) +* deviceId : Long representing the device identifier (obtained from listDevices() method) -Exceptions: Failure, AttachmentInvalid +Devices have the following (case-sensitive) properties: +* Id (read-only) : Long representing the device identifier +* Created (read-only) : Long representing the number of milliseconds since the Unix epoch +* LastSeen (read-only) : Long representing the number of milliseconds since the Unix epoch +* Name : String representing the display name of the device + +To get a property, use (replacing `--session` with `--system` if needed): +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Get string:org.asamk.Signal.Device string:$PROPERTY_NAME` + +To set a property, use: +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.Set string:org.asamk.Signal.Device string:$PROPERTY_NAME variant:$PROPERTY_TYPE:$PROPERTY_VALUE` + +To get all properties, use: +`dbus-send --session --dest=org.asamk.Signal --print-reply $OBJECT_PATH org.freedesktop.DBus.Properties.GetAll string:org.asamk.Signal.Device` + +removeDevice() -> <>:: + +Exceptions: Failure + +=== Other methods + +getContactName(number) -> name:: +* number : Phone number +* name : Contact's name in local storage (from the master device for a linked account, or the one set with setContactName); if not set, contact's profile name is used + +Exceptions: None + +getContactNumber(name) -> numbers:: +* numbers : Array of phone number +* name : Contact or profile name ("firstname lastname") + +Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set. + +Exceptions: None + +getSelfNumber() -> number:: +* number : Your phone number + +Exceptions: None + +isContactBlocked(number) -> blocked:: +* number : Phone number +* blocked : true=blocked, false=not blocked + +For unknown numbers false is returned but no exception is raised. + +Exceptions: InvalidPhoneNumber + +isRegistered() -> result:: +isRegistered(number) -> result:: +isRegistered(numbers) -> results:: +* number : Phone number +* numbers : String array of phone numbers +* result : true=number is registered, false=number is not registered +* results : Boolean array of results + +For unknown numbers, false is returned, but no exception is raised. If no number is given, returns true (indicating that you are registered). + +Exceptions: InvalidNumber + +listNumbers() -> numbers:: +* numbers : String array of all known numbers + +This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages) + +Exceptions: None + +removePin() -> <>:: + +Removes registration PIN protection. + +Exceptions: Failure + +sendEndSessionMessage(recipients) -> <>:: +* recipients : Array of phone numbers + +Exceptions: Failure, InvalidNumber, UntrustedIdentity sendMessage(message, attachments, recipient) -> timestamp:: sendMessage(message, attachments, recipients) -> timestamp:: @@ -218,28 +435,6 @@ Depending on the type of the recipient field this sends a message to one or mult Exceptions: AttachmentInvalid, Failure, InvalidNumber, UntrustedIdentity -sendTyping(recipient, stop) -> <>:: -* recipient : Phone number of a single recipient -* targetSentTimestamp : True, if typing state should be stopped - -Exceptions: Failure, GroupNotFound, UntrustedIdentity - -sendReadReceipt(recipient, targetSentTimestamps) -> <>:: -* recipient : Phone number of a single recipient -* targetSentTimestamps : Array of Longs to identify the corresponding Signal messages - -Exceptions: Failure, UntrustedIdentity - -sendGroupMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, groupId) -> timestamp:: -* emoji : Unicode grapheme cluster of the emoji -* remove : Boolean, whether a previously sent reaction (emoji) should be removed -* targetAuthor : String with the phone number of the author of the message to which to react -* targetSentTimestamp : Long representing timestamp of the message to which to react -* groupId : Byte array representing the internal group identifier -* timestamp : Long, can be used to identify the corresponding signal reply - -Exceptions: Failure, InvalidNumber, GroupNotFound, InvalidGroupId - sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipient) -> timestamp:: sendMessageReaction(emoji, remove, targetAuthor, targetSentTimestamp, recipients) -> timestamp:: * emoji : Unicode grapheme cluster of the emoji @@ -254,12 +449,18 @@ Depending on the type of the recipient(s) field this sends a reaction to one or Exceptions: Failure, InvalidNumber -sendGroupRemoteDeleteMessage(targetSentTimestamp, groupId) -> timestamp:: -* targetSentTimestamp : Long representing timestamp of the message to delete -* groupId : Byte array with base64 encoded group identifier -* timestamp : Long, can be used to identify the corresponding signal reply +sendNoteToSelfMessage(message, attachments) -> timestamp:: +* message : Text to send (can be UTF8) +* attachments : String array of filenames to send as attachments (passed as filename, so need to be readable by the user signal-cli is running under) +* timestamp : Long, can be used to identify the corresponding Signal reply -Exceptions: Failure, GroupNotFound, InvalidGroupId +Exceptions: Failure, AttachmentInvalid + +sendReadReceipt(recipient, targetSentTimestamps) -> <>:: +* recipient : Phone number of a single recipient +* targetSentTimestamps : Array of Longs to identify the corresponding Signal messages + +Exceptions: Failure, UntrustedIdentity sendRemoteDeleteMessage(targetSentTimestamp, recipient) -> timestamp:: sendRemoteDeleteMessage(targetSentTimestamp, recipients) -> timestamp:: @@ -272,11 +473,19 @@ Depending on the type of the recipient(s) field this deletes a message with one Exceptions: Failure, InvalidNumber -getContactName(number) -> name:: -* number : Phone number -* name : Contact's name in local storage (from the master device for a linked account, or the one set with setContactName); if not set, contact's profile name is used +sendTyping(recipient, stop) -> <>:: +* recipient : Phone number of a single recipient +* targetSentTimestamp : True, if typing state should be stopped -Exceptions: None +Exceptions: Failure, GroupNotFound, UntrustedIdentity + +setContactBlocked(number, block) -> <>:: +* number : Phone number affected by method +* block : false=remove block, true=blocked + +Messages from blocked numbers will no longer be forwarded via DBus. + +Exceptions: InvalidNumber setContactName(number,name<>) -> <>:: * number : Phone number @@ -284,61 +493,11 @@ setContactName(number,name<>) -> <>:: Exceptions: InvalidNumber, Failure -getGroupIds() -> groupList:: -groupList : Array of Byte arrays representing the internal group identifiers +setExpirationTimer(number, expiration) -> <>:: +* number : Phone number of recipient +* expiration : int32 for the number of seconds before messages to this recipient disappear. Set to 0 to disable expiration. -All groups known are returned, regardless of their active or blocked status. To query that use isMember() and isGroupBlocked() - -Exceptions: None - -getGroupName(groupId) -> groupName:: -* groupId : Byte array representing the internal group identifier -* groupName : The display name of the group - -Exceptions: None, if the group name is not found an empty string is returned - -getGroupMembers(groupId) -> members:: -* groupId : Byte array representing the internal group identifier -* members : String array with the phone numbers of all active members of a group - -Exceptions: None, if the group name is not found an empty array is returned - -listNumbers() -> numbers:: -* numbers : String array of all known numbers - -This is a concatenated list of all defined contacts as well of profiles known (e.g. peer group members or sender of received messages) - -Exceptions: None - -getContactNumber(name) -> numbers:: -* numbers : Array of phone number -* name : Contact or profile name ("firstname lastname") - -Searches contacts and known profiles for a given name and returns the list of all known numbers. May result in e.g. two entries if a contact and profile name is set. - -Exceptions: None - -isContactBlocked(number) -> blocked:: -* number : Phone number -* blocked : true=blocked, false=not blocked - -For unknown numbers false is returned but no exception is raised. - -Exceptions: InvalidPhoneNumber - -isGroupBlocked(groupId) -> isGroupBlocked:: -* groupId : Byte array representing the internal group identifier -* isGroupBlocked : true=group is blocked; false=group is not blocked - -Dbus will not forward messages from a group when you have blocked it. - -Exceptions: InvalidGroupId, Failure - -removePin() -> <>:: - -Removes registration PIN protection. - -Exceptions: Failure +Exceptions: Failure, InvalidNumber setPin(pin) -> <>:: * pin : PIN you set after registration (resets after 7 days of inactivity) @@ -347,51 +506,6 @@ Sets a registration lock PIN, to prevent others from registering your number. Exceptions: Failure -version() -> version:: -* version : Version string of signal-cli - -Exceptions: None - -getSelfNumber() -> number:: -* number : Your phone number - -Exceptions: None - -isRegistered() -> result:: -isRegistered(number) -> result:: -isRegistered(numbers) -> results:: -* number : Phone number -* numbers : String array of phone numbers -* result : true=number is registered, false=number is not registered -* results : Boolean array of results - -For unknown numbers, false is returned, but no exception is raised. If no number is given, returns true (indicating that you are registered). - -Exceptions: InvalidNumber - -addDevice(deviceUri) -> <>:: -* deviceUri : URI in the form of "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. - -listDevices() -> devices:: -* devices : Array of structs (objectPath, id, name) -** objectPath : DBusPath representing the device's object path -** id : Long representing the deviceId -** name : String representing the device's name - -Exceptions: InvalidUri - -getDevice(deviceId) -> devicePath:: -* deviceId : Long representing a (potential) deviceId -* devicePath : DBusPath object for the device - -Exceptions: DeviceNotFound - -uploadStickerPack(stickerPackPath) -> url:: -* stickerPackPath : Path to the manifest.json file or a zip file in the same directory -* url : URL of sticker pack after successful upload - -Exceptions: Failure - submitRateLimitChallenge(challenge, captcha) -> <>:: * challenge : The challenge token taken from the proof required error. * captcha : The captcha token from the solved captcha on the Signal website.. @@ -399,6 +513,29 @@ Can be used to lift some rate-limits by solving a captcha. Exception: IOErrorException +updateProfile(name, about, aboutEmoji , avatar, remove) -> <>:: +updateProfile(givenName, familyName, about, aboutEmoji , avatar, remove) -> <>:: +* name : Name for your own profile (empty if unchanged) +* givenName : Given name for your own profile (empty if unchanged) +* familyName : Family name for your own profile (empty if unchanged) +* about : About message for profile (empty if unchanged) +* aboutEmoji : Emoji for profile (empty if unchanged) +* avatar : Filename of avatar picture for profile (empty if unchanged) +* remove : Set to true if the existing avatar picture should be removed + +Exceptions: Failure + +uploadStickerPack(stickerPackPath) -> url:: +* stickerPackPath : Path to the manifest.json file or a zip file in the same directory +* url : URL of sticker pack after successful upload + +Exceptions: Failure + +version() -> version:: +* version : Version string of signal-cli + +Exceptions: None + == Signals SyncMessageReceived (timestamp, sender, destination, groupId, message, attachments):: * timestamp : Integer value that can be used to associate this e.g. with a sendMessage() From 15c66684c10bec8650b511decf2f86251f671050 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 12 Oct 2021 18:25:03 +0200 Subject: [PATCH 0858/2005] Update graalvm buildtools --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 51b2ef75..5d18e9e0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ plugins { application eclipse `check-lib-versions` - id("org.graalvm.buildtools.native") version "0.9.5" + id("org.graalvm.buildtools.native") version "0.9.6" } version = "0.9.0" From e977f38bdd9d955eafe557d1135d36fdf3056306 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 12 Oct 2021 20:48:56 +0200 Subject: [PATCH 0859/2005] Refactor to remove ProfileKeyProvider and UnidentifiedAccessSenderCertificateProvider --- .../org/asamk/signal/manager/ManagerImpl.java | 25 ++-------- .../signal/manager/helper/ProfileHelper.java | 5 +- .../manager/helper/ProfileKeyProvider.java | 9 ---- .../helper/UnidentifiedAccessHelper.java | 46 +++++++++++++------ ...tifiedAccessSenderCertificateProvider.java | 6 --- 5 files changed, 38 insertions(+), 53 deletions(-) delete mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java delete mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 8ccab36b..f88469ac 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -176,14 +176,13 @@ public class ManagerImpl implements Manager { this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); - final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, - account.getProfileStore()::getProfileKey, - this::getRecipientProfile, - this::getSenderCertificate); + final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account, + dependencies, + account::getProfileKey, + this::getRecipientProfile); this.profileHelper = new ProfileHelper(account, dependencies, avatarStore, - account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, this::resolveSignalServiceAddress); final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, @@ -782,22 +781,6 @@ public class ManagerImpl implements Manager { } } - private byte[] getSenderCertificate() { - byte[] certificate; - try { - if (account.isPhoneNumberShared()) { - certificate = dependencies.getAccountManager().getSenderCertificate(); - } else { - certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy(); - } - } catch (IOException e) { - logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); - return null; - } - // TODO cache for a day - return certificate; - } - private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException { final var address = resolveSignalServiceAddress(recipientId); if (!address.getNumber().isPresent()) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index e24d41fa..7a492d9f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -45,7 +45,6 @@ public final class ProfileHelper { private final SignalAccount account; private final SignalDependencies dependencies; private final AvatarStore avatarStore; - private final ProfileKeyProvider profileKeyProvider; private final UnidentifiedAccessProvider unidentifiedAccessProvider; private final SignalServiceAddressResolver addressResolver; @@ -53,14 +52,12 @@ public final class ProfileHelper { final SignalAccount account, final SignalDependencies dependencies, final AvatarStore avatarStore, - final ProfileKeyProvider profileKeyProvider, final UnidentifiedAccessProvider unidentifiedAccessProvider, final SignalServiceAddressResolver addressResolver ) { this.account = account; this.dependencies = dependencies; this.avatarStore = avatarStore; - this.profileKeyProvider = profileKeyProvider; this.unidentifiedAccessProvider = unidentifiedAccessProvider; this.addressResolver = addressResolver; } @@ -296,7 +293,7 @@ public final class ProfileHelper { RecipientId recipientId, SignalServiceProfile.RequestType requestType ) { var unidentifiedAccess = getUnidentifiedAccess(recipientId); - var profileKey = Optional.fromNullable(profileKeyProvider.getProfileKey(recipientId)); + var profileKey = Optional.fromNullable(account.getProfileStore().getProfileKey(recipientId)); final var address = addressResolver.resolveSignalServiceAddress(recipientId); return retrieveProfile(address, profileKey, unidentifiedAccess, requestType); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java deleted file mode 100644 index b98d674e..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileKeyProvider.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.asamk.signal.manager.helper; - -import org.asamk.signal.manager.storage.recipients.RecipientId; -import org.signal.zkgroup.profiles.ProfileKey; - -public interface ProfileKeyProvider { - - ProfileKey getProfileKey(RecipientId address); -} diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java index 87e23c1b..661a7f96 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessHelper.java @@ -1,11 +1,16 @@ package org.asamk.signal.manager.helper; +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.signal.libsignal.metadata.certificate.InvalidCertificateException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess; import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; +import java.io.IOException; import java.util.List; import java.util.stream.Collectors; @@ -13,24 +18,39 @@ import static org.whispersystems.signalservice.internal.util.Util.getSecretBytes public class UnidentifiedAccessHelper { + private final static Logger logger = LoggerFactory.getLogger(UnidentifiedAccessHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; private final SelfProfileKeyProvider selfProfileKeyProvider; - - private final ProfileKeyProvider profileKeyProvider; - private final ProfileProvider profileProvider; - private final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider; - public UnidentifiedAccessHelper( + final SignalAccount account, + final SignalDependencies dependencies, final SelfProfileKeyProvider selfProfileKeyProvider, - final ProfileKeyProvider profileKeyProvider, - final ProfileProvider profileProvider, - final UnidentifiedAccessSenderCertificateProvider senderCertificateProvider + final ProfileProvider profileProvider ) { + this.account = account; + this.dependencies = dependencies; this.selfProfileKeyProvider = selfProfileKeyProvider; - this.profileKeyProvider = profileKeyProvider; this.profileProvider = profileProvider; - this.senderCertificateProvider = senderCertificateProvider; + } + + private byte[] getSenderCertificate() { + byte[] certificate; + try { + if (account.isPhoneNumberShared()) { + certificate = dependencies.getAccountManager().getSenderCertificate(); + } else { + certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy(); + } + } catch (IOException e) { + logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); + return null; + } + // TODO cache for a day + return certificate; } private byte[] getSelfUnidentifiedAccessKey() { @@ -45,7 +65,7 @@ public class UnidentifiedAccessHelper { switch (targetProfile.getUnidentifiedAccessMode()) { case ENABLED: - var theirProfileKey = profileKeyProvider.getProfileKey(recipient); + var theirProfileKey = account.getProfileStore().getProfileKey(recipient); if (theirProfileKey == null) { return null; } @@ -60,7 +80,7 @@ public class UnidentifiedAccessHelper { public Optional getAccessForSync() { var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); - var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate(); + var selfUnidentifiedAccessCertificate = getSenderCertificate(); if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) { return Optional.absent(); @@ -82,7 +102,7 @@ public class UnidentifiedAccessHelper { public Optional getAccessFor(RecipientId recipient) { var recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient); var selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(); - var selfUnidentifiedAccessCertificate = senderCertificateProvider.getSenderCertificate(); + var selfUnidentifiedAccessCertificate = getSenderCertificate(); if (recipientUnidentifiedAccessKey == null || selfUnidentifiedAccessKey == null diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java b/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java deleted file mode 100644 index b0597346..00000000 --- a/lib/src/main/java/org/asamk/signal/manager/helper/UnidentifiedAccessSenderCertificateProvider.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.asamk.signal.manager.helper; - -public interface UnidentifiedAccessSenderCertificateProvider { - - byte[] getSenderCertificate(); -} From 997b3c6a2a1f2e9a50fe579ac726677ea0d57d0c Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 12 Oct 2021 20:49:41 +0200 Subject: [PATCH 0860/2005] Restrict blocking of group to master device --- lib/src/main/java/org/asamk/signal/manager/Manager.java | 2 +- lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java | 5 ++++- src/main/java/org/asamk/signal/commands/BlockCommand.java | 2 ++ src/main/java/org/asamk/signal/commands/UnblockCommand.java | 2 ++ src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 4 ++++ 5 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index f529b408..3cfeb4f7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -184,7 +184,7 @@ public interface Manager extends Closeable { void setGroupBlocked( GroupId groupId, boolean blocked - ) throws GroupNotFoundException, IOException; + ) throws GroupNotFoundException, IOException, NotMasterDeviceException; void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index f88469ac..a14f2d1f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -714,7 +714,10 @@ public class ManagerImpl implements Manager { @Override public void setGroupBlocked( final GroupId groupId, final boolean blocked - ) throws GroupNotFoundException, IOException { + ) throws GroupNotFoundException, IOException, NotMasterDeviceException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } groupHelper.setGroupBlocked(groupId, blocked); // TODO cycle our profile key syncHelper.sendBlockedList(); diff --git a/src/main/java/org/asamk/signal/commands/BlockCommand.java b/src/main/java/org/asamk/signal/commands/BlockCommand.java index 516224f5..1ec1036c 100644 --- a/src/main/java/org/asamk/signal/commands/BlockCommand.java +++ b/src/main/java/org/asamk/signal/commands/BlockCommand.java @@ -52,6 +52,8 @@ public class BlockCommand implements JsonRpcLocalCommand { for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) { try { m.setGroupBlocked(groupId, true); + } catch (NotMasterDeviceException e) { + throw new UserErrorException("This command doesn't work on linked devices."); } catch (GroupNotFoundException e) { logger.warn("Group not found {}: {}", groupId.toBase64(), e.getMessage()); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/commands/UnblockCommand.java b/src/main/java/org/asamk/signal/commands/UnblockCommand.java index 7cf209fa..53eab3b3 100644 --- a/src/main/java/org/asamk/signal/commands/UnblockCommand.java +++ b/src/main/java/org/asamk/signal/commands/UnblockCommand.java @@ -51,6 +51,8 @@ public class UnblockCommand implements JsonRpcLocalCommand { for (var groupId : CommandUtil.getGroupIds(groupIdStrings)) { try { m.setGroupBlocked(groupId, false); + } catch (NotMasterDeviceException e) { + throw new UserErrorException("This command doesn't work on linked devices."); } catch (GroupNotFoundException e) { logger.warn("Unknown group id: {}", groupId); } catch (IOException e) { diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 56ecdc55..2cf3c813 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -405,6 +405,8 @@ public class DbusSignalImpl implements Signal { public void setGroupBlocked(final byte[] groupId, final boolean blocked) { try { m.setGroupBlocked(getGroupId(groupId), blocked); + } catch (NotMasterDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException e) { @@ -1060,6 +1062,8 @@ public class DbusSignalImpl implements Signal { private void setIsBlocked(final boolean isBlocked) { try { m.setGroupBlocked(groupId, isBlocked); + } catch (NotMasterDeviceException e) { + throw new Error.Failure("This command doesn't work on linked devices."); } catch (GroupNotFoundException e) { throw new Error.GroupNotFound(e.getMessage()); } catch (IOException e) { From f094cd6806aae68fba9ebd0cf29eaf4eabf042d2 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 12 Oct 2021 22:14:39 +0200 Subject: [PATCH 0861/2005] Extract IdentityHelper --- .../org/asamk/signal/manager/Manager.java | 3 - .../org/asamk/signal/manager/ManagerImpl.java | 99 +++---------- .../signal/manager/helper/IdentityHelper.java | 135 ++++++++++++++++++ .../asamk/signal/ReceiveMessageHandler.java | 4 - .../asamk/signal/dbus/DbusManagerImpl.java | 8 -- 5 files changed, 151 insertions(+), 98 deletions(-) create mode 100644 lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 3cfeb4f7..733e3dcc 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -22,7 +22,6 @@ import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.recipients.Contact; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; -import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -228,8 +227,6 @@ public interface Manager extends Closeable { boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient); - String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address); @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index a14f2d1f..d2ffaaab 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -38,6 +38,7 @@ import org.asamk.signal.manager.helper.AttachmentHelper; import org.asamk.signal.manager.helper.ContactHelper; import org.asamk.signal.manager.helper.GroupHelper; import org.asamk.signal.manager.helper.GroupV2Helper; +import org.asamk.signal.manager.helper.IdentityHelper; import org.asamk.signal.manager.helper.IncomingMessageHandler; import org.asamk.signal.manager.helper.PinHelper; import org.asamk.signal.manager.helper.PreKeyHelper; @@ -59,15 +60,10 @@ import org.asamk.signal.manager.storage.stickers.Sticker; import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.StickerUtils; -import org.asamk.signal.manager.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.ecc.ECPublicKey; -import org.whispersystems.libsignal.fingerprint.Fingerprint; -import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; -import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.SignalSessionLock; @@ -98,9 +94,7 @@ import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.SignatureException; -import java.util.Arrays; import java.util.Collection; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -112,7 +106,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Function; import java.util.stream.Collectors; import static org.asamk.signal.manager.config.ServiceConfig.capabilities; @@ -138,6 +131,7 @@ public class ManagerImpl implements Manager { private final ContactHelper contactHelper; private final IncomingMessageHandler incomingMessageHandler; private final PreKeyHelper preKeyHelper; + private final IdentityHelper identityHelper; private final Context context; private boolean hasCaughtUpWithOldMessages = false; @@ -238,6 +232,11 @@ public class ManagerImpl implements Manager { syncHelper, this::getRecipientProfile, jobExecutor); + this.identityHelper = new IdentityHelper(account, + dependencies, + this::resolveSignalServiceAddress, + syncHelper, + profileHelper); } @Override @@ -1042,7 +1041,7 @@ public class ManagerImpl implements Manager { return toGroup(groupHelper.getGroup(groupId)); } - public GroupInfo getGroupInfo(GroupId groupId) { + private GroupInfo getGroupInfo(GroupId groupId) { return groupHelper.getGroup(groupId); } @@ -1063,8 +1062,9 @@ public class ManagerImpl implements Manager { final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId()); return new Identity(address, identityInfo.getIdentityKey(), - computeSafetyNumber(address.toSignalServiceAddress(), identityInfo.getIdentityKey()), - computeSafetyNumberForScanning(address.toSignalServiceAddress(), identityInfo.getIdentityKey()), + identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()), + identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(), + identityInfo.getIdentityKey()).getSerialized(), identityInfo.getTrustLevel(), identityInfo.getDateAdded()); } @@ -1094,9 +1094,7 @@ public class ManagerImpl implements Manager { } catch (UnregisteredUserException e) { return false; } - return trustIdentity(recipientId, - identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), - TrustLevel.TRUSTED_VERIFIED); + return identityHelper.trustIdentityVerified(recipientId, fingerprint); } /** @@ -1113,10 +1111,7 @@ public class ManagerImpl implements Manager { } catch (UnregisteredUserException e) { return false; } - var address = resolveSignalServiceAddress(recipientId); - return trustIdentity(recipientId, - identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), - TrustLevel.TRUSTED_VERIFIED); + return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); } /** @@ -1133,15 +1128,7 @@ public class ManagerImpl implements Manager { } catch (UnregisteredUserException e) { return false; } - var address = resolveSignalServiceAddress(recipientId); - return trustIdentity(recipientId, identityKey -> { - final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); - try { - return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber); - } catch (FingerprintVersionMismatchException | FingerprintParsingException e) { - return false; - } - }, TrustLevel.TRUSTED_VERIFIED); + return identityHelper.trustIdentityVerifiedSafetyNumber(recipientId, safetyNumber); } /** @@ -1157,66 +1144,13 @@ public class ManagerImpl implements Manager { } catch (UnregisteredUserException e) { return false; } - return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); - } - - private boolean trustIdentity( - RecipientId recipientId, Function verifier, TrustLevel trustLevel - ) { - var identity = account.getIdentityKeyStore().getIdentity(recipientId); - if (identity == null) { - return false; - } - - if (!verifier.apply(identity.getIdentityKey())) { - return false; - } - - account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); - try { - var address = resolveSignalServiceAddress(recipientId); - syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); - } catch (IOException e) { - logger.warn("Failed to send verification sync message: {}", e.getMessage()); - } - - return true; + return identityHelper.trustIdentityAllKeys(recipientId); } private void handleIdentityFailure( final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure ) { - final var identityKey = identityFailure.getIdentityKey(); - if (identityKey != null) { - final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); - if (newIdentity) { - account.getSessionStore().archiveSessions(recipientId); - } - } else { - // Retrieve profile to get the current identity key from the server - profileHelper.refreshRecipientProfile(recipientId); - } - } - - @Override - public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); - } - - private byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); - } - - private Fingerprint computeSafetyNumberFingerprint( - final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey - ) { - return Utils.computeSafetyNumber(capabilities.isUuid(), - account.getSelfAddress(), - account.getIdentityKeyPair().getPublicKey(), - theirAddress, - theirIdentityKey); + this.identityHelper.handleIdentityFailure(recipientId, identityFailure); } @Override @@ -1291,5 +1225,4 @@ public class ManagerImpl implements Manager { } account = null; } - } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java new file mode 100644 index 00000000..531870d9 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IdentityHelper.java @@ -0,0 +1,135 @@ +package org.asamk.signal.manager.helper; + +import org.asamk.signal.manager.SignalDependencies; +import org.asamk.signal.manager.TrustLevel; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.recipients.RecipientId; +import org.asamk.signal.manager.util.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.fingerprint.Fingerprint; +import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; +import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; +import org.whispersystems.libsignal.fingerprint.ScannableFingerprint; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.function.Function; + +import static org.asamk.signal.manager.config.ServiceConfig.capabilities; + +public class IdentityHelper { + + private final static Logger logger = LoggerFactory.getLogger(IdentityHelper.class); + + private final SignalAccount account; + private final SignalDependencies dependencies; + private final SignalServiceAddressResolver addressResolver; + private final SyncHelper syncHelper; + private final ProfileHelper profileHelper; + + public IdentityHelper( + final SignalAccount account, + final SignalDependencies dependencies, + final SignalServiceAddressResolver addressResolver, + final SyncHelper syncHelper, + final ProfileHelper profileHelper + ) { + this.account = account; + this.dependencies = dependencies; + this.addressResolver = addressResolver; + this.syncHelper = syncHelper; + this.profileHelper = profileHelper; + } + + public boolean trustIdentityVerified(RecipientId recipientId, byte[] fingerprint) { + return trustIdentity(recipientId, + identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), + TrustLevel.TRUSTED_VERIFIED); + } + + public boolean trustIdentityVerifiedSafetyNumber(RecipientId recipientId, String safetyNumber) { + return trustIdentity(recipientId, + identityKey -> safetyNumber.equals(computeSafetyNumber(recipientId, identityKey)), + TrustLevel.TRUSTED_VERIFIED); + } + + public boolean trustIdentityVerifiedSafetyNumber(RecipientId recipientId, byte[] safetyNumber) { + return trustIdentity(recipientId, identityKey -> { + final var fingerprint = computeSafetyNumberForScanning(recipientId, identityKey); + try { + return fingerprint != null && fingerprint.compareTo(safetyNumber); + } catch (FingerprintVersionMismatchException | FingerprintParsingException e) { + return false; + } + }, TrustLevel.TRUSTED_VERIFIED); + } + + public boolean trustIdentityAllKeys(RecipientId recipientId) { + return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); + } + + public String computeSafetyNumber(RecipientId recipientId, IdentityKey theirIdentityKey) { + var address = addressResolver.resolveSignalServiceAddress(recipientId); + final Fingerprint fingerprint = computeSafetyNumberFingerprint(address, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); + } + + public ScannableFingerprint computeSafetyNumberForScanning(RecipientId recipientId, IdentityKey theirIdentityKey) { + var address = addressResolver.resolveSignalServiceAddress(recipientId); + final Fingerprint fingerprint = computeSafetyNumberFingerprint(address, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getScannableFingerprint(); + } + + private Fingerprint computeSafetyNumberFingerprint( + final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey + ) { + return Utils.computeSafetyNumber(capabilities.isUuid(), + account.getSelfAddress(), + account.getIdentityKeyPair().getPublicKey(), + theirAddress, + theirIdentityKey); + } + + private boolean trustIdentity( + RecipientId recipientId, Function verifier, TrustLevel trustLevel + ) { + var identity = account.getIdentityKeyStore().getIdentity(recipientId); + if (identity == null) { + return false; + } + + if (!verifier.apply(identity.getIdentityKey())) { + return false; + } + + account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); + try { + var address = addressResolver.resolveSignalServiceAddress(recipientId); + syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); + } catch (IOException e) { + logger.warn("Failed to send verification sync message: {}", e.getMessage()); + } + + return true; + } + + public void handleIdentityFailure( + final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure + ) { + final var identityKey = identityFailure.getIdentityKey(); + if (identityKey != null) { + final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } + } else { + // Retrieve profile to get the current identity key from the server + profileHelper.refreshRecipientProfile(recipientId); + } + } +} diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index 35790678..b1580b34 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -6,7 +6,6 @@ import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.util.DateUtils; -import org.asamk.signal.util.Util; import org.slf4j.helpers.MessageFormatter; import org.whispersystems.libsignal.protocol.DecryptionErrorMessage; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; @@ -377,9 +376,6 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { writer.println("Received sync message with verified identities:"); final var verifiedMessage = syncMessage.getVerified().get(); writer.println("- {}: {}", formatContact(verifiedMessage.getDestination()), verifiedMessage.getVerified()); - var safetyNumber = Util.formatSafetyNumber(m.computeSafetyNumber(verifiedMessage.getDestination(), - verifiedMessage.getIdentityKey())); - writer.indentedWriter().println(safetyNumber); } if (syncMessage.getConfiguration().isPresent()) { writer.println("Received sync message with configuration:"); diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 6e655bdc..59422e69 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -30,7 +30,6 @@ import org.freedesktop.dbus.DBusPath; import org.freedesktop.dbus.connections.impl.DBusConnection; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.interfaces.DBusInterface; -import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; @@ -538,13 +537,6 @@ public class DbusManagerImpl implements Manager { throw new UnsupportedOperationException(); } - @Override - public String computeSafetyNumber( - final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey - ) { - throw new UnsupportedOperationException(); - } - @Override public SignalServiceAddress resolveSignalServiceAddress(final SignalServiceAddress address) { return address; From 18ad9fbb4ec06b20be9043ae462997b266b8de1e Mon Sep 17 00:00:00 2001 From: John Freed Date: Wed, 13 Oct 2021 08:00:07 +0200 Subject: [PATCH 0862/2005] fix typos in DBus doc (#774) and expand on functioning of joinGroup method --- man/signal-cli-dbus.5.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index f38afc93..55058580 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -129,7 +129,7 @@ Exceptions: None, if the group name is not found an empty array is returned joinGroup(inviteURI) -> <>:: * inviteURI : String starting with https://signal.group/# -If the link requires admin approval, this adds you to the requesting members list. Otherwise, this adds you to the pending members list. +Behavior of this method depends on the `requirePermission` parameter of the `enableLink` method. If permission is required, `joinGroup` adds you to the requesting members list. Permission may be granted based on the group's `PermissionAddMember` property (`ONLY_ADMINS` or `EVERY_MEMBER`). If permission is not required, `joinGroup` admits you immediately to the group. Exceptions: Failure @@ -203,7 +203,7 @@ To get all properties, use: addAdmins(recipients) -> <>:: * recipients : String array of phone numbers -Grant admit privileges to recipients. +Grant admin privileges to recipients. Exceptions: Failure From 56487146413cdb98d9d70b3baf16032be2506691 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 Oct 2021 19:40:45 +0200 Subject: [PATCH 0863/2005] Clear queued message actions after handling Fixes #777 --- lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index d2ffaaab..668c0b13 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -939,9 +939,11 @@ public class ManagerImpl implements Manager { if (hasCaughtUpWithOldMessages) { handleQueuedActions(queuedActions); + queuedActions.clear(); } if (cachedMessage[0] != null) { if (exception instanceof UntrustedIdentityException) { + logger.debug("Keeping message with untrusted identity in message cache"); final var address = ((UntrustedIdentityException) exception).getSender(); final var recipientId = resolveRecipient(address); if (!envelope.hasSourceUuid()) { @@ -958,6 +960,7 @@ public class ManagerImpl implements Manager { } } handleQueuedActions(queuedActions); + queuedActions.clear(); } @Override @@ -966,6 +969,7 @@ public class ManagerImpl implements Manager { } private void handleQueuedActions(final Collection queuedActions) { + logger.debug("Handling message actions"); var interrupted = false; for (var action : queuedActions) { try { From 4a3b0e5124adde512741f45b708c3402abf6a883 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 Oct 2021 20:00:52 +0200 Subject: [PATCH 0864/2005] Reconnect websocket with exponential backof if connection is lost --- .../org/asamk/signal/manager/ManagerImpl.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 668c0b13..0deafc83 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -887,6 +887,8 @@ public class ManagerImpl implements Manager { signalWebSocket.connect(); hasCaughtUpWithOldMessages = false; + var backOffCounter = 0; + final var MAX_BACKOFF_COUNTER = 9; while (!Thread.interrupted()) { SignalServiceEnvelope envelope; @@ -901,6 +903,8 @@ public class ManagerImpl implements Manager { // store message on disk, before acknowledging receipt to the server cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); }); + backOffCounter = 0; + if (result.isPresent()) { envelope = result.get(); logger.debug("New message received from server"); @@ -924,11 +928,24 @@ public class ManagerImpl implements Manager { } else { throw e; } - } catch (WebSocketUnavailableException e) { - logger.debug("Pipe unexpectedly unavailable, connecting"); - signalWebSocket.connect(); - continue; + } catch (IOException e) { + logger.debug("Pipe unexpectedly unavailable: {}", e.getMessage()); + if (e instanceof WebSocketUnavailableException || "Connection closed!".equals(e.getMessage())) { + final var sleepMilliseconds = 100 * (long) Math.pow(2, backOffCounter); + backOffCounter = Math.min(backOffCounter + 1, MAX_BACKOFF_COUNTER); + logger.warn("Connection closed unexpectedly, reconnecting in {} ms", sleepMilliseconds); + try { + Thread.sleep(sleepMilliseconds); + } catch (InterruptedException interruptedException) { + return; + } + hasCaughtUpWithOldMessages = false; + signalWebSocket.connect(); + continue; + } + throw e; } catch (TimeoutException e) { + backOffCounter = 0; if (returnOnTimeout) return; continue; } From ea7f4845e8468dd64819040378c267f94e5d1441 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 Oct 2021 20:43:25 +0200 Subject: [PATCH 0865/2005] Update libsignal-service-java --- lib/build.gradle.kts | 2 +- .../main/java/org/asamk/signal/manager/config/LiveConfig.java | 2 +- .../java/org/asamk/signal/manager/config/SandboxConfig.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index e4e7dc20..57c80c01 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -14,7 +14,7 @@ repositories { } dependencies { - api("com.github.turasa:signal-service-java:2.15.3_unofficial_29") + api("com.github.turasa:signal-service-java:2.15.3_unofficial_30") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.slf4j:slf4j-api:1.7.30") diff --git a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java index 177f6697..0da1f2f9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/LiveConfig.java @@ -46,7 +46,7 @@ class LiveConfig { private final static Optional proxy = Optional.absent(); private final static byte[] zkGroupServerPublicParams = Base64.getDecoder() - .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0="); + .decode("AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY"); static SignalServiceConfiguration createDefaultServiceConfiguration( final List interceptors diff --git a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java index d643f10a..975c95d3 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/SandboxConfig.java @@ -46,7 +46,7 @@ class SandboxConfig { private final static Optional proxy = Optional.absent(); private final static byte[] zkGroupServerPublicParams = Base64.getDecoder() - .decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls="); + .decode("ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARB"); static SignalServiceConfiguration createDefaultServiceConfiguration( final List interceptors From 3b685190a807dc1e91f169be716055a84e9ef7d0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 Oct 2021 21:00:21 +0200 Subject: [PATCH 0866/2005] Add missing unexport groups call --- src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java index 2cf3c813..ae7fc0de 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusSignalImpl.java @@ -78,6 +78,7 @@ public class DbusSignalImpl implements Signal { public void close() { unExportDevices(); + unExportGroups(); } @Override From 1c27723083ed98eba054794dbab5a6ed8e7c9d1b Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 Oct 2021 21:01:37 +0200 Subject: [PATCH 0867/2005] Update build pipeline to java 17 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28f5039e..1fb5f7f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '11', '16' ] + java: [ '11', '17' ] steps: - uses: actions/checkout@v1 From 0e56d1c32a266eeaa98f002e287e5a7a0cc3e2c5 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 Oct 2021 21:18:18 +0200 Subject: [PATCH 0868/2005] Update reflect-config --- graalvm-config-dir/reflect-config.json | 17 +++++++++++++++++ run_tests.sh | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 819eafa7..945da972 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -1491,6 +1491,12 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true }, +{ + "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfile$Badge", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, { "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfile$Capabilities", "allDeclaredFields":true, @@ -2181,6 +2187,17 @@ {"name":"uuids_"} ] }, +{ + "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Configuration", + "fields":[ + {"name":"bitField0_"}, + {"name":"linkPreviews_"}, + {"name":"provisioningVersion_"}, + {"name":"readReceipts_"}, + {"name":"typingIndicators_"}, + {"name":"unidentifiedDeliveryIndicators_"} + ] +}, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Contacts", "fields":[ diff --git a/run_tests.sh b/run_tests.sh index d306dfa9..7ed88da9 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -15,7 +15,7 @@ fi NUMBER_1="$1" NUMBER_2="$2" TEST_PIN_1=456test_pin_foo123 -NATIVE=1 +NATIVE=0 PATH_TEST_CONFIG="$PWD/build/test-config" PATH_MAIN="$PATH_TEST_CONFIG/main" From cf31ad6ccf209f3d69f5ee9de4349f9141e60a4b Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 Oct 2021 21:18:47 +0200 Subject: [PATCH 0869/2005] Check if configuration message contains value before using it --- .../manager/helper/IncomingMessageHandler.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 16f47d3c..47aa6156 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -358,11 +358,19 @@ public final class IncomingMessageHandler { if (syncMessage.getConfiguration().isPresent()) { final var configurationMessage = syncMessage.getConfiguration().get(); final var configurationStore = account.getConfigurationStore(); - configurationStore.setReadReceipts(configurationMessage.getReadReceipts().orNull()); - configurationStore.setLinkPreviews(configurationMessage.getLinkPreviews().orNull()); - configurationStore.setTypingIndicators(configurationMessage.getTypingIndicators().orNull()); - configurationStore.setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators() - .orNull()); + if (configurationMessage.getReadReceipts().isPresent()) { + configurationStore.setReadReceipts(configurationMessage.getReadReceipts().get()); + } + if (configurationMessage.getLinkPreviews().isPresent()) { + configurationStore.setLinkPreviews(configurationMessage.getLinkPreviews().get()); + } + if (configurationMessage.getTypingIndicators().isPresent()) { + configurationStore.setTypingIndicators(configurationMessage.getTypingIndicators().get()); + } + if (configurationMessage.getUnidentifiedDeliveryIndicators().isPresent()) { + configurationStore.setUnidentifiedDeliveryIndicators(configurationMessage.getUnidentifiedDeliveryIndicators() + .get()); + } } return actions; } From f57db857da1033d8bb65a9e9e80c3d9be1d75518 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 15 Oct 2021 22:36:57 +0200 Subject: [PATCH 0870/2005] Update CHANGELOG.md --- CHANGELOG.md | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 157 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b96784..ab4bb65d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ # Changelog ## [Unreleased] +**Attention**: Now requires native libzkgroup version 0.8 + +### Added +- New command `updateConfiguration` which allows setting configurations for linked devices +- Improved dbus daemon for group handling, groups are now exported as separate dbus objects +- Linked devices can be managed via dbus +- New dbus methods sendTyping and sendReadReceipt (Thanks @JtheSaw) +- New dbus methods submitRateLimitChallenge, isRegistered, listDevices, setExpirationTimer, sendContacts, sendSyncRequest, uploadStickerPack, setPin and removePin (Thanks @John Freed) +- New dbus method getSelfNumber + +### Fixed +- Do not send message resend request to own device +- Allow message from pending member to accept group invitations +- Fix issue which could cause signal-cli to repeatedly sending the same delivery receipts +- Reconnect websocket after connection loss + +### Changed +- Use new provisioning URL `sgnl://linkdevice` instead of `tsdevice:/` +- The gradle command to build a graalvm native image is now `./gradlew nativeCompile` ## [0.9.0] - 2021-09-12 **Attention**: Now requires native libsignal-client version 0.9 @@ -189,6 +208,142 @@ See https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal fo ### Fixed - Issue where some messages were sent with an old counter index -## Older +## [0.6.11] - 2020-10-14 +- Fix issue with receiving message reactions -Look at the [release tags](https://github.com/AsamK/signal-cli/releases) for information about older releases. +## [0.6.10] - 2020-09-11 +- Fix issue when retrieving profiles +- Workaround issue with libzkgroup on platforms other than linux x86_64 + +## [0.6.9] - 2020-09-10 +- Minor bug fixes and improvements +- dbus functionality now works on FreeBSD +- signal-cli now requires Java 11 + +**Warning: this version only works on Linux x86_64, will be fixed in 0.6.10** + +## [0.6.8] - 2020-05-22 +- Switch to hypfvieh dbus-java, which doesn't require a native library anymore (drops requirement of libmatthew-unix-java) +- Bugfixes for messages with uuids +- Add `--expiration` parameter to `updateContact` command to set expiration timer + +## [0.6.7] - 2020-04-03 +- Send command now returns the timestamp of the sent message +- DBus daemon: Publish received sync message to SyncMessageReceived signal +- Fix issue with resolving e164/uuid addresses for sessions +- Fix pack key length for sticker upload + +## [0.6.6] - 2020-03-29 +- Added listContacts command +- Added block/unblock commands to block contacts and groups +- Added uploadStickerPack command to upload sticker packs (see man page for more details) +- Full support for sending and receiving unidentified sender messages +- Support for message reactions with emojis +- Internal: support recipients with uuids + +## [0.6.5] - 2019-11-11 +Supports receiving messages sent with unidentified sender + +## [0.6.4] - 2019-11-02 +- Fix rounding error for attachment ids in json output +- Add additional info to json output +- Add commands to update profile name and avatar +- Add command to update contact names + +## [0.6.3] - 2019-09-05 +Bug fixes and small improvements + +## [0.6.2] - 2018-12-16 +- Fixes sending of group messages + +## [0.6.1] - 2018-12-09 +- Added getGroupIds dbus command +- Use "NativePRNG" pseudo random number generator, if available +- Switch default data path: + `$XDG_DATA_HOME/signal-cli` (`$HOME/.local/share/signal-cli`) + Existing data paths will continue to work (used as fallback) + +## [0.6.0] - 2018-05-03 +- Simple json output +- dbus signal for receiving messages +- Registration lock PIN +- Output quoted message + +## [0.5.6] - 2017-06-16 +* new listGroups command +* Support for attachments with file names +* Support for complete contacts sync +* Support for contact verification sync +* DBus interface: + * Get/Set group info + * Get/Set contact info + +## [0.5.5] - 2017-02-18 +- fix receiving messages on linked devices +- add unregister command + +## [0.5.4] - 2017-02-17 +- Fix linking of new devices + +## [0.5.3] - 2017-01-29 +* New commandline paramter for receive: --ignore-attachments +* Updated dependencies + +## [0.5.2] - 2016-12-16 +- Add support for group info requests +- Improve closing of file streams + +## [0.5.1] - 2016-11-18 +- Support new safety numbers (https://whispersystems.org/blog/safety-number-updates/) +- Add a man page +- Support sending disappearing messages, if the recipient has activated it + +## [0.5.0] - 2016-08-29 +- Check if a number is registered on Signal, before adding it to a group +- Prevent sending to groups that the user has quit +- Commands to trust new identity keys (see README) +- Messages from untrusted identities are stored on disk and decrypted when the user trusts the identity +- Timestamps shown in ISO 8601 format + +## [0.4.1] - 2016-07-18 +- Fix issue with creating groups +- Lock config file to prevent parallel access by multiple instances of signal-cli +- Improve return codes, always return non-zero code, when sending failed + +## [0.4.0] - 2016-06-19 +- Linking to Signal-Desktop and Signal-Android is now possible (Provisioning) +- Added a contact store, mainly for syncing contacts with linked devices (editing not yet possible via cli) +- Avatars for groups and contacts are now stored (new folder "avatars" in the config path) + +## [0.3.1] - 2016-04-03 +- Fix running with Oracle JRE 8 +- Fix registering +- Fix unicode warning when compiling with non utf8 locale + +## [0.3.0] - 2016-04-02 +- Renamed textsecure-cli to signal-cli, following the rename of libtextsecure-java to libsignal-service-java +- The experimental dbus interface was also renamed to org.asamk.Signal +- Upload new prekeys to the server, when there are less than 20 left, prekeys are needed to create new sessions + +## [0.2.1] - 2016-02-10 +- Improve dbus service +- New command line argument --config to specify config directory + +## [0.2.0] - 2015-12-30 +Added an experimental dbus interface, for sending and receiving messages (The interface is unstable and may change with future releases). + +This release works with Java 7 and 8. + +## [0.1.0] - 2015-11-28 +Add support for creating/updating groups and sending to them + +## [0.0.5] - 2015-11-21 +- Add receive timeout commandline parameter +- Show message group info + +## [0.0.4] - 2015-09-22 + +## [0.0.3] - 2015-08-07 + +## [0.0.2] - 2015-07-08 +First release From f5089789fb964403abeceef3be0cd98523a77dd4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sat, 16 Oct 2021 10:05:41 +0200 Subject: [PATCH 0871/2005] Bump version --- CHANGELOG.md | 4 +++- build.gradle.kts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab4bb65d..463419b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] + +## [0.9.1] - 2021-10-16 **Attention**: Now requires native libzkgroup version 0.8 ### Added @@ -14,7 +16,7 @@ ### Fixed - Do not send message resend request to own device - Allow message from pending member to accept group invitations -- Fix issue which could cause signal-cli to repeatedly sending the same delivery receipts +- Fix issue which could cause signal-cli to repeatedly send the same delivery receipts - Reconnect websocket after connection loss ### Changed diff --git a/build.gradle.kts b/build.gradle.kts index 5d18e9e0..a419df8a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { id("org.graalvm.buildtools.native") version "0.9.6" } -version = "0.9.0" +version = "0.9.1" java { sourceCompatibility = JavaVersion.VERSION_11 From bff0030aed13173e6b90106730b60f09266c6fd7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 18 Oct 2021 16:47:26 +0200 Subject: [PATCH 0872/2005] Update reflect-config.json --- graalvm-config-dir/reflect-config.json | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index 945da972..b1939327 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -316,6 +316,16 @@ "allDeclaredMethods":true, "allDeclaredClasses":true }, +{ + "name":"org.asamk.Signal$Device", + "allDeclaredMethods":true, + "allDeclaredClasses":true +}, +{ + "name":"org.asamk.Signal$Group", + "allDeclaredMethods":true, + "allDeclaredClasses":true +}, { "name":"org.asamk.Signal$MessageReceived", "allDeclaredConstructors":true, @@ -326,11 +336,24 @@ "allDeclaredConstructors":true, "allPublicConstructors":true }, +{ + "name":"org.asamk.Signal$StructDevice", + "allDeclaredFields":true +}, +{ + "name":"org.asamk.Signal$StructGroup", + "allDeclaredFields":true +}, { "name":"org.asamk.Signal$SyncMessageReceived", "allDeclaredConstructors":true, "allPublicConstructors":true }, +{ + "name":"org.asamk.SignalControl", + "allDeclaredMethods":true, + "allDeclaredClasses":true +}, { "name":"org.asamk.signal.commands.GetUserStatusCommand$JsonUserStatus", "allDeclaredFields":true, @@ -1114,6 +1137,15 @@ "allDeclaredMethods":true, "allDeclaredClasses":true }, +{ + "name":"org.freedesktop.dbus.interfaces.Properties", + "allDeclaredMethods":true, + "allDeclaredClasses":true +}, +{ + "name":"org.freedesktop.dbus.interfaces.Properties$PropertiesChanged", + "allPublicConstructors":true +}, { "name":"org.objectweb.asm.util.TraceMethodVisitor" }, From d4b9356c5c05e36f217ad803d51b8808598d3ae7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 18 Oct 2021 16:48:07 +0200 Subject: [PATCH 0873/2005] Add missing null check Fixes #784 --- lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 0deafc83..cc90de5c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -1081,11 +1081,12 @@ public class ManagerImpl implements Manager { } final var address = account.getRecipientStore().resolveRecipientAddress(identityInfo.getRecipientId()); + final var scannableFingerprint = identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(), + identityInfo.getIdentityKey()); return new Identity(address, identityInfo.getIdentityKey(), identityHelper.computeSafetyNumber(identityInfo.getRecipientId(), identityInfo.getIdentityKey()), - identityHelper.computeSafetyNumberForScanning(identityInfo.getRecipientId(), - identityInfo.getIdentityKey()).getSerialized(), + scannableFingerprint == null ? null : scannableFingerprint.getSerialized(), identityInfo.getTrustLevel(), identityInfo.getDateAdded()); } From 3636023cb8f2202eb9b95f507b22ca3db2b6c3c8 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 19 Oct 2021 22:16:35 +0200 Subject: [PATCH 0874/2005] Improve error message when the last provisioning steps fail --- .../java/org/asamk/signal/manager/ProvisioningManager.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 226de9be..c7876569 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -134,16 +134,15 @@ public class ProvisioningManager { try { m.refreshPreKeys(); } catch (Exception e) { - logger.error("Failed to check new account state."); - throw e; + logger.error("Failed to refresh pre keys."); } logger.debug("Requesting sync data"); try { m.requestAllSyncData(); } catch (Exception e) { - logger.error("Failed to request sync messages from linked device."); - throw e; + logger.error( + "Failed to request sync messages from linked device, data can be requested again with `sendSyncRequest`."); } final var result = m; From f5ba7894ae06909baeebfe9c23ccb9ab6b00a147 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 21 Oct 2021 21:01:48 +0200 Subject: [PATCH 0875/2005] Add setIgnoreAttachments method --- .../org/asamk/signal/manager/Manager.java | 8 +++----- .../org/asamk/signal/manager/ManagerImpl.java | 20 ++++++++++--------- .../asamk/signal/commands/DaemonCommand.java | 10 ++++++---- .../commands/JsonRpcDispatcherCommand.java | 9 ++++----- .../asamk/signal/commands/ReceiveCommand.java | 7 ++----- .../asamk/signal/dbus/DbusManagerImpl.java | 11 +++++----- 6 files changed, 32 insertions(+), 33 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index 733e3dcc..f70c4e29 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -194,13 +194,11 @@ public interface Manager extends Closeable { void requestAllSyncData() throws IOException; void receiveMessages( - long timeout, - TimeUnit unit, - boolean returnOnTimeout, - boolean ignoreAttachments, - ReceiveMessageHandler handler + long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler ) throws IOException; + void setIgnoreAttachments(boolean ignoreAttachments); + boolean hasCaughtUpWithOldMessages(); boolean isContactBlocked(RecipientIdentifier.Single recipient); diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index cc90de5c..bec7f521 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -135,6 +135,7 @@ public class ManagerImpl implements Manager { private final Context context; private boolean hasCaughtUpWithOldMessages = false; + private boolean ignoreAttachments = false; ManagerImpl( SignalAccount account, @@ -824,10 +825,10 @@ public class ManagerImpl implements Manager { return registeredUsers; } - private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { + private void retryFailedReceivedMessages(ReceiveMessageHandler handler) { Set queuedActions = new HashSet<>(); for (var cachedMessage : account.getMessageCache().getCachedMessages()) { - var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); + var actions = retryFailedReceivedMessage(handler, cachedMessage); if (actions != null) { queuedActions.addAll(actions); } @@ -836,7 +837,7 @@ public class ManagerImpl implements Manager { } private List retryFailedReceivedMessage( - final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage + final ReceiveMessageHandler handler, final CachedMessage cachedMessage ) { var envelope = cachedMessage.loadEnvelope(); if (envelope == null) { @@ -873,13 +874,9 @@ public class ManagerImpl implements Manager { @Override public void receiveMessages( - long timeout, - TimeUnit unit, - boolean returnOnTimeout, - boolean ignoreAttachments, - ReceiveMessageHandler handler + long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler ) throws IOException { - retryFailedReceivedMessages(handler, ignoreAttachments); + retryFailedReceivedMessages(handler); Set queuedActions = new HashSet<>(); @@ -980,6 +977,11 @@ public class ManagerImpl implements Manager { queuedActions.clear(); } + @Override + public void setIgnoreAttachments(final boolean ignoreAttachments) { + this.ignoreAttachments = ignoreAttachments; + } + @Override public boolean hasCaughtUpWithOldMessages() { return hasCaughtUpWithOldMessages; diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 02063b87..9997f56a 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -55,6 +55,7 @@ public class DaemonCommand implements MultiLocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); + m.setIgnoreAttachments(ignoreAttachments); DBusConnection.DBusBusType busType; if (Boolean.TRUE.equals(ns.getBoolean("system"))) { @@ -65,7 +66,7 @@ public class DaemonCommand implements MultiLocalCommand { try (var conn = DBusConnection.getConnection(busType)) { var objectPath = DbusConfig.getObjectPath(); - var t = run(conn, objectPath, m, outputWriter, ignoreAttachments); + var t = run(conn, objectPath, m, outputWriter); conn.requestBusName(DbusConfig.getBusname()); @@ -94,9 +95,10 @@ public class DaemonCommand implements MultiLocalCommand { try (var conn = DBusConnection.getConnection(busType)) { final var signalControl = new DbusSignalControlImpl(c, m -> { + m.setIgnoreAttachments(ignoreAttachments); try { final var objectPath = DbusConfig.getObjectPath(m.getSelfNumber()); - return run(conn, objectPath, m, outputWriter, ignoreAttachments); + return run(conn, objectPath, m, outputWriter); } catch (DBusException e) { logger.error("Failed to export object", e); return null; @@ -118,7 +120,7 @@ public class DaemonCommand implements MultiLocalCommand { } private Thread run( - DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter, boolean ignoreAttachments + DBusConnection conn, String objectPath, Manager m, OutputWriter outputWriter ) throws DBusException { final var signal = new DbusSignalImpl(m, conn, objectPath); conn.exportObject(signal); @@ -133,7 +135,7 @@ public class DaemonCommand implements MultiLocalCommand { final var receiveMessageHandler = outputWriter instanceof JsonWriter ? new JsonDbusReceiveMessageHandler(m, (JsonWriter) outputWriter, conn, objectPath) : new DbusReceiveMessageHandler(m, (PlainTextWriter) outputWriter, conn, objectPath); - m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, receiveMessageHandler); + m.receiveMessages(1, TimeUnit.HOURS, false, receiveMessageHandler); break; } catch (IOException e) { logger.warn("Receiving messages failed, retrying", e); diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java index 9af67322..349bd0c4 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java @@ -66,6 +66,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { final boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); + m.setIgnoreAttachments(ignoreAttachments); final var objectMapper = Util.createJsonObjectMapper(); final var jsonRpcSender = new JsonRpcSender((JsonWriter) outputWriter); @@ -73,7 +74,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand { final var receiveThread = receiveMessages(s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification( "receive", objectMapper.valueToTree(s), - null)), m, ignoreAttachments); + null)), m); // Maybe this should be handled inside the Manager while (!m.hasCaughtUpWithOldMessages()) { @@ -167,14 +168,12 @@ public class JsonRpcDispatcherCommand implements LocalCommand { command.handleCommand(requestParams, m, outputWriter); } - private Thread receiveMessages( - JsonWriter jsonWriter, Manager m, boolean ignoreAttachments - ) { + private Thread receiveMessages(JsonWriter jsonWriter, Manager m) { final var thread = new Thread(() -> { while (!Thread.interrupted()) { try { final var receiveMessageHandler = new JsonReceiveMessageHandler(m, jsonWriter); - m.receiveMessages(1, TimeUnit.HOURS, false, ignoreAttachments, receiveMessageHandler); + m.receiveMessages(1, TimeUnit.HOURS, false, receiveMessageHandler); break; } catch (IOException e) { logger.warn("Receiving messages failed, retrying", e); diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 4686f26d..b4797be3 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -148,14 +148,11 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { timeout = 3600; } boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); + m.setIgnoreAttachments(ignoreAttachments); try { final var handler = outputWriter instanceof JsonWriter ? new JsonReceiveMessageHandler(m, (JsonWriter) outputWriter) : new ReceiveMessageHandler(m, (PlainTextWriter) outputWriter); - m.receiveMessages((long) (timeout * 1000), - TimeUnit.MILLISECONDS, - returnOnTimeout, - ignoreAttachments, - handler); + m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, handler); } catch (IOException e) { throw new IOErrorException("Error while receiving messages: " + e.getMessage(), e); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 59422e69..93d36888 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -425,15 +425,16 @@ public class DbusManagerImpl implements Manager { @Override public void receiveMessages( - final long timeout, - final TimeUnit unit, - final boolean returnOnTimeout, - final boolean ignoreAttachments, - final ReceiveMessageHandler handler + final long timeout, final TimeUnit unit, final boolean returnOnTimeout, final ReceiveMessageHandler handler ) throws IOException { throw new UnsupportedOperationException(); } + @Override + public void setIgnoreAttachments(final boolean ignoreAttachments) { + throw new UnsupportedOperationException(); + } + @Override public boolean hasCaughtUpWithOldMessages() { throw new UnsupportedOperationException(); From 430c155f7ee0a13b63cd37856e94853eb218f876 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 21 Oct 2021 21:02:02 +0200 Subject: [PATCH 0876/2005] Fix comment --- .../java/org/asamk/signal/commands/JsonRpcLocalCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java index 5b926732..47229538 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcLocalCommand.java @@ -32,7 +32,7 @@ public interface JsonRpcLocalCommand extends JsonRpcCommand> } /** - * Namepace implementation, that defaults booleans to false and converts camel case keys to dashed strings + * Namespace implementation, that has plural handling for list arguments and converts camel case keys to dashed strings */ final class JsonRpcNamespace extends Namespace { From 5c389c875d91bacba127d0e9cbdc1746b022e5aa Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 21 Oct 2021 21:19:14 +0200 Subject: [PATCH 0877/2005] Split receiveMessages method --- .../main/java/org/asamk/signal/manager/Manager.java | 12 +++++++++--- .../java/org/asamk/signal/manager/ManagerImpl.java | 11 ++++++++++- .../org/asamk/signal/commands/DaemonCommand.java | 3 +-- .../signal/commands/JsonRpcDispatcherCommand.java | 3 +-- .../org/asamk/signal/commands/ReceiveCommand.java | 11 +++++------ .../java/org/asamk/signal/dbus/DbusManagerImpl.java | 7 ++++++- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index f70c4e29..ac0cc02f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -193,9 +193,15 @@ public interface Manager extends Closeable { void requestAllSyncData() throws IOException; - void receiveMessages( - long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler - ) throws IOException; + /** + * Receive new messages from server, returns if no new message arrive in a timespan of timeout. + */ + void receiveMessages(long timeout, TimeUnit unit, ReceiveMessageHandler handler) throws IOException; + + /** + * Receive new messages from server, returns only if the thread is interrupted. + */ + void receiveMessages(ReceiveMessageHandler handler) throws IOException; void setIgnoreAttachments(boolean ignoreAttachments); diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index bec7f521..0421a401 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -873,7 +873,16 @@ public class ManagerImpl implements Manager { } @Override - public void receiveMessages( + public void receiveMessages(long timeout, TimeUnit unit, ReceiveMessageHandler handler) throws IOException { + receiveMessages(timeout, unit, true, handler); + } + + @Override + public void receiveMessages(ReceiveMessageHandler handler) throws IOException { + receiveMessages(1L, TimeUnit.HOURS, false, handler); + } + + private void receiveMessages( long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler ) throws IOException { retryFailedReceivedMessages(handler); diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 9997f56a..9627d9fb 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.List; -import java.util.concurrent.TimeUnit; public class DaemonCommand implements MultiLocalCommand { @@ -135,7 +134,7 @@ public class DaemonCommand implements MultiLocalCommand { final var receiveMessageHandler = outputWriter instanceof JsonWriter ? new JsonDbusReceiveMessageHandler(m, (JsonWriter) outputWriter, conn, objectPath) : new DbusReceiveMessageHandler(m, (PlainTextWriter) outputWriter, conn, objectPath); - m.receiveMessages(1, TimeUnit.HOURS, false, receiveMessageHandler); + m.receiveMessages(receiveMessageHandler); break; } catch (IOException e) { logger.warn("Receiving messages failed, retrying", e); diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java index 349bd0c4..2a95a880 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java @@ -33,7 +33,6 @@ import java.io.IOException; import java.io.InputStreamReader; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; public class JsonRpcDispatcherCommand implements LocalCommand { @@ -173,7 +172,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand { while (!Thread.interrupted()) { try { final var receiveMessageHandler = new JsonReceiveMessageHandler(m, jsonWriter); - m.receiveMessages(1, TimeUnit.HOURS, false, receiveMessageHandler); + m.receiveMessages(receiveMessageHandler); break; } catch (IOException e) { logger.warn("Receiving messages failed, retrying", e); diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index b4797be3..e72d8090 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -142,17 +142,16 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { final Namespace ns, final Manager m, final OutputWriter outputWriter ) throws CommandException { double timeout = ns.getDouble("timeout"); - var returnOnTimeout = true; - if (timeout < 0) { - returnOnTimeout = false; - timeout = 3600; - } boolean ignoreAttachments = Boolean.TRUE.equals(ns.getBoolean("ignore-attachments")); m.setIgnoreAttachments(ignoreAttachments); try { final var handler = outputWriter instanceof JsonWriter ? new JsonReceiveMessageHandler(m, (JsonWriter) outputWriter) : new ReceiveMessageHandler(m, (PlainTextWriter) outputWriter); - m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, returnOnTimeout, handler); + if (timeout < 0) { + m.receiveMessages(handler); + } else { + m.receiveMessages((long) (timeout * 1000), TimeUnit.MILLISECONDS, handler); + } } catch (IOException e) { throw new IOErrorException("Error while receiving messages: " + e.getMessage(), e); } diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 93d36888..31e29ac9 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -423,9 +423,14 @@ public class DbusManagerImpl implements Manager { signal.sendSyncRequest(); } + @Override + public void receiveMessages(final ReceiveMessageHandler handler) throws IOException { + throw new UnsupportedOperationException(); + } + @Override public void receiveMessages( - final long timeout, final TimeUnit unit, final boolean returnOnTimeout, final ReceiveMessageHandler handler + final long timeout, final TimeUnit unit, final ReceiveMessageHandler handler ) throws IOException { throw new UnsupportedOperationException(); } From fc0a9b4102feef185e4a09881e3b079b82df3da7 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 21 Oct 2021 22:59:52 +0200 Subject: [PATCH 0878/2005] Move receive thread handling to manager --- .../org/asamk/signal/manager/Manager.java | 14 +++ .../org/asamk/signal/manager/ManagerImpl.java | 112 ++++++++++++++++++ .../asamk/signal/commands/DaemonCommand.java | 31 ++--- .../commands/JsonRpcDispatcherCommand.java | 33 +----- .../asamk/signal/dbus/DbusManagerImpl.java | 15 +++ 5 files changed, 156 insertions(+), 49 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index ac0cc02f..0a8762d9 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -193,6 +193,20 @@ public interface Manager extends Closeable { void requestAllSyncData() throws IOException; + /** + * Add a handler to receive new messages. + * Will start receiving messages from server, if not already started. + */ + void addReceiveHandler(ReceiveMessageHandler handler); + + /** + * Remove a handler to receive new messages. + * Will stop receiving messages from server, if this was the last registered receiver. + */ + void removeReceiveHandler(ReceiveMessageHandler handler); + + boolean isReceiving(); + /** * Receive new messages from server, returns if no new message arrive in a timespan of timeout. */ diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 0421a401..2ea96591 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -137,6 +137,10 @@ public class ManagerImpl implements Manager { private boolean hasCaughtUpWithOldMessages = false; private boolean ignoreAttachments = false; + private Thread receiveThread; + private final Set messageHandlers = new HashSet<>(); + private boolean isReceivingSynchronous; + ManagerImpl( SignalAccount account, PathConfig pathConfig, @@ -872,6 +876,88 @@ public class ManagerImpl implements Manager { return actions; } + @Override + public void addReceiveHandler(final ReceiveMessageHandler handler) { + if (isReceivingSynchronous) { + throw new IllegalStateException("Already receiving message synchronously."); + } + synchronized (messageHandlers) { + messageHandlers.add(handler); + + startReceiveThreadIfRequired(); + } + } + + private void startReceiveThreadIfRequired() { + if (receiveThread != null) { + return; + } + receiveThread = new Thread(() -> { + while (!Thread.interrupted()) { + try { + receiveMessagesInternal(1L, TimeUnit.HOURS, false, (envelope, decryptedContent, e) -> { + synchronized (messageHandlers) { + for (ReceiveMessageHandler h : messageHandlers) { + try { + h.handleMessage(envelope, decryptedContent, e); + } catch (Exception ex) { + logger.warn("Message handler failed, ignoring", ex); + } + } + } + }); + break; + } catch (IOException e) { + logger.warn("Receiving messages failed, retrying", e); + } + } + hasCaughtUpWithOldMessages = false; + synchronized (messageHandlers) { + receiveThread = null; + + // Check if in the meantime another handler has been registered + if (!messageHandlers.isEmpty()) { + startReceiveThreadIfRequired(); + } + } + }); + + receiveThread.start(); + } + + @Override + public void removeReceiveHandler(final ReceiveMessageHandler handler) { + final Thread thread; + synchronized (messageHandlers) { + thread = receiveThread; + receiveThread = null; + messageHandlers.remove(handler); + if (!messageHandlers.isEmpty() || isReceivingSynchronous) { + return; + } + } + + stopReceiveThread(thread); + } + + private void stopReceiveThread(final Thread thread) { + thread.interrupt(); + try { + thread.join(); + } catch (InterruptedException ignored) { + } + } + + @Override + public boolean isReceiving() { + if (isReceivingSynchronous) { + return true; + } + synchronized (messageHandlers) { + return messageHandlers.size() > 0; + } + } + @Override public void receiveMessages(long timeout, TimeUnit unit, ReceiveMessageHandler handler) throws IOException { receiveMessages(timeout, unit, true, handler); @@ -884,6 +970,23 @@ public class ManagerImpl implements Manager { private void receiveMessages( long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler + ) throws IOException { + if (isReceiving()) { + throw new IllegalStateException("Already receiving message."); + } + isReceivingSynchronous = true; + receiveThread = Thread.currentThread(); + try { + receiveMessagesInternal(timeout, unit, returnOnTimeout, handler); + } finally { + receiveThread = null; + hasCaughtUpWithOldMessages = false; + isReceivingSynchronous = false; + } + } + + private void receiveMessagesInternal( + long timeout, TimeUnit unit, boolean returnOnTimeout, ReceiveMessageHandler handler ) throws IOException { retryFailedReceivedMessages(handler); @@ -1249,6 +1352,15 @@ public class ManagerImpl implements Manager { } private void close(boolean closeAccount) throws IOException { + Thread thread; + synchronized (messageHandlers) { + messageHandlers.clear(); + thread = receiveThread; + receiveThread = null; + } + if (thread != null) { + stopReceiveThread(thread); + } executor.shutdown(); dependencies.getSignalWebSocket().disconnect(); diff --git a/src/main/java/org/asamk/signal/commands/DaemonCommand.java b/src/main/java/org/asamk/signal/commands/DaemonCommand.java index 9627d9fb..a121c7e9 100644 --- a/src/main/java/org/asamk/signal/commands/DaemonCommand.java +++ b/src/main/java/org/asamk/signal/commands/DaemonCommand.java @@ -71,6 +71,9 @@ public class DaemonCommand implements MultiLocalCommand { try { t.join(); + synchronized (this) { + wait(); + } } catch (InterruptedException ignored) { } } catch (DBusException | IOException e) { @@ -128,27 +131,11 @@ public class DaemonCommand implements MultiLocalCommand { logger.info("Exported dbus object: " + objectPath); - final var thread = new Thread(() -> { - while (!Thread.interrupted()) { - try { - final var receiveMessageHandler = outputWriter instanceof JsonWriter - ? new JsonDbusReceiveMessageHandler(m, (JsonWriter) outputWriter, conn, objectPath) - : new DbusReceiveMessageHandler(m, (PlainTextWriter) outputWriter, conn, objectPath); - m.receiveMessages(receiveMessageHandler); - break; - } catch (IOException e) { - logger.warn("Receiving messages failed, retrying", e); - } - } - try { - initThread.join(); - } catch (InterruptedException ignored) { - } - signal.close(); - }); - - thread.start(); - - return thread; + final var receiveMessageHandler = outputWriter instanceof JsonWriter ? new JsonDbusReceiveMessageHandler(m, + (JsonWriter) outputWriter, + conn, + objectPath) : new DbusReceiveMessageHandler(m, (PlainTextWriter) outputWriter, conn, objectPath); + m.addReceiveHandler(receiveMessageHandler); + return initThread; } } diff --git a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java index 2a95a880..6e0c3173 100644 --- a/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java +++ b/src/main/java/org/asamk/signal/commands/JsonRpcDispatcherCommand.java @@ -70,10 +70,11 @@ public class JsonRpcDispatcherCommand implements LocalCommand { final var objectMapper = Util.createJsonObjectMapper(); final var jsonRpcSender = new JsonRpcSender((JsonWriter) outputWriter); - final var receiveThread = receiveMessages(s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification( - "receive", - objectMapper.valueToTree(s), - null)), m); + final var receiveMessageHandler = new JsonReceiveMessageHandler(m, + s -> jsonRpcSender.sendRequest(JsonRpcRequest.forNotification("receive", + objectMapper.valueToTree(s), + null))); + m.addReceiveHandler(receiveMessageHandler); // Maybe this should be handled inside the Manager while (!m.hasCaughtUpWithOldMessages()) { @@ -97,11 +98,7 @@ public class JsonRpcDispatcherCommand implements LocalCommand { jsonRpcReader.readRequests((method, params) -> handleRequest(m, objectMapper, method, params), response -> logger.debug("Received unexpected response for id {}", response.getId())); - receiveThread.interrupt(); - try { - receiveThread.join(); - } catch (InterruptedException ignored) { - } + m.removeReceiveHandler(receiveMessageHandler); } private JsonNode handleRequest( @@ -166,22 +163,4 @@ public class JsonRpcDispatcherCommand implements LocalCommand { } command.handleCommand(requestParams, m, outputWriter); } - - private Thread receiveMessages(JsonWriter jsonWriter, Manager m) { - final var thread = new Thread(() -> { - while (!Thread.interrupted()) { - try { - final var receiveMessageHandler = new JsonReceiveMessageHandler(m, jsonWriter); - m.receiveMessages(receiveMessageHandler); - break; - } catch (IOException e) { - logger.warn("Receiving messages failed, retrying", e); - } - } - }); - - thread.start(); - - return thread; - } } diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index 31e29ac9..fcbadd38 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -423,6 +423,21 @@ public class DbusManagerImpl implements Manager { signal.sendSyncRequest(); } + @Override + public void addReceiveHandler(final ReceiveMessageHandler handler) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeReceiveHandler(final ReceiveMessageHandler handler) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isReceiving() { + throw new UnsupportedOperationException(); + } + @Override public void receiveMessages(final ReceiveMessageHandler handler) throws IOException { throw new UnsupportedOperationException(); From 004293362eed2871e2645c5388e7a8a62a754f73 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 Oct 2021 19:16:01 +0200 Subject: [PATCH 0879/2005] Update libsignal-service-java --- graalvm-config-dir/jni-config.json | 125 +- graalvm-config-dir/proxy-config.json | 8 +- graalvm-config-dir/reflect-config.json | 1765 +++++++++-------- graalvm-config-dir/resource-config.json | 116 +- lib/build.gradle.kts | 2 +- .../signal/manager/helper/ProfileHelper.java | 13 +- run_tests.sh | 25 +- 7 files changed, 1081 insertions(+), 973 deletions(-) diff --git a/graalvm-config-dir/jni-config.json b/graalvm-config-dir/jni-config.json index 8c8c30f5..bceda6f3 100644 --- a/graalvm-config-dir/jni-config.json +++ b/graalvm-config-dir/jni-config.json @@ -1,29 +1,33 @@ [ +{ + "name":"java.lang.Boolean", + "methods":[{"name":"getBoolean","parameterTypes":["java.lang.String"] }]} +, { "name":"java.lang.ClassLoader", "methods":[ {"name":"getPlatformClassLoader","parameterTypes":[] }, {"name":"loadClass","parameterTypes":["java.lang.String"] } - ] -}, + ]} +, { "name":"java.lang.IllegalStateException", - "methods":[{"name":"","parameterTypes":["java.lang.String"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.String"] }]} +, { - "name":"java.lang.NoSuchMethodError" -}, + "name":"java.lang.NoSuchMethodError"} +, { "name":"java.lang.UnsatisfiedLinkError", - "methods":[{"name":"","parameterTypes":["java.lang.String"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.String"] }]} +, { "name":"java.util.UUID", - "methods":[{"name":"","parameterTypes":["long","long"] }] -}, + "methods":[{"name":"","parameterTypes":["long","long"] }]} +, { - "name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader" -}, + "name":"jdk.internal.loader.ClassLoaders$PlatformClassLoader"} +, { "name":"org.asamk.signal.manager.storage.protocol.SignalProtocolStore", "methods":[ @@ -39,106 +43,111 @@ {"name":"saveIdentity","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.IdentityKey"] }, {"name":"storeSenderKey","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","java.util.UUID","org.whispersystems.libsignal.groups.state.SenderKeyRecord"] }, {"name":"storeSession","parameterTypes":["org.whispersystems.libsignal.SignalProtocolAddress","org.whispersystems.libsignal.state.SessionRecord"] } - ] -}, + ]} +, { "name":"org.graalvm.nativebridge.jni.JNIExceptionWrapperEntryPoints", - "methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }] -}, + "methods":[{"name":"getClassName","parameterTypes":["java.lang.Class"] }]} +, { "name":"org.whispersystems.libsignal.DuplicateMessageException", - "methods":[{"name":"","parameterTypes":["java.lang.String"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.String"] }]} +, { "name":"org.whispersystems.libsignal.IdentityKey", "methods":[ {"name":"","parameterTypes":["byte[]"] }, {"name":"serialize","parameterTypes":[] } - ] -}, + ]} +, { "name":"org.whispersystems.libsignal.IdentityKeyPair", - "methods":[{"name":"serialize","parameterTypes":[] }] -}, + "methods":[{"name":"serialize","parameterTypes":[] }]} +, { "name":"org.whispersystems.libsignal.InvalidMessageException", - "methods":[{"name":"","parameterTypes":["java.lang.String"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.String"] }]} +, { "name":"org.whispersystems.libsignal.SignalProtocolAddress", - "methods":[{"name":"","parameterTypes":["java.lang.String","int"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.String","int"] }]} +, { "name":"org.whispersystems.libsignal.UntrustedIdentityException", - "methods":[{"name":"","parameterTypes":["java.lang.String"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.String"] }]} +, { "name":"org.whispersystems.libsignal.groups.state.SenderKeyRecord", "methods":[ {"name":"","parameterTypes":["long"] }, {"name":"nativeHandle","parameterTypes":[] } - ] -}, + ]} +, { - "name":"org.whispersystems.libsignal.groups.state.SenderKeyStore" -}, + "name":"org.whispersystems.libsignal.groups.state.SenderKeyStore"} +, { "name":"org.whispersystems.libsignal.logging.Log", - "methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }] -}, + "methods":[{"name":"log","parameterTypes":["int","java.lang.String","java.lang.String"] }]} +, { "name":"org.whispersystems.libsignal.protocol.PlaintextContent", - "methods":[{"name":"nativeHandle","parameterTypes":[] }] -}, + "methods":[{"name":"nativeHandle","parameterTypes":[] }]} +, { "name":"org.whispersystems.libsignal.protocol.PreKeySignalMessage", + "fields":[{"name":"unsafeHandle"}], "methods":[ {"name":"","parameterTypes":["long"] }, {"name":"nativeHandle","parameterTypes":[] } - ] -}, + ]} +, { - "name":"org.whispersystems.libsignal.protocol.SenderKeyMessage" -}, + "name":"org.whispersystems.libsignal.protocol.SenderKeyMessage"} +, { "name":"org.whispersystems.libsignal.protocol.SignalMessage", + "fields":[{"name":"unsafeHandle"}], "methods":[ {"name":"","parameterTypes":["long"] }, {"name":"nativeHandle","parameterTypes":[] } - ] -}, + ]} +, { - "name":"org.whispersystems.libsignal.state.IdentityKeyStore" -}, + "name":"org.whispersystems.libsignal.state.IdentityKeyStore"} +, { "name":"org.whispersystems.libsignal.state.IdentityKeyStore$Direction", "fields":[ {"name":"RECEIVING"}, {"name":"SENDING"} - ] -}, + ]} +, { "name":"org.whispersystems.libsignal.state.PreKeyRecord", - "methods":[{"name":"nativeHandle","parameterTypes":[] }] -}, + "fields":[{"name":"unsafeHandle"}], + "methods":[{"name":"nativeHandle","parameterTypes":[] }]} +, { - "name":"org.whispersystems.libsignal.state.PreKeyStore" -}, + "name":"org.whispersystems.libsignal.state.PreKeyStore"} +, { "name":"org.whispersystems.libsignal.state.SessionRecord", + "fields":[{"name":"unsafeHandle"}], "methods":[ {"name":"","parameterTypes":["byte[]"] }, {"name":"nativeHandle","parameterTypes":[] } - ] -}, + ]} +, { - "name":"org.whispersystems.libsignal.state.SessionStore" -}, + "name":"org.whispersystems.libsignal.state.SessionStore"} +, { "name":"org.whispersystems.libsignal.state.SignedPreKeyRecord", - "methods":[{"name":"nativeHandle","parameterTypes":[] }] -}, + "fields":[{"name":"unsafeHandle"}], + "methods":[{"name":"nativeHandle","parameterTypes":[] }]} +, { - "name":"org.whispersystems.libsignal.state.SignedPreKeyStore" -} + "name":"org.whispersystems.libsignal.state.SignedPreKeyStore"} + ] diff --git a/graalvm-config-dir/proxy-config.json b/graalvm-config-dir/proxy-config.json index 7abe9244..be8f8d3c 100644 --- a/graalvm-config-dir/proxy-config.json +++ b/graalvm-config-dir/proxy-config.json @@ -1,4 +1,8 @@ [ - ["org.asamk.Signal"], - ["org.freedesktop.dbus.interfaces.DBus"] + { + "interfaces":["org.asamk.Signal"]} + , + { + "interfaces":["org.freedesktop.dbus.interfaces.DBus"]} + ] diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index b1939327..aa3effb8 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -1,1162 +1,1187 @@ [ +{ + "name":"[B", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true} +, +{ + "name":"[C"} +, +{ + "name":"[I", + "queryAllDeclaredMethods":true, + "queryAllPublicMethods":true} +, +{ + "name":"[J"} +, +{ + "name":"[Lorg.whispersystems.signalservice.api.groupsv2.TemporalCredential;"} +, { "name":"byte[]", "allDeclaredMethods":true, - "allPublicMethods":true -}, + "allPublicMethods":true} +, { - "name":"char[]" -}, + "name":"char[]"} +, { "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"com.google.protobuf.AbstractProtobufList", "allDeclaredFields":true, - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"com.google.protobuf.Internal$LongList", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"com.google.protobuf.Internal$ProtobufList", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"com.google.protobuf.LongArrayList", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"com.google.protobuf.PrimitiveNonBoxingCollection", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"com.kenai.jffi.Invoker", "methods":[ {"name":"invokeI6","parameterTypes":["com.kenai.jffi.CallContext","long","int","int","int","int","int","int"] }, {"name":"invokeL6","parameterTypes":["com.kenai.jffi.CallContext","long","long","long","long","long","long","long"] }, {"name":"invokeN6","parameterTypes":["com.kenai.jffi.CallContext","long","long","long","long","long","long","long"] } - ] -}, + ]} +, { "name":"com.kenai.jffi.Version", "fields":[ {"name":"MAJOR"}, {"name":"MICRO"}, {"name":"MINOR"} - ] -}, + ]} +, { "name":"com.kenai.jffi.internal.StubLoader", - "methods":[{"name":"isLoaded","parameterTypes":[] }] -}, + "methods":[{"name":"isLoaded","parameterTypes":[] }]} +, { "name":"com.sun.crypto.provider.AESCipher$General", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"com.sun.crypto.provider.DHParameters", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"com.sun.crypto.provider.HmacCore$HmacSHA256", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, +{ + "name":"com.sun.crypto.provider.HmacCore$HmacSHA384", + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"com.sun.crypto.provider.TlsKeyMaterialGenerator", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"com.sun.crypto.provider.TlsMasterSecretGenerator", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"com.sun.crypto.provider.TlsPrfGenerator$V12", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"int", "allDeclaredMethods":true, - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"int[]", "allDeclaredMethods":true, - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"java.io.Serializable", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"java.lang.Boolean", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"java.lang.Comparable", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"java.lang.Double", - "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] -}, + "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }]} +, { "name":"java.lang.Enum", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"java.lang.Integer", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true, - "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] -}, + "allDeclaredConstructors":true} +, { "name":"java.lang.Iterable", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"java.lang.Long", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true, - "methods":[{"name":"valueOf","parameterTypes":["java.lang.String"] }] -}, + "allDeclaredConstructors":true} +, { "name":"java.lang.Number", "allDeclaredFields":true, - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"java.lang.String", - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"java.lang.reflect.Method", - "methods":[{"name":"isDefault","parameterTypes":[] }] -}, + "methods":[{"name":"isDefault","parameterTypes":[] }]} +, { "name":"java.nio.Buffer", "allDeclaredMethods":true, - "fields":[{"name":"address"}] -}, + "fields":[{"name":"address"}]} +, { "name":"java.nio.ByteBuffer", "allDeclaredMethods":true, - "allPublicMethods":true -}, + "allPublicMethods":true} +, { - "name":"java.security.KeyStoreSpi" -}, + "name":"java.security.KeyStoreSpi"} +, { - "name":"java.security.SecureRandomParameters" -}, + "name":"java.security.SecureRandomParameters"} +, { - "name":"java.security.cert.PKIXRevocationChecker" -}, + "name":"java.security.cert.PKIXRevocationChecker"} +, { - "name":"java.security.interfaces.ECPrivateKey" -}, + "name":"java.security.interfaces.ECPrivateKey"} +, { - "name":"java.security.interfaces.ECPublicKey" -}, + "name":"java.security.interfaces.ECPublicKey"} +, { - "name":"java.security.interfaces.RSAPrivateKey" -}, + "name":"java.security.interfaces.RSAPrivateKey"} +, { - "name":"java.security.interfaces.RSAPublicKey" -}, + "name":"java.security.interfaces.RSAPublicKey"} +, { "name":"java.util.AbstractCollection", "allDeclaredFields":true, - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"java.util.AbstractList", "allDeclaredFields":true, - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"java.util.ArrayList", "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"java.util.Collection", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"java.util.HashSet", "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"java.util.LinkedHashMap", "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"java.util.List", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"java.util.Locale", - "methods":[{"name":"getUnicodeLocaleType","parameterTypes":["java.lang.String"] }] -}, + "methods":[{"name":"getUnicodeLocaleType","parameterTypes":["java.lang.String"] }]} +, { "name":"java.util.RandomAccess", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"java.util.UUID", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { - "name":"jnr.constants.platform.linux.ProtocolFamily" -}, + "name":"javax.security.auth.x500.X500Principal", + "methods":[{"name":"","parameterTypes":["sun.security.x509.X500Name"] }]} +, { - "name":"jnr.constants.platform.linux.Shutdown" -}, + "name":"jnr.constants.platform.linux.ProtocolFamily"} +, { - "name":"jnr.constants.platform.linux.Sock" -}, + "name":"jnr.constants.platform.linux.Shutdown"} +, { - "name":"jnr.constants.platform.linux.SocketLevel" -}, + "name":"jnr.constants.platform.linux.Sock"} +, { - "name":"jnr.constants.platform.linux.SocketOption" -}, + "name":"jnr.constants.platform.linux.SocketLevel"} +, +{ + "name":"jnr.constants.platform.linux.SocketOption"} +, { "name":"jnr.enxio.channels.Native$LibC", - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"jnr.enxio.channels.Native$LibC$jnr$ffi$1", - "methods":[{"name":"","parameterTypes":["jnr.ffi.Runtime","jnr.ffi.provider.jffi.NativeLibrary","java.lang.Object[]"] }] -}, + "methods":[{"name":"","parameterTypes":["jnr.ffi.Runtime","jnr.ffi.provider.jffi.NativeLibrary","java.lang.Object[]"] }]} +, { "name":"jnr.ffi.Pointer", "allDeclaredMethods":true, - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"jnr.ffi.StructLayout$gid_t", - "methods":[{"name":"","parameterTypes":["jnr.ffi.StructLayout"] }] -}, + "methods":[{"name":"","parameterTypes":["jnr.ffi.StructLayout"] }]} +, { "name":"jnr.ffi.byref.IntByReference", - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"jnr.ffi.provider.converters.ByReferenceParameterConverter", - "methods":[{"name":"nativeType","parameterTypes":[] }] -}, + "methods":[{"name":"nativeType","parameterTypes":[] }]} +, { "name":"jnr.ffi.provider.converters.ByReferenceParameterConverter$Out", - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"jnr.ffi.provider.converters.StringResultConverter", - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"jnr.ffi.provider.converters.StructByReferenceToNativeConverter", - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"jnr.ffi.provider.jffi.BufferParameterStrategy", - "methods":[{"name":"address","parameterTypes":["java.nio.Buffer"] }] -}, + "methods":[{"name":"address","parameterTypes":["java.nio.Buffer"] }]} +, { "name":"jnr.ffi.provider.jffi.PointerParameterStrategy", - "methods":[{"name":"address","parameterTypes":["jnr.ffi.Pointer"] }] -}, + "methods":[{"name":"address","parameterTypes":["jnr.ffi.Pointer"] }]} +, { "name":"jnr.ffi.provider.jffi.Provider", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"jnr.ffi.provider.jffi.platform.x86_64.linux.TypeAliases", - "fields":[{"name":"ALIASES"}] -}, + "fields":[{"name":"ALIASES"}]} +, { "name":"jnr.posix.Timeval", - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"jnr.unixsocket.Native$LibC", - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"jnr.unixsocket.Native$LibC$jnr$ffi$0", - "methods":[{"name":"","parameterTypes":["jnr.ffi.Runtime","jnr.ffi.provider.jffi.NativeLibrary","java.lang.Object[]"] }] -}, + "methods":[{"name":"","parameterTypes":["jnr.ffi.Runtime","jnr.ffi.provider.jffi.NativeLibrary","java.lang.Object[]"] }]} +, { "name":"jnr.unixsocket.SockAddrUnix", - "allPublicMethods":true -}, + "allPublicMethods":true} +, { "name":"long", "allDeclaredMethods":true, - "allPublicMethods":true -}, + "allPublicMethods":true} +, { - "name":"long[]" -}, + "name":"long[]"} +, { "name":"org.asamk.Signal", "allDeclaredMethods":true, - "allDeclaredClasses":true -}, + "allDeclaredClasses":true} +, { "name":"org.asamk.Signal$Device", "allDeclaredMethods":true, - "allDeclaredClasses":true -}, + "allDeclaredClasses":true} +, { "name":"org.asamk.Signal$Group", "allDeclaredMethods":true, - "allDeclaredClasses":true -}, + "allDeclaredClasses":true} +, { "name":"org.asamk.Signal$MessageReceived", "allDeclaredConstructors":true, - "allPublicConstructors":true -}, + "allPublicConstructors":true} +, { "name":"org.asamk.Signal$ReceiptReceived", "allDeclaredConstructors":true, - "allPublicConstructors":true -}, + "allPublicConstructors":true} +, { "name":"org.asamk.Signal$StructDevice", - "allDeclaredFields":true -}, + "allDeclaredFields":true} +, { "name":"org.asamk.Signal$StructGroup", - "allDeclaredFields":true -}, + "allDeclaredFields":true} +, { "name":"org.asamk.Signal$SyncMessageReceived", "allDeclaredConstructors":true, - "allPublicConstructors":true -}, + "allPublicConstructors":true} +, { "name":"org.asamk.SignalControl", "allDeclaredMethods":true, - "allDeclaredClasses":true -}, + "allDeclaredClasses":true} +, { "name":"org.asamk.signal.commands.GetUserStatusCommand$JsonUserStatus", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.commands.ListContactsCommand$JsonContact", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.commands.ListDevicesCommand$JsonDevice", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.commands.ListGroupsCommand$JsonGroup", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.commands.ListGroupsCommand$JsonGroupMember", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.commands.ListIdentitiesCommand$JsonIdentity", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonAttachment", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonCallMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonContactAddress", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonContactAvatar", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonContactEmail", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonContactName", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonContactPhone", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonDataMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonError", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonGroupInfo", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonMention", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonMessageEnvelope", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonQuote", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonQuotedAttachment", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonReaction", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonReceiptMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonRemoteDelete", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonSharedContact", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonSticker", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonSyncDataMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonSyncMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonSyncMessageType", "allDeclaredFields":true, - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"org.asamk.signal.json.JsonSyncReadMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.json.JsonTypingMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.jsonrpc.JsonRpcBulkMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.jsonrpc.JsonRpcException", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.jsonrpc.JsonRpcMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.jsonrpc.JsonRpcRequest", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.jsonrpc.JsonRpcResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.jsonrpc.JsonRpcResponse$Error", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore", "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true, - "fields":[{"name":"contacts", "allowWrite":true}] -}, + "fields":[{"name":"contacts", "allowWrite":true}]} +, { "name":"org.asamk.signal.manager.storage.groups.GroupInfo", "allDeclaredFields":true, - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"org.asamk.signal.manager.storage.groups.GroupInfoV1", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.groups.GroupStore$GroupsDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage$Group", "allDeclaredFields":true, - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage$GroupV1", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage$GroupV1$MembersDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage$GroupV1$MembersSerializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.asamk.signal.manager.storage.groups.GroupStore$Storage$GroupV2", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.identities.IdentityKeyStore$IdentityStorage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore", "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true, - "fields":[{"name":"profiles", "allowWrite":true}] -}, + "fields":[{"name":"profiles", "allowWrite":true}]} +, { "name":"org.asamk.signal.manager.storage.profiles.LegacyProfileStore$ProfileStoreDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.asamk.signal.manager.storage.profiles.LegacySignalProfileEntry", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.profiles.ProfileStore", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.profiles.SignalProfile", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.profiles.SignalProfile$Capabilities", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.protocol.LegacyJsonIdentityKeyStore$JsonIdentityKeyStoreDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.asamk.signal.manager.storage.protocol.LegacyJsonPreKeyStore$JsonPreKeyStoreDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.asamk.signal.manager.storage.protocol.LegacyJsonSessionStore$JsonSessionStoreDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.protocol.LegacyJsonSignedPreKeyStore$JsonSignedPreKeyStoreDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.asamk.signal.manager.storage.recipients.LegacyRecipientStore", "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true, - "fields":[{"name":"addresses", "allowWrite":true}] -}, + "fields":[{"name":"addresses", "allowWrite":true}]} +, { "name":"org.asamk.signal.manager.storage.recipients.LegacyRecipientStore$RecipientStoreDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage$Recipient", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage$Recipient$Contact", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.recipients.RecipientStore$Storage$Recipient$Profile", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore$Storage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.senderKeys.SenderKeySharedStore$Storage$SharedSenderKey", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.stickers.StickerStore", "allDeclaredFields":true, "allDeclaredMethods":true, "allDeclaredConstructors":true, - "fields":[{"name":"stickers", "allowWrite":true}] -}, + "fields":[{"name":"stickers", "allowWrite":true}]} +, { "name":"org.asamk.signal.manager.storage.stickers.StickerStore$Storage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.manager.storage.stickers.StickerStore$Storage$Sticker", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.asamk.signal.util.SecurityProvider$DefaultRandom", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.COMPOSITE$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.DH$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.DSA$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.DSTU4145$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.EC$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.ECGOST$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.EdEC$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.ElGamal$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.GM$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.GOST$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.IES$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.X509$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.edec.SignatureSpi$Ed25519", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.edec.SignatureSpi$Ed448", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.asymmetric.x509.CertificateFactory", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.Blake2b$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.Blake2s$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.DSTU7564$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.GOST3411$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.Haraka$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.Keccak$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.MD2$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.MD4$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.MD5$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD128$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD160$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD256$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD320$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.SHA1$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.SHA224$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.SHA256$Digest", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.SHA256$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.SHA3$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.SHA384$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.SHA512$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.SM3$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.Skein$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.Tiger$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.digest.Whirlpool$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.drbg.DRBG$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.keystore.BC$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.keystore.BCFKS$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.keystore.PKCS12$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.keystore.bc.BcKeyStoreSpi$Std", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.AES$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.ARC4$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.ARIA$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Blowfish$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.CAST5$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.CAST6$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Camellia$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.ChaCha$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.DES$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.DESede$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.DSTU7624$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.GOST28147$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.GOST3412_2015$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Grain128$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Grainv1$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.HC128$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.HC256$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.IDEA$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Noekeon$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.OpenSSLPBKDF$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF1$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF2$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPKCS12$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Poly1305$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.RC2$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.RC5$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.RC6$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Rijndael$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.SCRYPT$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.SEED$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.SM4$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Salsa20$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Serpent$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Shacal2$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.SipHash$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.SipHash128$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Skipjack$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.TEA$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.TLSKDF$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Threefish$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Twofish$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.VMPC$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.VMPCKSA3$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.XSalsa20$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.XTEA$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.bouncycastle.jcajce.provider.symmetric.Zuc$Mappings", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.freedesktop.dbus.errors.ServiceUnknown", - "methods":[{"name":"","parameterTypes":["java.lang.String"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.String"] }]} +, { "name":"org.freedesktop.dbus.interfaces.DBus$NameAcquired", - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.freedesktop.dbus.interfaces.Introspectable", "allDeclaredMethods":true, - "allDeclaredClasses":true -}, + "allDeclaredClasses":true} +, { "name":"org.freedesktop.dbus.interfaces.Peer", "allDeclaredMethods":true, - "allDeclaredClasses":true -}, + "allDeclaredClasses":true} +, { "name":"org.freedesktop.dbus.interfaces.Properties", "allDeclaredMethods":true, - "allDeclaredClasses":true -}, + "allDeclaredClasses":true} +, { "name":"org.freedesktop.dbus.interfaces.Properties$PropertiesChanged", - "allPublicConstructors":true -}, + "allPublicConstructors":true} +, { - "name":"org.objectweb.asm.util.TraceMethodVisitor" -}, + "name":"org.objectweb.asm.util.TraceMethodVisitor"} +, { "name":"org.signal.storageservice.protos.groups.AccessControl", "fields":[ {"name":"addFromInviteLink_"}, {"name":"attributes_"}, {"name":"members_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.AvatarUploadAttributes", "fields":[ @@ -1167,8 +1192,8 @@ {"name":"key_"}, {"name":"policy_"}, {"name":"signature_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.Group", "fields":[ @@ -1184,23 +1209,23 @@ {"name":"requestingMembers_"}, {"name":"revision_"}, {"name":"title_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.GroupAttributeBlob", "fields":[ {"name":"contentCase_"}, {"name":"content_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange", "fields":[ {"name":"actions_"}, {"name":"changeEpoch_"}, {"name":"serverSignature_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions", "fields":[ @@ -1225,99 +1250,99 @@ {"name":"promoteRequestingMembers_"}, {"name":"revision_"}, {"name":"sourceUuid_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$AddMemberAction", "fields":[ {"name":"added_"}, {"name":"joinFromInviteLink_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$AddPendingMemberAction", - "fields":[{"name":"added_"}] -}, + "fields":[{"name":"added_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$AddRequestingMemberAction", - "fields":[{"name":"added_"}] -}, + "fields":[{"name":"added_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$DeleteMemberAction", - "fields":[{"name":"deletedUserId_"}] -}, + "fields":[{"name":"deletedUserId_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$DeletePendingMemberAction", - "fields":[{"name":"deletedUserId_"}] -}, + "fields":[{"name":"deletedUserId_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$DeleteRequestingMemberAction", - "fields":[{"name":"deletedUserId_"}] -}, + "fields":[{"name":"deletedUserId_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyAddFromInviteLinkAccessControlAction", - "fields":[{"name":"addFromInviteLinkAccess_"}] -}, + "fields":[{"name":"addFromInviteLinkAccess_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyAttributesAccessControlAction", - "fields":[{"name":"attributesAccess_"}] -}, + "fields":[{"name":"attributesAccess_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyDescriptionAction", - "fields":[{"name":"description_"}] -}, + "fields":[{"name":"description_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyDisappearingMessagesTimerAction", - "fields":[{"name":"timer_"}] -}, + "fields":[{"name":"timer_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyInviteLinkPasswordAction", - "fields":[{"name":"inviteLinkPassword_"}] -}, + "fields":[{"name":"inviteLinkPassword_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMemberProfileKeyAction", - "fields":[{"name":"presentation_"}] -}, + "fields":[{"name":"presentation_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMemberRoleAction", "fields":[ {"name":"role_"}, {"name":"userId_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyMembersAccessControlAction", - "fields":[{"name":"membersAccess_"}] -}, + "fields":[{"name":"membersAccess_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$ModifyTitleAction", - "fields":[{"name":"title_"}] -}, + "fields":[{"name":"title_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$PromotePendingMemberAction", - "fields":[{"name":"presentation_"}] -}, + "fields":[{"name":"presentation_"}]} +, { "name":"org.signal.storageservice.protos.groups.GroupChange$Actions$PromoteRequestingMemberAction", "fields":[ {"name":"role_"}, {"name":"userId_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.GroupInviteLink", "fields":[ {"name":"contentsCase_"}, {"name":"contents_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.GroupInviteLink$GroupInviteLinkContentsV1", "fields":[ {"name":"groupMasterKey_"}, {"name":"inviteLinkPassword_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.Member", "fields":[ @@ -1326,16 +1351,16 @@ {"name":"profileKey_"}, {"name":"role_"}, {"name":"userId_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.PendingMember", "fields":[ {"name":"addedByUserId_"}, {"name":"member_"}, {"name":"timestamp_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.RequestingMember", "fields":[ @@ -1343,8 +1368,8 @@ {"name":"profileKey_"}, {"name":"timestamp_"}, {"name":"userId_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.local.DecryptedGroup", "fields":[ @@ -1359,8 +1384,8 @@ {"name":"requestingMembers_"}, {"name":"revision_"}, {"name":"title_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.local.DecryptedGroupChange", "fields":[ @@ -1385,8 +1410,8 @@ {"name":"promotePendingMembers_"}, {"name":"promoteRequestingMembers_"}, {"name":"revision_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.local.DecryptedMember", "fields":[ @@ -1394,15 +1419,15 @@ {"name":"profileKey_"}, {"name":"role_"}, {"name":"uuid_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole", "fields":[ {"name":"role_"}, {"name":"uuid_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.local.DecryptedPendingMember", "fields":[ @@ -1411,216 +1436,216 @@ {"name":"timestamp_"}, {"name":"uuidCipherText_"}, {"name":"uuid_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval", "fields":[ {"name":"uuidCipherText_"}, {"name":"uuid_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.local.DecryptedRequestingMember", "fields":[ {"name":"profileKey_"}, {"name":"timestamp_"}, {"name":"uuid_"} - ] -}, + ]} +, { "name":"org.signal.storageservice.protos.groups.local.DecryptedString", - "fields":[{"name":"value_"}] -}, + "fields":[{"name":"value_"}]} +, { "name":"org.signal.storageservice.protos.groups.local.DecryptedTimer", - "fields":[{"name":"duration_"}] -}, + "fields":[{"name":"duration_"}]} +, { "name":"org.whispersystems.libsignal.state.IdentityKeyStore", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"org.whispersystems.libsignal.state.PreKeyStore", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"org.whispersystems.libsignal.state.SessionStore", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"org.whispersystems.libsignal.state.SignalProtocolStore", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"org.whispersystems.libsignal.state.SignedPreKeyStore", - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"org.whispersystems.signalservice.api.account.AccountAttributes", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.account.AccountAttributes$Capabilities", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.groupsv2.CredentialResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { - "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]" -}, + "name":"org.whispersystems.signalservice.api.groupsv2.TemporalCredential[]"} +, { "name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.messages.calls.HangupMessage$Type", "allDeclaredFields":true, - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"org.whispersystems.signalservice.api.messages.calls.IceUpdateMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.messages.calls.OfferMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.messages.calls.OfferMessage$Type", "allDeclaredFields":true, - "allDeclaredMethods":true -}, + "allDeclaredMethods":true} +, { "name":"org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfile", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfile$Badge", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfile$Capabilities", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.profiles.SignalServiceProfileWrite", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.push.SignedPreKeyEntity", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.api.push.SignedPreKeyEntity$ByteArrayDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.whispersystems.signalservice.api.push.SignedPreKeyEntity$ByteArraySerializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.whispersystems.signalservice.api.storage.StorageAuthResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.contacts.crypto.SignatureBodyEntity", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.contacts.entities.DiscoveryRequest", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.contacts.entities.DiscoveryResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.contacts.entities.KeyBackupRequest", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.contacts.entities.KeyBackupResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.contacts.entities.MultiRemoteAttestationResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.contacts.entities.QueryEnvelope", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationRequest", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.contacts.entities.RemoteAttestationResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.contacts.entities.TokenResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.devices.DeviceNameProtos$DeviceName", "fields":[ @@ -1628,8 +1653,8 @@ {"name":"ciphertext_"}, {"name":"ephemeralPublic_"}, {"name":"syntheticIv_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.BackupRequest", "fields":[ @@ -1641,24 +1666,24 @@ {"name":"token_"}, {"name":"tries_"}, {"name":"validFrom_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.BackupResponse", "fields":[ {"name":"bitField0_"}, {"name":"status_"}, {"name":"token_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.DeleteRequest", "fields":[ {"name":"backupId_"}, {"name":"bitField0_"}, {"name":"serviceId_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.Request", "fields":[ @@ -1666,8 +1691,8 @@ {"name":"bitField0_"}, {"name":"delete_"}, {"name":"restore_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.Response", "fields":[ @@ -1675,8 +1700,8 @@ {"name":"bitField0_"}, {"name":"delete_"}, {"name":"restore_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.RestoreRequest", "fields":[ @@ -1686,8 +1711,8 @@ {"name":"serviceId_"}, {"name":"token_"}, {"name":"validFrom_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.keybackup.protos.RestoreResponse", "fields":[ @@ -1696,120 +1721,120 @@ {"name":"status_"}, {"name":"token_"}, {"name":"tries_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.AttachmentV2UploadAttributes", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.AuthCredentials", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.ConfirmCodeMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.DeviceCode", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.DeviceId", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.DeviceInfoList", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.MismatchedDevices", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.OutgoingPushMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.OutgoingPushMessageList", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.PreKeyEntity", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.PreKeyEntity$ECPublicKeyDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.whispersystems.signalservice.internal.push.PreKeyEntity$ECPublicKeySerializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.whispersystems.signalservice.internal.push.PreKeyResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.PreKeyResponseItem", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.PreKeyState", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.PreKeyStatus", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.ProfileAvatarUploadAttributes", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.ProvisioningMessage", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.ProvisioningProtos$ProvisionEnvelope", "fields":[ {"name":"bitField0_"}, {"name":"body_"}, {"name":"publicKey_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.ProvisioningProtos$ProvisionMessage", "fields":[ @@ -1823,43 +1848,43 @@ {"name":"readReceipts_"}, {"name":"userAgent_"}, {"name":"uuid_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.ProvisioningProtos$ProvisioningUuid", "fields":[ {"name":"bitField0_"}, {"name":"uuid_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.PushServiceSocket$RegistrationLockFailure", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.PushServiceSocket$RegistrationLockV2", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.SendMessageResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.SenderCertificate", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.push.SenderCertificate$ByteArrayDesieralizer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$AttachmentPointer", "fields":[ @@ -1879,8 +1904,8 @@ {"name":"thumbnail_"}, {"name":"uploadTimestamp_"}, {"name":"width_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage", "fields":[ @@ -1894,8 +1919,8 @@ {"name":"multiRing_"}, {"name":"offer_"}, {"name":"opaque_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$Hangup", "fields":[ @@ -1903,8 +1928,8 @@ {"name":"deviceId_"}, {"name":"id_"}, {"name":"type_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$IceUpdate", "fields":[ @@ -1914,8 +1939,8 @@ {"name":"mid_"}, {"name":"opaque_"}, {"name":"sdp_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$Offer", "fields":[ @@ -1924,15 +1949,15 @@ {"name":"opaque_"}, {"name":"sdp_"}, {"name":"type_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$CallMessage$Opaque", "fields":[ {"name":"bitField0_"}, {"name":"data_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$ContactDetails", "fields":[ @@ -1948,16 +1973,16 @@ {"name":"profileKey_"}, {"name":"uuid_"}, {"name":"verified_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$ContactDetails$Avatar", "fields":[ {"name":"bitField0_"}, {"name":"contentType_"}, {"name":"length_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Content", "fields":[ @@ -1970,8 +1995,8 @@ {"name":"senderKeyDistributionMessage_"}, {"name":"syncMessage_"}, {"name":"typingMessage_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage", "fields":[ @@ -1995,8 +2020,8 @@ {"name":"requiredProtocolVersion_"}, {"name":"sticker_"}, {"name":"timestamp_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$BodyRange", "fields":[ @@ -2005,8 +2030,8 @@ {"name":"bitField0_"}, {"name":"length_"}, {"name":"start_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact", "fields":[ @@ -2017,16 +2042,16 @@ {"name":"name_"}, {"name":"number_"}, {"name":"organization_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Avatar", "fields":[ {"name":"avatar_"}, {"name":"bitField0_"}, {"name":"isProfile_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Email", "fields":[ @@ -2034,8 +2059,8 @@ {"name":"label_"}, {"name":"type_"}, {"name":"value_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Name", "fields":[ @@ -2046,8 +2071,8 @@ {"name":"middleName_"}, {"name":"prefix_"}, {"name":"suffix_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$Phone", "fields":[ @@ -2055,8 +2080,8 @@ {"name":"label_"}, {"name":"type_"}, {"name":"value_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Contact$PostalAddress", "fields":[ @@ -2070,22 +2095,22 @@ {"name":"region_"}, {"name":"street_"}, {"name":"type_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Delete", "fields":[ {"name":"bitField0_"}, {"name":"targetSentTimestamp_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$GroupCallUpdate", "fields":[ {"name":"bitField0_"}, {"name":"eraId_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Preview", "fields":[ @@ -2095,8 +2120,8 @@ {"name":"image_"}, {"name":"title_"}, {"name":"url_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Quote", "fields":[ @@ -2107,8 +2132,8 @@ {"name":"bodyRanges_"}, {"name":"id_"}, {"name":"text_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Reaction", "fields":[ @@ -2117,8 +2142,8 @@ {"name":"remove_"}, {"name":"targetAuthorUuid_"}, {"name":"targetSentTimestamp_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$DataMessage$Sticker", "fields":[ @@ -2128,8 +2153,8 @@ {"name":"packId_"}, {"name":"packKey_"}, {"name":"stickerId_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Envelope", "fields":[ @@ -2144,8 +2169,8 @@ {"name":"sourceUuid_"}, {"name":"timestamp_"}, {"name":"type_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$GroupContext", "fields":[ @@ -2156,15 +2181,15 @@ {"name":"members_"}, {"name":"name_"}, {"name":"type_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$GroupContext$Member", "fields":[ {"name":"bitField0_"}, {"name":"e164_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$GroupContextV2", "fields":[ @@ -2172,23 +2197,23 @@ {"name":"groupChange_"}, {"name":"masterKey_"}, {"name":"revision_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$NullMessage", "fields":[ {"name":"bitField0_"}, {"name":"padding_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$ReceiptMessage", "fields":[ {"name":"bitField0_"}, {"name":"timestamp_"}, {"name":"type_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage", "fields":[ @@ -2209,16 +2234,16 @@ {"name":"verified_"}, {"name":"viewOnceOpen_"}, {"name":"viewed_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Blocked", "fields":[ {"name":"groupIds_"}, {"name":"numbers_"}, {"name":"uuids_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Configuration", "fields":[ @@ -2228,30 +2253,30 @@ {"name":"readReceipts_"}, {"name":"typingIndicators_"}, {"name":"unidentifiedDeliveryIndicators_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Contacts", "fields":[ {"name":"bitField0_"}, {"name":"blob_"}, {"name":"complete_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$FetchLatest", "fields":[ {"name":"bitField0_"}, {"name":"type_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Keys", "fields":[ {"name":"bitField0_"}, {"name":"storageService_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Read", "fields":[ @@ -2259,15 +2284,15 @@ {"name":"senderE164_"}, {"name":"senderUuid_"}, {"name":"timestamp_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Request", "fields":[ {"name":"bitField0_"}, {"name":"type_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Sent", "fields":[ @@ -2279,8 +2304,8 @@ {"name":"message_"}, {"name":"timestamp_"}, {"name":"unidentifiedStatus_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Sent$UnidentifiedDeliveryStatus", "fields":[ @@ -2288,8 +2313,8 @@ {"name":"destinationE164_"}, {"name":"destinationUuid_"}, {"name":"unidentified_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$StickerPackOperation", "fields":[ @@ -2297,8 +2322,8 @@ {"name":"packId_"}, {"name":"packKey_"}, {"name":"type_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$SyncMessage$Viewed", "fields":[ @@ -2306,8 +2331,8 @@ {"name":"senderE164_"}, {"name":"senderUuid_"}, {"name":"timestamp_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$TypingMessage", "fields":[ @@ -2315,8 +2340,8 @@ {"name":"bitField0_"}, {"name":"groupId_"}, {"name":"timestamp_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Verified", "fields":[ @@ -2326,22 +2351,22 @@ {"name":"identityKey_"}, {"name":"nullMessage_"}, {"name":"state_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.push.VerifyAccountResponse", "allDeclaredFields":true, "allDeclaredMethods":true, - "allDeclaredConstructors":true -}, + "allDeclaredConstructors":true} +, { "name":"org.whispersystems.signalservice.internal.serialize.protos.AddressProto", "fields":[ {"name":"bitField0_"}, {"name":"e164_"}, {"name":"uuid_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.serialize.protos.MetadataProto", "fields":[ @@ -2354,8 +2379,8 @@ {"name":"serverGuid_"}, {"name":"serverReceivedTimestamp_"}, {"name":"timestamp_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto", "fields":[ @@ -2364,20 +2389,20 @@ {"name":"data_"}, {"name":"localAddress_"}, {"name":"metadata_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.util.JsonUtil$IdentityKeyDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.whispersystems.signalservice.internal.util.JsonUtil$IdentityKeySerializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.whispersystems.signalservice.internal.util.JsonUtil$UuidDeserializer", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"org.whispersystems.signalservice.internal.websocket.WebSocketProtos$WebSocketMessage", "fields":[ @@ -2385,8 +2410,8 @@ {"name":"request_"}, {"name":"response_"}, {"name":"type_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.websocket.WebSocketProtos$WebSocketRequestMessage", "fields":[ @@ -2396,8 +2421,8 @@ {"name":"id_"}, {"name":"path_"}, {"name":"verb_"} - ] -}, + ]} +, { "name":"org.whispersystems.signalservice.internal.websocket.WebSocketProtos$WebSocketResponseMessage", "fields":[ @@ -2407,8 +2432,8 @@ {"name":"id_"}, {"name":"message_"}, {"name":"status_"} - ] -}, + ]} +, { "name":"sun.misc.Unsafe", "allDeclaredFields":true, @@ -2448,106 +2473,106 @@ {"name":"putLong","parameterTypes":["java.lang.Object","long","long"] }, {"name":"putObject","parameterTypes":["java.lang.Object","long","java.lang.Object"] }, {"name":"putShort","parameterTypes":["long","short"] } - ] -}, + ]} +, { "name":"sun.security.provider.DSA$SHA224withDSA", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.provider.JavaKeyStore$DualFormatJKS", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.provider.JavaKeyStore$JKS", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.provider.NativePRNG", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.provider.SHA", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.provider.SHA2$SHA224", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.provider.SHA2$SHA256", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.provider.SHA5$SHA384", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.provider.SHA5$SHA512", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.provider.SecureRandom", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.provider.certpath.PKIXCertPathValidator", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.rsa.PSSParameters", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.rsa.RSAKeyFactory$Legacy", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.rsa.RSAPSSSignature", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.rsa.RSASignature$SHA224withRSA", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.rsa.RSASignature$SHA256withRSA", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.rsa.RSASignature$SHA512withRSA", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.ssl.SSLContextImpl$TLSContext", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.ssl.TrustManagerFactoryImpl$PKIXFactory", - "methods":[{"name":"","parameterTypes":[] }] -}, + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"sun.security.x509.AuthorityKeyIdentifierExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]} +, { "name":"sun.security.x509.BasicConstraintsExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]} +, { "name":"sun.security.x509.CRLDistributionPointsExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]} +, { "name":"sun.security.x509.KeyUsageExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]} +, { "name":"sun.security.x509.SubjectAlternativeNameExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -}, + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]} +, { "name":"sun.security.x509.SubjectKeyIdentifierExtension", - "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }] -} + "methods":[{"name":"","parameterTypes":["java.lang.Boolean","java.lang.Object"] }]} + ] diff --git a/graalvm-config-dir/resource-config.json b/graalvm-config-dir/resource-config.json index 8153bb1c..e6532ae8 100644 --- a/graalvm-config-dir/resource-config.json +++ b/graalvm-config-dir/resource-config.json @@ -1,34 +1,92 @@ { "resources":{ "includes":[ - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AT\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CZ\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DE\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EE\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ES\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FI\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FR\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GB\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GR\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IN\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PL\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RU\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UA\\E"}, - {"pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_US\\E"}, - {"pattern":"\\Qjni/x86_64-Linux/libjffi-1.2.so\\E"}, - {"pattern":"\\Qjnr/constants/ConstantSet.class\\E"}, - {"pattern":"\\Qjnr/constants/platform/linux/ProtocolFamily.class\\E"}, - {"pattern":"\\Qjnr/constants/platform/linux/Shutdown.class\\E"}, - {"pattern":"\\Qjnr/constants/platform/linux/Sock.class\\E"}, - {"pattern":"\\Qjnr/constants/platform/linux/SocketLevel.class\\E"}, - {"pattern":"\\Qjnr/constants/platform/linux/SocketOption.class\\E"}, - {"pattern":"\\Qlibsignal_jni.so\\E"}, - {"pattern":"\\Qlibzkgroup.so\\E"}, - {"pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E"}, - {"pattern":"\\Qorg/asamk/signal/manager/config/whisper.store\\E"}, - {"pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E"}, - {"pattern":"com/google/i18n/phonenumbers/data/.*"} + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AT\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_CZ\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_DE\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_EE\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ES\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FI\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_FR\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GB\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GR\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IN\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_PL\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RU\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_UA\\E" + }, + { + "pattern":"\\Qcom/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_US\\E" + }, + { + "pattern":"\\Qjni/x86_64-Linux/libjffi-1.2.so\\E" + }, + { + "pattern":"\\Qjnr/constants/ConstantSet.class\\E" + }, + { + "pattern":"\\Qjnr/constants/platform/linux/ProtocolFamily.class\\E" + }, + { + "pattern":"\\Qjnr/constants/platform/linux/Shutdown.class\\E" + }, + { + "pattern":"\\Qjnr/constants/platform/linux/Sock.class\\E" + }, + { + "pattern":"\\Qjnr/constants/platform/linux/SocketLevel.class\\E" + }, + { + "pattern":"\\Qjnr/constants/platform/linux/SocketOption.class\\E" + }, + { + "pattern":"\\Qlibsignal_jni.so\\E" + }, + { + "pattern":"\\Qlibzkgroup.so\\E" + }, + { + "pattern":"\\Qorg/asamk/signal/manager/config/ias.store\\E" + }, + { + "pattern":"\\Qorg/asamk/signal/manager/config/whisper.store\\E" + }, + { + "pattern":"\\Qorg/slf4j/impl/StaticLoggerBinder.class\\E" + }, + { + "pattern":"com/google/i18n/phonenumbers/data/.*" + } ]}, - "bundles":[{"name":"net.sourceforge.argparse4j.internal.ArgumentParserImpl"}] + "bundles":[{ + "name":"net.sourceforge.argparse4j.internal.ArgumentParserImpl" + }] } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 57c80c01..6f367ddb 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -14,7 +14,7 @@ repositories { } dependencies { - api("com.github.turasa:signal-service-java:2.15.3_unofficial_30") + api("com.github.turasa:signal-service-java:2.15.3_unofficial_31") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("org.slf4j:slf4j-api:1.7.30") diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 7a492d9f..96188c4e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -33,6 +33,7 @@ import java.util.Base64; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.Set; @@ -228,7 +229,8 @@ public final class ProfileHelper { } private SignalServiceProfile retrieveProfileSync(String username) throws IOException { - return dependencies.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent()); + final var locale = Locale.getDefault(); + return dependencies.getMessageReceiver().retrieveProfileByUsername(username, Optional.absent(), locale); } private ProfileAndCredential retrieveProfileAndCredential( @@ -308,11 +310,16 @@ public final class ProfileHelper { var profileService = dependencies.getProfileService(); Single> responseSingle; + final var locale = Locale.getDefault(); try { - responseSingle = profileService.getProfile(address, profileKey, unidentifiedAccess, requestType); + responseSingle = profileService.getProfile(address, profileKey, unidentifiedAccess, requestType, locale); } catch (NoClassDefFoundError e) { // Native zkgroup lib not available for ProfileKey - responseSingle = profileService.getProfile(address, Optional.absent(), unidentifiedAccess, requestType); + responseSingle = profileService.getProfile(address, + Optional.absent(), + unidentifiedAccess, + requestType, + locale); } return responseSingle.map(pair -> { diff --git a/run_tests.sh b/run_tests.sh index 7ed88da9..c53dcc77 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -45,7 +45,7 @@ run_linked() { register() { NUMBER=$1 PIN=$2 - echo -n "Enter a captcha token (https://signalcaptchas.org/registration/generate.html): " + echo -n "Enter a captcha token (https://signalcaptchas.org/staging/challenge/generate.html): " read CAPTCHA run_main -u "$NUMBER" register --captcha "$CAPTCHA" echo -n "Enter validation code for ${NUMBER}: " @@ -81,6 +81,20 @@ register "$NUMBER_2" sleep 5 + +## DBus +run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m daemon_not_running || true +run_main daemon & +DAEMON_PID=$! +sleep 10 +echo send +run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m hii +run_main -u "$NUMBER_2" --dbus receive +echo kill +kill "$DAEMON_PID" +echo killed + + # JSON-RPC FIFO_FILE="${PATH_MAIN}/dbus-fifo" @@ -184,15 +198,6 @@ done run_main -u "$NUMBER_1" removeDevice -d 2 -## DBus -#run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m daemon_not_running -#run_main daemon & -#DAEMON_PID=$! -#sleep 5 -#run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m hii -#run_main -u "$NUMBER_2" --dbus receive -#kill "$DAEMON_PID" - ## Unregister run_main -u "$NUMBER_1" unregister run_main -u "$NUMBER_2" unregister --delete-account From 9b102c49d06bd6dcd59f032ddd17663f47991118 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 Oct 2021 19:16:35 +0200 Subject: [PATCH 0880/2005] Adapt behavior of receive command as dbus client to match normal mode --- .../org/asamk/signal/commands/ReceiveCommand.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index e72d8090..6b2e497e 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -128,11 +128,18 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { logger.error("Dbus client failed", e); throw new UnexpectedErrorException("Dbus client failed", e); } + + double timeout = ns.getDouble("timeout"); + long timeoutMilliseconds = timeout < 0 ? 10000 : (long) (timeout * 1000); + while (true) { try { - Thread.sleep(10000); + Thread.sleep(timeoutMilliseconds); } catch (InterruptedException ignored) { - return; + break; + } + if (timeout >= 0) { + break; } } } From b07200342ae661da49be1359852ffc5deaa6b79d Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 Oct 2021 19:32:13 +0200 Subject: [PATCH 0881/2005] Use challenge captchas for proof required exception --- src/main/java/org/asamk/signal/util/ErrorUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index 8e824d34..e40ed218 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -65,7 +65,7 @@ public class ErrorUtils { + ( failure.getOptions().contains(ProofRequiredException.Option.RECAPTCHA) ? - "To get the captcha token, go to https://signalcaptchas.org/registration/generate.html\n" + "To get the captcha token, go to https://signalcaptchas.org/challenge/generate.html\n" + "Check the developer tools (F12) console for a failed redirect to signalcaptcha://\n" + "Everything after signalcaptcha:// is the captcha token.\n" + "Use the following command to submit the captcha token:\n" From f69d9e64aaf0d6589977e53f6808b5435711a66f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 Oct 2021 19:57:31 +0200 Subject: [PATCH 0882/2005] Update dbus-java version --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index a419df8a..0353aa2e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ repositories { dependencies { implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("net.sourceforge.argparse4j:argparse4j:0.9.0") - implementation("com.github.hypfvieh:dbus-java:3.3.0") + implementation("com.github.hypfvieh:dbus-java:3.3.1") implementation("org.slf4j:slf4j-simple:1.7.30") implementation(project(":lib")) } From e83e9ae31389a564097ab68d20ebff9934ea983f Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 Oct 2021 19:57:39 +0200 Subject: [PATCH 0883/2005] Bump version --- CHANGELOG.md | 9 +++++++++ build.gradle.kts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 463419b1..a4f3bdbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased] +## [0.9.2] - 2021-10-24 +### Fixed +- dbus `listNumbers` method works again + +### Changed +- Improved provisioning error handling if the last steps fail +- Adapt behavior of receive command as dbus client to match normal mode +- Update captcha url for proof required handling + ## [0.9.1] - 2021-10-16 **Attention**: Now requires native libzkgroup version 0.8 diff --git a/build.gradle.kts b/build.gradle.kts index 0353aa2e..238f4ec6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { id("org.graalvm.buildtools.native") version "0.9.6" } -version = "0.9.1" +version = "0.9.2" java { sourceCompatibility = JavaVersion.VERSION_11 From 06aeeaa6e6df77c74907bdaa48db33376a5721d0 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 25 Oct 2021 11:39:21 +0200 Subject: [PATCH 0884/2005] Update reflect-config.json --- graalvm-config-dir/jni-config.json | 1 + graalvm-config-dir/reflect-config.json | 44 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/graalvm-config-dir/jni-config.json b/graalvm-config-dir/jni-config.json index bceda6f3..c4ebca36 100644 --- a/graalvm-config-dir/jni-config.json +++ b/graalvm-config-dir/jni-config.json @@ -92,6 +92,7 @@ , { "name":"org.whispersystems.libsignal.protocol.PlaintextContent", + "fields":[{"name":"unsafeHandle"}], "methods":[{"name":"nativeHandle","parameterTypes":[] }]} , { diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index aa3effb8..d2472035 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -26,6 +26,10 @@ { "name":"char[]"} , +{ + "name":"com.fasterxml.jackson.databind.ext.Java7HandlersImpl", + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", "methods":[{"name":"","parameterTypes":[] }]} @@ -77,10 +81,30 @@ "name":"com.sun.crypto.provider.AESCipher$General", "methods":[{"name":"","parameterTypes":[] }]} , +{ + "name":"com.sun.crypto.provider.ARCFOURCipher", + "methods":[{"name":"","parameterTypes":[] }]} +, +{ + "name":"com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305", + "methods":[{"name":"","parameterTypes":[] }]} +, +{ + "name":"com.sun.crypto.provider.DESCipher", + "methods":[{"name":"","parameterTypes":[] }]} +, +{ + "name":"com.sun.crypto.provider.DESedeCipher", + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"com.sun.crypto.provider.DHParameters", "methods":[{"name":"","parameterTypes":[] }]} , +{ + "name":"com.sun.crypto.provider.GaloisCounterMode$AESGCM", + "methods":[{"name":"","parameterTypes":[] }]} +, { "name":"com.sun.crypto.provider.HmacCore$HmacSHA256", "methods":[{"name":"","parameterTypes":[] }]} @@ -121,6 +145,10 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true} , +{ + "name":"java.lang.Class", + "methods":[{"name":"getRecordComponents","parameterTypes":[] }]} +, { "name":"java.lang.Comparable", "allDeclaredMethods":true} @@ -154,6 +182,11 @@ "allDeclaredFields":true, "allDeclaredMethods":true} , +{ + "name":"java.lang.Record", + "allDeclaredFields":true, + "queryAllDeclaredMethods":true} +, { "name":"java.lang.String", "allPublicMethods":true} @@ -162,6 +195,13 @@ "name":"java.lang.reflect.Method", "methods":[{"name":"isDefault","parameterTypes":[] }]} , +{ + "name":"java.lang.reflect.RecordComponent", + "methods":[ + {"name":"getName","parameterTypes":[] }, + {"name":"getType","parameterTypes":[] } + ]} +, { "name":"java.nio.Buffer", "allDeclaredMethods":true, @@ -1148,6 +1188,10 @@ "name":"org.freedesktop.dbus.errors.ServiceUnknown", "methods":[{"name":"","parameterTypes":["java.lang.String"] }]} , +{ + "name":"org.freedesktop.dbus.errors.UnknownObject", + "methods":[{"name":"","parameterTypes":["java.lang.String"] }]} +, { "name":"org.freedesktop.dbus.interfaces.DBus$NameAcquired", "allDeclaredConstructors":true} From 95a27c8ec462aeed58f91cad86aeacde99854c83 Mon Sep 17 00:00:00 2001 From: AsamK Date: Wed, 27 Oct 2021 14:01:58 +0200 Subject: [PATCH 0885/2005] Update tests --- run_tests.sh | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/run_tests.sh b/run_tests.sh index c53dcc77..b3b51835 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -83,16 +83,13 @@ sleep 5 ## DBus -run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m daemon_not_running || true -run_main daemon & -DAEMON_PID=$! -sleep 10 -echo send -run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m hii -run_main -u "$NUMBER_2" --dbus receive -echo kill -kill "$DAEMON_PID" -echo killed +#run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m daemon_not_running || true +#run_main daemon & +#DAEMON_PID=$! +#sleep 10 +#run_main -u "$NUMBER_1" --dbus send "$NUMBER_2" -m hii +#run_main -u "$NUMBER_2" --dbus receive +#kill "$DAEMON_PID" # JSON-RPC From 69b7e730635a0da45f5074a22506edcfc95ef33a Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 26 Oct 2021 21:40:48 +0200 Subject: [PATCH 0886/2005] Update slf4j --- build.gradle.kts | 2 +- lib/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 238f4ec6..f59bc2d0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,7 +35,7 @@ dependencies { implementation("org.bouncycastle:bcprov-jdk15on:1.69") implementation("net.sourceforge.argparse4j:argparse4j:0.9.0") implementation("com.github.hypfvieh:dbus-java:3.3.1") - implementation("org.slf4j:slf4j-simple:1.7.30") + implementation("org.slf4j:slf4j-simple:1.7.32") implementation(project(":lib")) } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 6f367ddb..d720f3b9 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { api("com.github.turasa:signal-service-java:2.15.3_unofficial_31") implementation("com.google.protobuf:protobuf-javalite:3.10.0") implementation("org.bouncycastle:bcprov-jdk15on:1.69") - implementation("org.slf4j:slf4j-api:1.7.30") + implementation("org.slf4j:slf4j-api:1.7.32") } configurations { From 4e69b34efe78e7bad03e549c966d986b47777668 Mon Sep 17 00:00:00 2001 From: AsamK Date: Tue, 26 Oct 2021 21:41:00 +0200 Subject: [PATCH 0887/2005] Update documentation --- man/signal-cli-dbus.5.adoc | 4 ++-- man/signal-cli.1.adoc | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/man/signal-cli-dbus.5.adoc b/man/signal-cli-dbus.5.adoc index 55058580..4c317db3 100755 --- a/man/signal-cli-dbus.5.adoc +++ b/man/signal-cli-dbus.5.adoc @@ -58,7 +58,7 @@ link(newDeviceName) -> deviceLinkUri:: * newDeviceName : Name to give new device (defaults to "cli" if no name is given) * deviceLinkUri : URI of newly linked device -Returns a URI of the form "sgnl://linkdevice/?uuid=...". This can be piped to a QR encoder to create a display that +Returns a URI of the form "sgnl://linkdevice?uuid=...". This can be piped to a QR encoder to create a display that can be captured by a Signal smartphone client. For example: `dbus-send --session --dest=org.asamk.Signal --type=method_call --print-reply /org/asamk/Signal org.asamk.Signal.link string:"My secondary client"|tr '\n' '\0'|sed 's/.*string //g'|sed 's/\"//g'|qrencode -s10 -tANSI256` @@ -311,7 +311,7 @@ The following methods listen to the recipient's object path, which is constructe * DBusNumber : recipient's phone number, with underscore (_) replacing plus (+) addDevice(deviceUri) -> <>:: -* deviceUri : URI in the form of "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. +* deviceUri : URI in the form of "sgnl://linkdevice?uuid=..." (formerly "tsdevice:/?uuid=...") Normally displayed by a Signal desktop app, smartphone app, or another signal-cli instance using the `link` control method. getDevice(deviceId) -> devicePath:: * deviceId : Long representing a deviceId diff --git a/man/signal-cli.1.adoc b/man/signal-cli.1.adoc index f2a1a960..334f7407 100644 --- a/man/signal-cli.1.adoc +++ b/man/signal-cli.1.adoc @@ -144,7 +144,7 @@ Remove the registration lock pin. === link Link to an existing device, instead of registering a new number. -This shows a "sgnl://linkdevice/?uuid=..." URI. If you want to connect to another signal-cli instance, you can just use this URI. +This shows a "sgnl://linkdevice?uuid=..." URI. If you want to connect to another signal-cli instance, you can just use this URI. If you want to link to an Android/iOS device, create a QR code with the URI (e.g. with qrencode) and scan that in the Signal app. *-n* NAME, *--name* NAME:: @@ -158,7 +158,7 @@ Only works, if this is the master device. *--uri* URI:: Specify the uri contained in the QR code shown by the new device. -You will need the full URI such as "sgnl://linkdevice/?uuid=..." (formerly "tsdevice:/?uuid=...") +You will need the full URI such as "sgnl://linkdevice?uuid=..." (formerly "tsdevice:/?uuid=...") Make sure to enclose it in quotation marks for shells. === listDevices From fc5af35a04152d2759a73f545f4bfd5056089867 Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 25 Oct 2021 15:33:40 +0200 Subject: [PATCH 0888/2005] Replace File.delete with Files.delete --- .../asamk/signal/manager/storage/groups/GroupStore.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java index fe8f85a6..b38d3902 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -30,6 +30,7 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; @@ -122,7 +123,11 @@ public class GroupStore { } final var groupFileLegacy = getGroupV2FileLegacy(group.getGroupId()); if (groupFileLegacy.exists()) { - groupFileLegacy.delete(); + try { + Files.delete(groupFileLegacy.toPath()); + } catch (IOException e) { + logger.error("Failed to delete legacy group file {}: {}", groupFileLegacy, e.getMessage()); + } } } catch (IOException e) { logger.warn("Failed to cache group, ignoring: {}", e.getMessage()); From 9cb1409918fe151422cc557997aa5691c01284b6 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 Oct 2021 20:46:33 +0200 Subject: [PATCH 0889/2005] Fix unlikely issues with null values --- .../manager/storage/senderKeys/SenderKeyRecordStore.java | 4 ++-- .../java/org/asamk/signal/manager/util/MessageCacheUtils.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java index f84903e4..f0bbddc6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/senderKeys/SenderKeyRecordStore.java @@ -95,7 +95,7 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups return; } - logger.debug("Only to be merged recipient had sender keys, re-assigning to the new recipient."); + logger.debug("To be merged recipient had sender keys, re-assigning to the new recipient."); for (var key : keys) { final var toBeMergedSenderKey = loadSenderKeyLocked(key); deleteSenderKeyLocked(key); @@ -108,7 +108,7 @@ public class SenderKeyRecordStore implements org.whispersystems.libsignal.groups if (senderKeyRecord != null) { continue; } - storeSenderKeyLocked(newKey, senderKeyRecord); + storeSenderKeyLocked(newKey, toBeMergedSenderKey); } } } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java index 66b14296..ed94f39e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/MessageCacheUtils.java @@ -59,7 +59,7 @@ public class MessageCacheUtils { if (version >= 4) { serverDeliveredTimestamp = in.readLong(); } - Optional addressOptional = sourceUuid == null && source.isEmpty() + Optional addressOptional = sourceUuid == null ? Optional.absent() : Optional.of(new SignalServiceAddress(sourceUuid, source)); return new SignalServiceEnvelope(type, From ce70a623c21a267679d59838d041f9bc1d486cd9 Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 Oct 2021 21:06:13 +0200 Subject: [PATCH 0890/2005] Use Java 17 --- .github/workflows/ci.yml | 2 +- .github/workflows/codeql-analysis.yml | 2 +- CHANGELOG.md | 1 + README.md | 2 +- build.gradle.kts | 5 ++- lib/build.gradle.kts | 4 +- .../asamk/signal/manager/LibSignalLogger.java | 21 +++------ .../org/asamk/signal/manager/ManagerImpl.java | 9 ++-- .../manager/SignalWebSocketHealthMonitor.java | 12 ++--- .../org/asamk/signal/manager/TrustLevel.java | 45 +++++++------------ .../SendRetryMessageRequestAction.java | 17 +++---- .../manager/api/RecipientIdentifier.java | 12 ++--- .../signal/manager/api/TypingAction.java | 12 ++--- .../signal/manager/config/ServiceConfig.java | 24 +++++----- .../asamk/signal/manager/groups/GroupId.java | 2 +- .../signal/manager/groups/GroupIdV1.java | 2 +- .../signal/manager/groups/GroupIdV2.java | 2 +- .../manager/groups/GroupInviteLinkUrl.java | 5 +-- .../signal/manager/helper/GroupHelper.java | 3 +- .../signal/manager/helper/GroupV2Helper.java | 27 ++++------- .../signal/manager/helper/SyncHelper.java | 3 +- .../manager/storage/groups/GroupInfo.java | 2 +- .../manager/storage/groups/GroupInfoV1.java | 2 +- .../manager/storage/groups/GroupInfoV2.java | 13 +++--- .../manager/storage/groups/GroupStore.java | 12 ++--- .../storage/recipients/RecipientAddress.java | 2 +- .../signal/manager/util/ProfileUtils.java | 13 +++--- run_tests.sh | 2 +- src/main/java/org/asamk/SignalControl.java | 2 +- .../asamk/signal/ReceiveMessageHandler.java | 3 +- .../signal/commands/GetUserStatusCommand.java | 3 +- .../signal/commands/JoinGroupCommand.java | 3 +- .../signal/commands/ListContactsCommand.java | 3 +- .../signal/commands/ListDevicesCommand.java | 3 +- .../signal/commands/ListGroupsCommand.java | 3 +- .../commands/ListIdentitiesCommand.java | 3 +- .../signal/commands/QuitGroupCommand.java | 3 +- .../asamk/signal/commands/ReceiveCommand.java | 3 +- .../signal/commands/RegisterCommand.java | 9 ++-- .../signal/commands/RemoteDeleteCommand.java | 3 +- .../asamk/signal/commands/SendCommand.java | 3 +- .../signal/commands/SendReactionCommand.java | 3 +- .../signal/commands/UpdateGroupCommand.java | 35 +++++---------- .../commands/UploadStickerPackCommand.java | 3 +- .../asamk/signal/dbus/DbusManagerImpl.java | 12 ++--- .../signal/json/JsonMessageEnvelope.java | 3 +- .../signal/jsonrpc/JsonRpcBulkMessage.java | 2 +- .../asamk/signal/jsonrpc/JsonRpcMessage.java | 2 +- .../asamk/signal/jsonrpc/JsonRpcRequest.java | 2 +- .../asamk/signal/jsonrpc/JsonRpcResponse.java | 2 +- .../org/asamk/signal/util/ErrorUtils.java | 12 ++--- 51 files changed, 142 insertions(+), 236 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fb5f7f4..ff73584b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '11', '17' ] + java: [ '17' ] steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c55e656d..0ef62f7e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -19,7 +19,7 @@ jobs: - name: Setup Java JDK uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - name: Checkout repository uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f3bdbe..3e60c8a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## [Unreleased] +**Attention**: Now requires Java 17 ## [0.9.2] - 2021-10-24 ### Fixed diff --git a/README.md b/README.md index fe435849..b041b4a0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ It also has a JSON-RPC based interface, see the [documentation](https://github.c You can [build signal-cli](#building) yourself, or use the [provided binary files](https://github.com/AsamK/signal-cli/releases/latest), which should work on Linux, macOS and Windows. For Arch Linux there is also a [package in AUR](https://aur.archlinux.org/packages/signal-cli/) and there is a [FreeBSD port](https://www.freshports.org/net-im/signal-cli) available as well. System requirements: -- at least Java Runtime Environment (JRE) 11 +- at least Java Runtime Environment (JRE) 17 - native libraries: libzkgroup, libsignal-client Those are bundled for x86_64 Linux (with recent enough glibc, see #643), for other systems/architectures see: [Provide native lib for libsignal](https://github.com/AsamK/signal-cli/wiki/Provide-native-lib-for-libsignal) diff --git a/build.gradle.kts b/build.gradle.kts index f59bc2d0..481f2668 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,8 +9,8 @@ plugins { version = "0.9.2" java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } application { @@ -22,6 +22,7 @@ graalvmNative { this["main"].run { configurationFileDirectories.from(file("graalvm-config-dir")) buildArgs.add("--allow-incomplete-classpath") + buildArgs.add("--report-unsupported-elements-at-runtime") } } } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index d720f3b9..31de50e0 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -4,8 +4,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } repositories { diff --git a/lib/src/main/java/org/asamk/signal/manager/LibSignalLogger.java b/lib/src/main/java/org/asamk/signal/manager/LibSignalLogger.java index 3be4d7e9..c14b0f13 100644 --- a/lib/src/main/java/org/asamk/signal/manager/LibSignalLogger.java +++ b/lib/src/main/java/org/asamk/signal/manager/LibSignalLogger.java @@ -20,22 +20,11 @@ public class LibSignalLogger implements SignalProtocolLogger { public void log(final int priority, final String tag, final String message) { final var logMessage = String.format("[%s]: %s", tag, message); switch (priority) { - case SignalProtocolLogger.VERBOSE: - logger.trace(logMessage); - break; - case SignalProtocolLogger.DEBUG: - logger.debug(logMessage); - break; - case SignalProtocolLogger.INFO: - logger.info(logMessage); - break; - case SignalProtocolLogger.WARN: - logger.warn(logMessage); - break; - case SignalProtocolLogger.ERROR: - case SignalProtocolLogger.ASSERT: - logger.error(logMessage); - break; + case SignalProtocolLogger.VERBOSE -> logger.trace(logMessage); + case SignalProtocolLogger.DEBUG -> logger.debug(logMessage); + case SignalProtocolLogger.INFO -> logger.info(logMessage); + case SignalProtocolLogger.WARN -> logger.warn(logMessage); + case SignalProtocolLogger.ERROR, SignalProtocolLogger.ASSERT -> logger.error(logMessage); } } } diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index 2ea96591..1a1e735e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -569,16 +569,15 @@ public class ManagerImpl implements Manager { long timestamp = System.currentTimeMillis(); messageBuilder.withTimestamp(timestamp); for (final var recipient : recipients) { - if (recipient instanceof RecipientIdentifier.Single) { - final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); + if (recipient instanceof RecipientIdentifier.Single single) { + final var recipientId = resolveRecipient(single); final var result = sendHelper.sendMessage(messageBuilder, recipientId); results.put(recipient, List.of(result)); } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { final var result = sendHelper.sendSelfMessage(messageBuilder); results.put(recipient, List.of(result)); - } else if (recipient instanceof RecipientIdentifier.Group) { - final var groupId = ((RecipientIdentifier.Group) recipient).groupId; - final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId); + } else if (recipient instanceof RecipientIdentifier.Group group) { + final var result = sendHelper.sendAsGroupMessage(messageBuilder, group.groupId); results.put(recipient, result); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java b/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java index 556e227d..b905f9b7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java +++ b/lib/src/main/java/org/asamk/signal/manager/SignalWebSocketHealthMonitor.java @@ -63,15 +63,9 @@ public final class SignalWebSocketHealthMonitor implements HealthMonitor { private synchronized void onStateChange(WebSocketConnectionState connectionState, HealthState healthState) { switch (connectionState) { - case CONNECTED: - logger.debug("WebSocket is now connected"); - break; - case AUTHENTICATION_FAILED: - logger.debug("WebSocket authentication failed"); - break; - case FAILED: - logger.debug("WebSocket connection failed"); - break; + case CONNECTED -> logger.debug("WebSocket is now connected"); + case AUTHENTICATION_FAILED -> logger.debug("WebSocket authentication failed"); + case FAILED -> logger.debug("WebSocket connection failed"); } healthState.needsKeepAlive = connectionState == WebSocketConnectionState.CONNECTED; diff --git a/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java b/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java index 5c712866..fead442c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java +++ b/lib/src/main/java/org/asamk/signal/manager/TrustLevel.java @@ -18,40 +18,27 @@ public enum TrustLevel { } public static TrustLevel fromIdentityState(ContactRecord.IdentityState identityState) { - switch (identityState) { - case DEFAULT: - return TRUSTED_UNVERIFIED; - case UNVERIFIED: - return UNTRUSTED; - case VERIFIED: - return TRUSTED_VERIFIED; - case UNRECOGNIZED: - return null; - } - throw new RuntimeException("Unknown identity state: " + identityState); + return switch (identityState) { + case DEFAULT -> TRUSTED_UNVERIFIED; + case UNVERIFIED -> UNTRUSTED; + case VERIFIED -> TRUSTED_VERIFIED; + case UNRECOGNIZED -> null; + }; } public static TrustLevel fromVerifiedState(VerifiedMessage.VerifiedState verifiedState) { - switch (verifiedState) { - case DEFAULT: - return TRUSTED_UNVERIFIED; - case UNVERIFIED: - return UNTRUSTED; - case VERIFIED: - return TRUSTED_VERIFIED; - } - throw new RuntimeException("Unknown verified state: " + verifiedState); + return switch (verifiedState) { + case DEFAULT -> TRUSTED_UNVERIFIED; + case UNVERIFIED -> UNTRUSTED; + case VERIFIED -> TRUSTED_VERIFIED; + }; } public VerifiedMessage.VerifiedState toVerifiedState() { - switch (this) { - case TRUSTED_UNVERIFIED: - return VerifiedMessage.VerifiedState.DEFAULT; - case UNTRUSTED: - return VerifiedMessage.VerifiedState.UNVERIFIED; - case TRUSTED_VERIFIED: - return VerifiedMessage.VerifiedState.VERIFIED; - } - throw new RuntimeException("Unknown verified state: " + this); + return switch (this) { + case TRUSTED_UNVERIFIED -> VerifiedMessage.VerifiedState.DEFAULT; + case UNTRUSTED -> VerifiedMessage.VerifiedState.UNVERIFIED; + case TRUSTED_VERIFIED -> VerifiedMessage.VerifiedState.VERIFIED; + }; } } diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java index ecd5597d..3ecd9a1e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendRetryMessageRequestAction.java @@ -54,17 +54,12 @@ public class SendRetryMessageRequestAction implements HandleAction { } private static int envelopeTypeToCiphertextMessageType(int envelopeType) { - switch (envelopeType) { - case SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE: - return CiphertextMessage.PREKEY_TYPE; - case SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE: - return CiphertextMessage.SENDERKEY_TYPE; - case SignalServiceProtos.Envelope.Type.PLAINTEXT_CONTENT_VALUE: - return CiphertextMessage.PLAINTEXT_CONTENT_TYPE; - case SignalServiceProtos.Envelope.Type.CIPHERTEXT_VALUE: - default: - return CiphertextMessage.WHISPER_TYPE; - } + return switch (envelopeType) { + case SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE -> CiphertextMessage.PREKEY_TYPE; + case SignalServiceProtos.Envelope.Type.UNIDENTIFIED_SENDER_VALUE -> CiphertextMessage.SENDERKEY_TYPE; + case SignalServiceProtos.Envelope.Type.PLAINTEXT_CONTENT_VALUE -> CiphertextMessage.PLAINTEXT_CONTENT_TYPE; + default -> CiphertextMessage.WHISPER_TYPE; + }; } @Override diff --git a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java index be1029e6..ec2d00f5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/RecipientIdentifier.java @@ -9,9 +9,9 @@ import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.UUID; -public abstract class RecipientIdentifier { +public sealed abstract class RecipientIdentifier { - public static class NoteToSelf extends RecipientIdentifier { + public static final class NoteToSelf extends RecipientIdentifier { public static NoteToSelf INSTANCE = new NoteToSelf(); @@ -19,7 +19,7 @@ public abstract class RecipientIdentifier { } } - public abstract static class Single extends RecipientIdentifier { + public sealed static abstract class Single extends RecipientIdentifier { public static Single fromString(String identifier, String localNumber) throws InvalidNumberException { return UuidUtil.isUuid(identifier) @@ -43,7 +43,7 @@ public abstract class RecipientIdentifier { public abstract String getIdentifier(); } - public static class Uuid extends Single { + public static final class Uuid extends Single { public final UUID uuid; @@ -72,7 +72,7 @@ public abstract class RecipientIdentifier { } } - public static class Number extends Single { + public static final class Number extends Single { public final String number; @@ -101,7 +101,7 @@ public abstract class RecipientIdentifier { } } - public static class Group extends RecipientIdentifier { + public static final class Group extends RecipientIdentifier { public final GroupId groupId; diff --git a/lib/src/main/java/org/asamk/signal/manager/api/TypingAction.java b/lib/src/main/java/org/asamk/signal/manager/api/TypingAction.java index 228990c1..86a858f0 100644 --- a/lib/src/main/java/org/asamk/signal/manager/api/TypingAction.java +++ b/lib/src/main/java/org/asamk/signal/manager/api/TypingAction.java @@ -7,13 +7,9 @@ public enum TypingAction { STOP; public SignalServiceTypingMessage.Action toSignalService() { - switch (this) { - case START: - return SignalServiceTypingMessage.Action.STARTED; - case STOP: - return SignalServiceTypingMessage.Action.STOPPED; - default: - throw new IllegalStateException("Invalid typing action " + this); - } + return switch (this) { + case START -> SignalServiceTypingMessage.Action.STARTED; + case STOP -> SignalServiceTypingMessage.Action.STOPPED; + }; } } diff --git a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java index a9a08d93..2634a593 100644 --- a/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java +++ b/lib/src/main/java/org/asamk/signal/manager/config/ServiceConfig.java @@ -88,19 +88,15 @@ public class ServiceConfig { final var interceptors = List.of(userAgentInterceptor); - switch (serviceEnvironment) { - case LIVE: - return new ServiceEnvironmentConfig(LiveConfig.createDefaultServiceConfiguration(interceptors), - LiveConfig.getUnidentifiedSenderTrustRoot(), - LiveConfig.createKeyBackupConfig(), - LiveConfig.getCdsMrenclave()); - case SANDBOX: - return new ServiceEnvironmentConfig(SandboxConfig.createDefaultServiceConfiguration(interceptors), - SandboxConfig.getUnidentifiedSenderTrustRoot(), - SandboxConfig.createKeyBackupConfig(), - SandboxConfig.getCdsMrenclave()); - default: - throw new IllegalArgumentException("Unsupported environment"); - } + return switch (serviceEnvironment) { + case LIVE -> new ServiceEnvironmentConfig(LiveConfig.createDefaultServiceConfiguration(interceptors), + LiveConfig.getUnidentifiedSenderTrustRoot(), + LiveConfig.createKeyBackupConfig(), + LiveConfig.getCdsMrenclave()); + case SANDBOX -> new ServiceEnvironmentConfig(SandboxConfig.createDefaultServiceConfiguration(interceptors), + SandboxConfig.getUnidentifiedSenderTrustRoot(), + SandboxConfig.createKeyBackupConfig(), + SandboxConfig.getCdsMrenclave()); + }; } } diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/GroupId.java b/lib/src/main/java/org/asamk/signal/manager/groups/GroupId.java index 9ecb9630..38ddd4b6 100644 --- a/lib/src/main/java/org/asamk/signal/manager/groups/GroupId.java +++ b/lib/src/main/java/org/asamk/signal/manager/groups/GroupId.java @@ -3,7 +3,7 @@ package org.asamk.signal.manager.groups; import java.util.Arrays; import java.util.Base64; -public abstract class GroupId { +public abstract sealed class GroupId permits GroupIdV1, GroupIdV2 { private final byte[] id; diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java b/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java index d2012fa0..acec587b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java +++ b/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV1.java @@ -4,7 +4,7 @@ import java.util.Base64; import static org.asamk.signal.manager.util.KeyUtils.getSecretBytes; -public class GroupIdV1 extends GroupId { +public final class GroupIdV1 extends GroupId { public static GroupIdV1 createRandom() { return new GroupIdV1(getSecretBytes(16)); diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java b/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java index 913a9e93..35aac330 100644 --- a/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/groups/GroupIdV2.java @@ -2,7 +2,7 @@ package org.asamk.signal.manager.groups; import java.util.Base64; -public class GroupIdV2 extends GroupId { +public final class GroupIdV2 extends GroupId { public static GroupIdV2 fromBase64(String groupId) { return new GroupIdV2(Base64.getDecoder().decode(groupId)); diff --git a/lib/src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java b/lib/src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java index dd9dd2d2..0498fba1 100644 --- a/lib/src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java +++ b/lib/src/main/java/org/asamk/signal/manager/groups/GroupInviteLinkUrl.java @@ -56,7 +56,7 @@ public final class GroupInviteLinkUrl { var groupInviteLink = GroupInviteLink.parseFrom(bytes); switch (groupInviteLink.getContentsCase()) { - case V1CONTENTS: { + case V1CONTENTS -> { var groupInviteLinkContentsV1 = groupInviteLink.getV1Contents(); var groupMasterKey = new GroupMasterKey(groupInviteLinkContentsV1.getGroupMasterKey() .toByteArray()); @@ -65,8 +65,7 @@ public final class GroupInviteLinkUrl { return new GroupInviteLinkUrl(groupMasterKey, password); } - default: - throw new UnknownGroupLinkVersionException("Url contains no known group link content"); + default -> throw new UnknownGroupLinkVersionException("Url contains no known group link content"); } } catch (InvalidInputException | IOException e) { throw new InvalidGroupLinkException(e); diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index ee2e9416..dbee3e84 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -351,8 +351,7 @@ public class GroupHelper { private GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { final var group = account.getGroupStore().getGroup(groupId); - if (group instanceof GroupInfoV2) { - final var groupInfoV2 = (GroupInfoV2) group; + if (group instanceof GroupInfoV2 groupInfoV2) { if (forceUpdate || (!groupInfoV2.isPermissionDenied() && groupInfoV2.getGroup() == null)) { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()); DecryptedGroup decryptedGroup; diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 746af2f9..f526a596 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -391,27 +391,18 @@ public class GroupV2Helper { } private AccessControl.AccessRequired toAccessControl(final GroupLinkState state) { - switch (state) { - case DISABLED: - return AccessControl.AccessRequired.UNSATISFIABLE; - case ENABLED: - return AccessControl.AccessRequired.ANY; - case ENABLED_WITH_APPROVAL: - return AccessControl.AccessRequired.ADMINISTRATOR; - default: - throw new AssertionError(); - } + return switch (state) { + case DISABLED -> AccessControl.AccessRequired.UNSATISFIABLE; + case ENABLED -> AccessControl.AccessRequired.ANY; + case ENABLED_WITH_APPROVAL -> AccessControl.AccessRequired.ADMINISTRATOR; + }; } private AccessControl.AccessRequired toAccessControl(final GroupPermission permission) { - switch (permission) { - case EVERY_MEMBER: - return AccessControl.AccessRequired.MEMBER; - case ONLY_ADMINS: - return AccessControl.AccessRequired.ADMINISTRATOR; - default: - throw new AssertionError(); - } + return switch (permission) { + case EVERY_MEMBER -> AccessControl.AccessRequired.MEMBER; + case ONLY_ADMINS -> AccessControl.AccessRequired.ADMINISTRATOR; + }; } private GroupsV2Operations.GroupOperations getGroupOperations(final GroupInfoV2 groupInfoV2) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index e3fc7fc2..6fe04dce 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -83,8 +83,7 @@ public class SyncHelper { try (OutputStream fos = new FileOutputStream(groupsFile)) { var out = new DeviceGroupsOutputStream(fos); for (var record : account.getGroupStore().getGroups()) { - if (record instanceof GroupInfoV1) { - var groupInfo = (GroupInfoV1) record; + if (record instanceof GroupInfoV1 groupInfo) { out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), Optional.fromNullable(groupInfo.name), groupInfo.getMembers() diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java index b4f4e63a..2c210259 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfo.java @@ -9,7 +9,7 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -public abstract class GroupInfo { +public sealed abstract class GroupInfo permits GroupInfoV1, GroupInfoV2 { public abstract GroupId getGroupId(); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java index dbd2dcbb..4e759e5f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV1.java @@ -11,7 +11,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.Set; -public class GroupInfoV1 extends GroupInfo { +public final class GroupInfoV1 extends GroupInfo { private final GroupIdV1 groupId; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java index 34db2a28..793cde5d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupInfoV2.java @@ -15,7 +15,7 @@ import org.whispersystems.signalservice.api.util.UuidUtil; import java.util.Set; import java.util.stream.Collectors; -public class GroupInfoV2 extends GroupInfo { +public final class GroupInfoV2 extends GroupInfo { private final GroupIdV2 groupId; private final GroupMasterKey masterKey; @@ -197,12 +197,9 @@ public class GroupInfoV2 extends GroupInfo { } private static GroupPermission toGroupPermission(final AccessControl.AccessRequired permission) { - switch (permission) { - case ADMINISTRATOR: - return GroupPermission.ONLY_ADMINS; - case MEMBER: - default: - return GroupPermission.EVERY_MEMBER; - } + return switch (permission) { + case ADMINISTRATOR -> GroupPermission.ONLY_ADMINS; + default -> GroupPermission.EVERY_MEMBER; + }; } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java index b38d3902..5cef1b33 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/groups/GroupStore.java @@ -75,8 +75,7 @@ public class GroupStore { final Saver saver ) { final var groups = storage.groups.stream().map(g -> { - if (g instanceof Storage.GroupV1) { - final var g1 = (Storage.GroupV1) g; + if (g instanceof Storage.GroupV1 g1) { final var members = g1.members.stream().map(m -> { if (m.recipientId == null) { return recipientResolver.resolveRecipient(new RecipientAddress(UuidUtil.parseOrNull(m.uuid), @@ -186,8 +185,7 @@ public class GroupStore { synchronized (groups) { var modified = false; for (var group : this.groups.values()) { - if (group instanceof GroupInfoV1) { - var groupV1 = (GroupInfoV1) group; + if (group instanceof GroupInfoV1 groupV1) { if (groupV1.isMember(toBeMergedRecipientId)) { groupV1.removeMember(toBeMergedRecipientId); groupV1.addMembers(List.of(recipientId)); @@ -220,8 +218,7 @@ public class GroupStore { private GroupInfoV1 getGroupV1ByV2IdLocked(GroupIdV2 groupIdV2) { for (var g : groups.values()) { - if (g instanceof GroupInfoV1) { - final var gv1 = (GroupInfoV1) g; + if (g instanceof GroupInfoV1 gv1) { if (groupIdV2.equals(gv1.getExpectedV2Id())) { return gv1; } @@ -256,8 +253,7 @@ public class GroupStore { private Storage toStorageLocked() { return new Storage(groups.values().stream().map(g -> { - if (g instanceof GroupInfoV1) { - final var g1 = (GroupInfoV1) g; + if (g instanceof GroupInfoV1 g1) { return new Storage.GroupV1(g1.getGroupId().toBase64(), g1.getExpectedV2Id().toBase64(), g1.name, diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java index c0f5b0b8..bd8710dd 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientAddress.java @@ -19,7 +19,7 @@ public class RecipientAddress { */ public RecipientAddress(Optional uuid, Optional e164) { uuid = uuid.isPresent() && uuid.get().equals(UuidUtil.UNKNOWN_UUID) ? Optional.empty() : uuid; - if (!uuid.isPresent() && !e164.isPresent()) { + if (uuid.isEmpty() && e164.isEmpty()) { throw new AssertionError("Must have either a UUID or E164 number!"); } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java index c1b1183c..0f5b7407 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java @@ -91,14 +91,11 @@ public class ProfileUtils { } String[] parts = name.split("\0"); - switch (parts.length) { - case 0: - return new Pair<>(null, null); - case 1: - return new Pair<>(parts[0], null); - default: - return new Pair<>(parts[0], parts[1]); - } + return switch (parts.length) { + case 0 -> new Pair<>(null, null); + case 1 -> new Pair<>(parts[0], null); + default -> new Pair<>(parts[0], parts[1]); + }; } static String trimZeros(String str) { diff --git a/run_tests.sh b/run_tests.sh index b3b51835..b41a460d 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -6,7 +6,7 @@ fi set -e # To update graalvm config, set GRAALVM_HOME, e.g: -# export GRAALVM_HOME=/usr/lib/jvm/java-11-graalvm +# export GRAALVM_HOME=/usr/lib/jvm/java-17-graalvm if [ ! -z "$GRAALVM_HOME" ]; then export JAVA_HOME=$GRAALVM_HOME export SIGNAL_CLI_OPTS='-agentlib:native-image-agent=config-merge-dir=graalvm-config-dir/' diff --git a/src/main/java/org/asamk/SignalControl.java b/src/main/java/org/asamk/SignalControl.java index 911ccb61..610ca103 100644 --- a/src/main/java/org/asamk/SignalControl.java +++ b/src/main/java/org/asamk/SignalControl.java @@ -26,7 +26,7 @@ public interface SignalControl extends DBusInterface { String link(String newDeviceName) throws Error.Failure; - public String version(); + String version(); List listAccounts(); diff --git a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java index b1580b34..d86ab26c 100644 --- a/src/main/java/org/asamk/signal/ReceiveMessageHandler.java +++ b/src/main/java/org/asamk/signal/ReceiveMessageHandler.java @@ -53,8 +53,7 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler { writer.println("Got receipt."); } else if (envelope.isSignalMessage() || envelope.isPreKeySignalMessage() || envelope.isUnidentifiedSender()) { if (exception != null) { - if (exception instanceof UntrustedIdentityException) { - var e = (UntrustedIdentityException) exception; + if (exception instanceof UntrustedIdentityException e) { writer.println( "The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message."); final var recipientName = getLegacyIdentifier(m.resolveSignalServiceAddress(e.getSender())); diff --git a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java index be94fb36..e5685f1a 100644 --- a/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java +++ b/src/main/java/org/asamk/signal/commands/GetUserStatusCommand.java @@ -47,8 +47,7 @@ public class GetUserStatusCommand implements JsonRpcLocalCommand { } // Output - if (outputWriter instanceof JsonWriter) { - final var jsonWriter = (JsonWriter) outputWriter; + if (outputWriter instanceof JsonWriter jsonWriter) { var jsonUserStatuses = registered.entrySet().stream().map(entry -> { final var number = entry.getValue().first(); diff --git a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java index 1e06ea9c..892879f6 100644 --- a/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/JoinGroupCommand.java @@ -55,8 +55,7 @@ public class JoinGroupCommand implements JsonRpcLocalCommand { try { final var results = m.joinGroup(linkUrl); var newGroupId = results.first(); - if (outputWriter instanceof JsonWriter) { - final var writer = (JsonWriter) outputWriter; + if (outputWriter instanceof JsonWriter writer) { if (!m.getGroup(newGroupId).isMember()) { writer.write(Map.of("groupId", newGroupId.toBase64(), "onlyRequested", true)); } else { diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index b6dfc3ce..2d0f6322 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -27,8 +27,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand { public void handleCommand(final Namespace ns, final Manager m, final OutputWriter outputWriter) { var contacts = m.getContacts(); - if (outputWriter instanceof PlainTextWriter) { - final var writer = (PlainTextWriter) outputWriter; + if (outputWriter instanceof PlainTextWriter writer) { for (var c : contacts) { final var contact = c.second(); writer.println("Number: {} Name: {} Blocked: {} Message expiration: {}", diff --git a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java index 1de5b842..d9a5ea33 100644 --- a/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListDevicesCommand.java @@ -43,8 +43,7 @@ public class ListDevicesCommand implements JsonRpcLocalCommand { throw new IOErrorException("Failed to get linked devices: " + e.getMessage(), e); } - if (outputWriter instanceof PlainTextWriter) { - final var writer = (PlainTextWriter) outputWriter; + if (outputWriter instanceof PlainTextWriter writer) { for (var d : devices) { writer.println("- Device {}{}:", d.getId(), (d.isThisDevice() ? " (this device)" : "")); writer.indent(w -> { diff --git a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java index b2182429..3321c34b 100644 --- a/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListGroupsCommand.java @@ -80,8 +80,7 @@ public class ListGroupsCommand implements JsonRpcLocalCommand { ) throws CommandException { final var groups = m.getGroups(); - if (outputWriter instanceof JsonWriter) { - final var jsonWriter = (JsonWriter) outputWriter; + if (outputWriter instanceof JsonWriter jsonWriter) { var jsonGroups = groups.stream().map(group -> { final var groupInviteLink = group.getGroupInviteLinkUrl(); diff --git a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java index ed2942a5..d7646ebb 100644 --- a/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListIdentitiesCommand.java @@ -59,8 +59,7 @@ public class ListIdentitiesCommand implements JsonRpcLocalCommand { identities = m.getIdentities(CommandUtil.getSingleRecipientIdentifier(number, m.getSelfNumber())); } - if (outputWriter instanceof PlainTextWriter) { - final var writer = (PlainTextWriter) outputWriter; + if (outputWriter instanceof PlainTextWriter writer) { for (var id : identities) { printIdentityFingerprint(writer, m, id); } diff --git a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java index 1d6611b5..8c3ac3bd 100644 --- a/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/QuitGroupCommand.java @@ -79,8 +79,7 @@ public class QuitGroupCommand implements JsonRpcLocalCommand { } private void outputResult(final OutputWriter outputWriter, final long timestamp) { - if (outputWriter instanceof PlainTextWriter) { - final var writer = (PlainTextWriter) outputWriter; + if (outputWriter instanceof PlainTextWriter writer) { writer.println("{}", timestamp); } else { final var writer = (JsonWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 6b2e497e..e1a9a2f3 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -58,8 +58,7 @@ public class ReceiveCommand implements ExtendedDbusCommand, LocalCommand { final Namespace ns, final Signal signal, DBusConnection dbusconnection, final OutputWriter outputWriter ) throws CommandException { try { - if (outputWriter instanceof JsonWriter) { - final var jsonWriter = (JsonWriter) outputWriter; + if (outputWriter instanceof JsonWriter jsonWriter) { dbusconnection.addSigHandler(Signal.MessageReceived.class, signal, messageReceived -> { var envelope = new JsonMessageEnvelope(messageReceived); diff --git a/src/main/java/org/asamk/signal/commands/RegisterCommand.java b/src/main/java/org/asamk/signal/commands/RegisterCommand.java index af6c06ad..7e4fe505 100644 --- a/src/main/java/org/asamk/signal/commands/RegisterCommand.java +++ b/src/main/java/org/asamk/signal/commands/RegisterCommand.java @@ -40,10 +40,11 @@ public class RegisterCommand implements RegistrationCommand { } catch (CaptchaRequiredException e) { String message; if (captcha == null) { - message = "Captcha required for verification, use --captcha CAPTCHA\n" - + "To get the token, go to https://signalcaptchas.org/registration/generate.html\n" - + "Check the developer tools (F12) console for a failed redirect to signalcaptcha://\n" - + "Everything after signalcaptcha:// is the captcha token."; + message = """ + Captcha required for verification, use --captcha CAPTCHA + To get the token, go to https://signalcaptchas.org/registration/generate.html + Check the developer tools (F12) console for a failed redirect to signalcaptcha:// + Everything after signalcaptcha:// is the captcha token."""; } else { message = "Invalid captcha given."; } diff --git a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java index c9eab95c..1204963c 100644 --- a/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java +++ b/src/main/java/org/asamk/signal/commands/RemoteDeleteCommand.java @@ -67,8 +67,7 @@ public class RemoteDeleteCommand implements JsonRpcLocalCommand { } private void outputResult(final OutputWriter outputWriter, final long timestamp) { - if (outputWriter instanceof PlainTextWriter) { - final var writer = (PlainTextWriter) outputWriter; + if (outputWriter instanceof PlainTextWriter writer) { writer.println("{}", timestamp); } else { final var writer = (JsonWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/SendCommand.java b/src/main/java/org/asamk/signal/commands/SendCommand.java index dba7689f..ab8bce16 100644 --- a/src/main/java/org/asamk/signal/commands/SendCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendCommand.java @@ -113,8 +113,7 @@ public class SendCommand implements JsonRpcLocalCommand { } private void outputResult(final OutputWriter outputWriter, final long timestamp) { - if (outputWriter instanceof PlainTextWriter) { - final var writer = (PlainTextWriter) outputWriter; + if (outputWriter instanceof PlainTextWriter writer) { writer.println("{}", timestamp); } else { final var writer = (JsonWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java index 857f603d..62d040a3 100644 --- a/src/main/java/org/asamk/signal/commands/SendReactionCommand.java +++ b/src/main/java/org/asamk/signal/commands/SendReactionCommand.java @@ -83,8 +83,7 @@ public class SendReactionCommand implements JsonRpcLocalCommand { } private void outputResult(final OutputWriter outputWriter, final long timestamp) { - if (outputWriter instanceof PlainTextWriter) { - final var writer = (PlainTextWriter) outputWriter; + if (outputWriter instanceof PlainTextWriter writer) { writer.println("{}", timestamp); } else { final var writer = (JsonWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java index b63a7160..c51df8e0 100644 --- a/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java +++ b/src/main/java/org/asamk/signal/commands/UpdateGroupCommand.java @@ -77,33 +77,23 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand { if (value == null) { return null; } - switch (value) { - case "enabled": - return GroupLinkState.ENABLED; - case "enabled-with-approval": - case "enabledWithApproval": - return GroupLinkState.ENABLED_WITH_APPROVAL; - case "disabled": - return GroupLinkState.DISABLED; - default: - throw new UserErrorException("Invalid group link state: " + value); - } + return switch (value) { + case "enabled" -> GroupLinkState.ENABLED; + case "enabled-with-approval", "enabledWithApproval" -> GroupLinkState.ENABLED_WITH_APPROVAL; + case "disabled" -> GroupLinkState.DISABLED; + default -> throw new UserErrorException("Invalid group link state: " + value); + }; } GroupPermission getGroupPermission(String value) throws UserErrorException { if (value == null) { return null; } - switch (value) { - case "every-member": - case "everyMember": - return GroupPermission.EVERY_MEMBER; - case "only-admins": - case "onlyAdmins": - return GroupPermission.ONLY_ADMINS; - default: - throw new UserErrorException("Invalid group permission: " + value); - } + return switch (value) { + case "every-member", "everyMember" -> GroupPermission.EVERY_MEMBER; + case "only-admins", "onlyAdmins" -> GroupPermission.ONLY_ADMINS; + default -> throw new UserErrorException("Invalid group permission: " + value); + }; } @Override @@ -179,8 +169,7 @@ public class UpdateGroupCommand implements JsonRpcLocalCommand { } private void outputResult(final OutputWriter outputWriter, final Long timestamp, final GroupId groupId) { - if (outputWriter instanceof PlainTextWriter) { - final var writer = (PlainTextWriter) outputWriter; + if (outputWriter instanceof PlainTextWriter writer) { if (groupId != null) { writer.println("Created new group: \"{}\"", groupId.toBase64()); } diff --git a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java index 53b64b8c..23d29365 100644 --- a/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java +++ b/src/main/java/org/asamk/signal/commands/UploadStickerPackCommand.java @@ -42,8 +42,7 @@ public class UploadStickerPackCommand implements JsonRpcLocalCommand { try { var url = m.uploadStickerPack(path); - if (outputWriter instanceof PlainTextWriter) { - final var writer = (PlainTextWriter) outputWriter; + if (outputWriter instanceof PlainTextWriter writer) { writer.println("{}", url); } else { final var writer = (JsonWriter) outputWriter; diff --git a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java index fcbadd38..ea591224 100644 --- a/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java +++ b/src/main/java/org/asamk/signal/dbus/DbusManagerImpl.java @@ -274,15 +274,9 @@ public class DbusManagerImpl implements Manager { } if (updateGroup.getGroupLinkState() != null) { switch (updateGroup.getGroupLinkState()) { - case DISABLED: - group.disableLink(); - break; - case ENABLED: - group.enableLink(false); - break; - case ENABLED_WITH_APPROVAL: - group.enableLink(true); - break; + case DISABLED -> group.disableLink(); + case ENABLED -> group.enableLink(false); + case ENABLED_WITH_APPROVAL -> group.enableLink(true); } } return new SendGroupMessageResults(0, List.of()); diff --git a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java index e49e6125..0d44de5d 100644 --- a/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java +++ b/src/main/java/org/asamk/signal/json/JsonMessageEnvelope.java @@ -71,8 +71,7 @@ public class JsonMessageEnvelope { this.sourceNumber = source.getNumber().orNull(); this.sourceUuid = source.getUuid().toString(); this.sourceDevice = content.getSenderDevice(); - } else if (exception instanceof UntrustedIdentityException) { - var e = (UntrustedIdentityException) exception; + } else if (exception instanceof UntrustedIdentityException e) { final var source = m.resolveSignalServiceAddress(e.getSender()); this.source = getLegacyIdentifier(source); this.sourceNumber = source.getNumber().orNull(); diff --git a/src/main/java/org/asamk/signal/jsonrpc/JsonRpcBulkMessage.java b/src/main/java/org/asamk/signal/jsonrpc/JsonRpcBulkMessage.java index d1b63212..1ffdb016 100644 --- a/src/main/java/org/asamk/signal/jsonrpc/JsonRpcBulkMessage.java +++ b/src/main/java/org/asamk/signal/jsonrpc/JsonRpcBulkMessage.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import java.util.List; -public class JsonRpcBulkMessage extends JsonRpcMessage { +public final class JsonRpcBulkMessage extends JsonRpcMessage { List messages; diff --git a/src/main/java/org/asamk/signal/jsonrpc/JsonRpcMessage.java b/src/main/java/org/asamk/signal/jsonrpc/JsonRpcMessage.java index 7f8b0a1a..9cc7c87d 100644 --- a/src/main/java/org/asamk/signal/jsonrpc/JsonRpcMessage.java +++ b/src/main/java/org/asamk/signal/jsonrpc/JsonRpcMessage.java @@ -4,6 +4,6 @@ package org.asamk.signal.jsonrpc; * Represents a JSON-RPC (bulk) request or (bulk) response. * https://www.jsonrpc.org/specification */ -public abstract class JsonRpcMessage { +public sealed abstract class JsonRpcMessage permits JsonRpcBulkMessage, JsonRpcRequest, JsonRpcResponse { } diff --git a/src/main/java/org/asamk/signal/jsonrpc/JsonRpcRequest.java b/src/main/java/org/asamk/signal/jsonrpc/JsonRpcRequest.java index 1ae8552a..ac54c7b7 100644 --- a/src/main/java/org/asamk/signal/jsonrpc/JsonRpcRequest.java +++ b/src/main/java/org/asamk/signal/jsonrpc/JsonRpcRequest.java @@ -8,7 +8,7 @@ import com.fasterxml.jackson.databind.node.ValueNode; * Represents a JSON-RPC request. * https://www.jsonrpc.org/specification#request_object */ -public class JsonRpcRequest extends JsonRpcMessage { +public final class JsonRpcRequest extends JsonRpcMessage { /** * A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". diff --git a/src/main/java/org/asamk/signal/jsonrpc/JsonRpcResponse.java b/src/main/java/org/asamk/signal/jsonrpc/JsonRpcResponse.java index b5279b7d..406212d8 100644 --- a/src/main/java/org/asamk/signal/jsonrpc/JsonRpcResponse.java +++ b/src/main/java/org/asamk/signal/jsonrpc/JsonRpcResponse.java @@ -8,7 +8,7 @@ import com.fasterxml.jackson.databind.node.ValueNode; * Represents a JSON-RPC response. * https://www.jsonrpc.org/specification#response_object */ -public class JsonRpcResponse extends JsonRpcMessage { +public final class JsonRpcResponse extends JsonRpcMessage { /** * A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0". diff --git a/src/main/java/org/asamk/signal/util/ErrorUtils.java b/src/main/java/org/asamk/signal/util/ErrorUtils.java index e40ed218..1c8a8f38 100644 --- a/src/main/java/org/asamk/signal/util/ErrorUtils.java +++ b/src/main/java/org/asamk/signal/util/ErrorUtils.java @@ -64,12 +64,12 @@ public class ErrorUtils { "CAPTCHA proof required for sending to \"%s\", available options \"%s\" with challenge token \"%s\", or wait \"%d\" seconds.\n" + ( failure.getOptions().contains(ProofRequiredException.Option.RECAPTCHA) - ? - "To get the captcha token, go to https://signalcaptchas.org/challenge/generate.html\n" - + "Check the developer tools (F12) console for a failed redirect to signalcaptcha://\n" - + "Everything after signalcaptcha:// is the captcha token.\n" - + "Use the following command to submit the captcha token:\n" - + "signal-cli submitRateLimitChallenge --challenge CHALLENGE_TOKEN --captcha CAPTCHA_TOKEN" + ? """ + To get the captcha token, go to https://signalcaptchas.org/challenge/generate.html + Check the developer tools (F12) console for a failed redirect to signalcaptcha:// + Everything after signalcaptcha:// is the captcha token. + Use the following command to submit the captcha token: + signal-cli submitRateLimitChallenge --challenge CHALLENGE_TOKEN --captcha CAPTCHA_TOKEN""" : "" ), identifier, From ce7aa580b6f0580cdcf7fd68fcc8efba737d21ed Mon Sep 17 00:00:00 2001 From: AsamK Date: Sun, 24 Oct 2021 22:26:12 +0200 Subject: [PATCH 0891/2005] Use record classes --- .idea/codeStyles/Project.xml | 7 +- lib/build.gradle.kts | 1 + .../asamk/signal/manager/DeviceLinkInfo.java | 10 +- .../asamk/signal/manager/JsonStickerPack.java | 51 +--- .../org/asamk/signal/manager/Manager.java | 6 +- .../org/asamk/signal/manager/ManagerImpl.java | 12 +- .../org/asamk/signal/manager/PathConfig.java | 34 +-- .../signal/manager/ProvisioningManager.java | 8 +- .../signal/manager/RegistrationManager.java | 6 +- .../org/asamk/signal/manager/api/Device.java | 37 +-- .../org/asamk/signal/manager/api/Group.java | 130 ++--------- .../asamk/signal/manager/api/Identity.java | 60 +---- .../org/asamk/signal/manager/api/Message.java | 19 +- .../manager/api/SendGroupMessageResults.java | 21 +- .../manager/api/SendMessageResults.java | 21 +- .../configuration/ConfigurationStore.java | 26 +-- .../manager/storage/groups/GroupStore.java | 129 ++--------- .../storage/identities/IdentityKeyStore.java | 37 +-- .../storage/messageCache/MessageCache.java | 2 +- .../storage/recipients/RecipientId.java | 32 +-- .../storage/recipients/RecipientStore.java | 131 ++--------- .../senderKeys/SenderKeyRecordStore.java | 51 +--- .../senderKeys/SenderKeySharedStore.java | 80 +------ .../storage/sessions/SessionStore.java | 49 +--- .../storage/stickers/StickerStore.java | 27 +-- .../signal/manager/util/StickerUtils.java | 39 ++-- .../signal/JsonReceiveMessageHandler.java | 4 +- .../asamk/signal/ReceiveMessageHandler.java | 2 +- .../signal/commands/GetUserStatusCommand.java | 18 +- .../signal/commands/JoinGroupCommand.java | 2 +- .../signal/commands/JsonRpcLocalCommand.java | 3 +- .../signal/commands/ListContactsCommand.java | 23 +- .../signal/commands/ListDevicesCommand.java | 27 +-- .../signal/commands/ListGroupsCommand.java | 124 ++++------ .../commands/ListIdentitiesCommand.java | 55 ++--- .../signal/commands/QuitGroupCommand.java | 4 +- .../asamk/signal/commands/ReceiveCommand.java | 6 +- .../signal/commands/RemoteDeleteCommand.java | 4 +- .../asamk/signal/commands/SendCommand.java | 4 +- .../signal/commands/SendReactionCommand.java | 4 +- .../signal/commands/UpdateGroupCommand.java | 8 +- .../asamk/signal/dbus/DbusManagerImpl.java | 6 +- .../org/asamk/signal/dbus/DbusSignalImpl.java | 90 ++++---- .../org/asamk/signal/json/JsonAttachment.java | 40 +--- .../asamk/signal/json/JsonCallMessage.java | 41 ++-- .../asamk/signal/json/JsonContactAddress.java | 61 ++--- .../asamk/signal/json/JsonContactAvatar.java | 15 +- .../asamk/signal/json/JsonContactEmail.java | 19 +- .../asamk/signal/json/JsonContactName.java | 38 +-- .../asamk/signal/json/JsonContactPhone.java | 19 +- .../asamk/signal/json/JsonDataMessage.java | 187 +++++++-------- .../java/org/asamk/signal/json/JsonError.java | 15 +- .../org/asamk/signal/json/JsonGroupInfo.java | 61 ++--- .../org/asamk/signal/json/JsonMention.java | 32 +-- .../signal/json/JsonMessageEnvelope.java | 217 +++++++++--------- .../java/org/asamk/signal/json/JsonQuote.java | 63 ++--- .../signal/json/JsonQuotedAttachment.java | 25 +- .../org/asamk/signal/json/JsonReaction.java | 50 ++-- .../asamk/signal/json/JsonReceiptMessage.java | 36 +-- .../asamk/signal/json/JsonRemoteDelete.java | 11 +- .../asamk/signal/json/JsonSharedContact.java | 73 +++--- .../org/asamk/signal/json/JsonSticker.java | 22 +- .../signal/json/JsonSyncDataMessage.java | 45 ++-- .../asamk/signal/json/JsonSyncMessage.java | 91 ++++---- .../signal/json/JsonSyncReadMessage.java | 32 +-- .../asamk/signal/json/JsonTypingMessage.java | 28 ++- 66 files changed, 754 insertions(+), 1877 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index e2bacc48..9cbd20b7 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -26,14 +26,14 @@ +