1package eu.siacs.conversations.utils;
2
3import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
4
5import android.os.Bundle;
6import android.util.Base64;
7import android.util.Pair;
8import androidx.annotation.StringRes;
9
10import org.bouncycastle.asn1.x500.X500Name;
11import org.bouncycastle.asn1.x500.style.BCStyle;
12import org.bouncycastle.asn1.x500.style.IETFUtils;
13import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
14
15import java.io.InputStream;
16import java.io.IOException;
17import eu.siacs.conversations.Config;
18import eu.siacs.conversations.R;
19import eu.siacs.conversations.entities.Account;
20import eu.siacs.conversations.entities.Message;
21import eu.siacs.conversations.xmpp.Jid;
22import java.nio.charset.StandardCharsets;
23import java.security.MessageDigest;
24import java.security.NoSuchAlgorithmException;
25import java.security.SecureRandom;
26import java.security.cert.CertificateEncodingException;
27import java.security.cert.CertificateParsingException;
28import java.security.cert.X509Certificate;
29import java.text.Normalizer;
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.Collection;
33import java.util.Iterator;
34import java.util.LinkedHashSet;
35import java.util.List;
36import java.util.regex.Pattern;
37
38import io.ipfs.cid.Cid;
39import io.ipfs.multihash.Multihash;
40
41import eu.siacs.conversations.Config;
42import eu.siacs.conversations.R;
43import eu.siacs.conversations.entities.Account;
44import eu.siacs.conversations.entities.Message;
45import eu.siacs.conversations.xmpp.Jid;
46
47public final class CryptoHelper {
48
49 public static final Pattern UUID_PATTERN =
50 Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}");
51 public static final byte[] ONE = new byte[] {0, 0, 0, 1};
52 private static final char[] CHARS =
53 "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789+-/#$!?".toCharArray();
54 private static final int PW_LENGTH = 25;
55 private static final char[] VOWELS = "aeiou".toCharArray();
56 private static final char[] CONSONANTS = "bcfghjklmnpqrstvwxyz".toCharArray();
57 private static final char[] hexArray = "0123456789abcdef".toCharArray();
58
59 public static String bytesToHex(byte[] bytes) {
60 char[] hexChars = new char[bytes.length * 2];
61 for (int j = 0; j < bytes.length; j++) {
62 int v = bytes[j] & 0xFF;
63 hexChars[j * 2] = hexArray[v >>> 4];
64 hexChars[j * 2 + 1] = hexArray[v & 0x0F];
65 }
66 return new String(hexChars);
67 }
68
69 public static String createPassword(SecureRandom random) {
70 StringBuilder builder = new StringBuilder(PW_LENGTH);
71 for (int i = 0; i < PW_LENGTH; ++i) {
72 builder.append(CHARS[random.nextInt(CHARS.length - 1)]);
73 }
74 return builder.toString();
75 }
76
77 public static String pronounceable() {
78 final int rand = SECURE_RANDOM.nextInt(4);
79 char[] output = new char[rand * 2 + (5 - rand)];
80 boolean vowel = SECURE_RANDOM.nextBoolean();
81 for (int i = 0; i < output.length; ++i) {
82 output[i] =
83 vowel
84 ? VOWELS[SECURE_RANDOM.nextInt(VOWELS.length)]
85 : CONSONANTS[SECURE_RANDOM.nextInt(CONSONANTS.length)];
86 vowel = !vowel;
87 }
88 return String.valueOf(output);
89 }
90
91 public static byte[] hexToBytes(String hexString) {
92 int len = hexString.length();
93 byte[] array = new byte[len / 2];
94 for (int i = 0; i < len; i += 2) {
95 array[i / 2] =
96 (byte)
97 ((Character.digit(hexString.charAt(i), 16) << 4)
98 + Character.digit(hexString.charAt(i + 1), 16));
99 }
100 return array;
101 }
102
103 public static String hexToString(final String hexString) {
104 return new String(hexToBytes(hexString));
105 }
106
107 public static byte[] concatenateByteArrays(byte[] a, byte[] b) {
108 byte[] result = new byte[a.length + b.length];
109 System.arraycopy(a, 0, result, 0, a.length);
110 System.arraycopy(b, 0, result, a.length, b.length);
111 return result;
112 }
113
114 /** Escapes usernames or passwords for SASL. */
115 public static String saslEscape(final String s) {
116 final StringBuilder sb = new StringBuilder((int) (s.length() * 1.1));
117 for (int i = 0; i < s.length(); i++) {
118 char c = s.charAt(i);
119 switch (c) {
120 case ',':
121 sb.append("=2C");
122 break;
123 case '=':
124 sb.append("=3D");
125 break;
126 default:
127 sb.append(c);
128 break;
129 }
130 }
131 return sb.toString();
132 }
133
134 public static String saslPrep(final String s) {
135 return Normalizer.normalize(s, Normalizer.Form.NFKC);
136 }
137
138 public static String random(final int length) {
139 final byte[] bytes = new byte[length];
140 SECURE_RANDOM.nextBytes(bytes);
141 return Base64.encodeToString(bytes, Base64.NO_PADDING | Base64.NO_WRAP | Base64.URL_SAFE);
142 }
143
144 public static String prettifyFingerprint(String fingerprint) {
145 if (fingerprint == null) {
146 return "";
147 } else if (fingerprint.length() < 40) {
148 return fingerprint;
149 }
150 StringBuilder builder = new StringBuilder(fingerprint);
151 for (int i = 8; i < builder.length(); i += 9) {
152 builder.insert(i, ' ');
153 }
154 return builder.toString();
155 }
156
157 public static String prettifyFingerprintCert(String fingerprint) {
158 StringBuilder builder = new StringBuilder(fingerprint);
159 for (int i = 2; i < builder.length(); i += 3) {
160 builder.insert(i, ':');
161 }
162 return builder.toString();
163 }
164
165 public static String[] getOrderedCipherSuites(final String[] platformSupportedCipherSuites) {
166 final Collection<String> cipherSuites =
167 new LinkedHashSet<>(Arrays.asList(Config.ENABLED_CIPHERS));
168 final List<String> platformCiphers = Arrays.asList(platformSupportedCipherSuites);
169 cipherSuites.retainAll(platformCiphers);
170 cipherSuites.addAll(platformCiphers);
171 filterWeakCipherSuites(cipherSuites);
172 cipherSuites.remove("TLS_FALLBACK_SCSV");
173 return cipherSuites.toArray(new String[cipherSuites.size()]);
174 }
175
176 private static void filterWeakCipherSuites(final Collection<String> cipherSuites) {
177 final Iterator<String> it = cipherSuites.iterator();
178 while (it.hasNext()) {
179 String cipherName = it.next();
180 // remove all ciphers with no or very weak encryption or no authentication
181 for (String weakCipherPattern : Config.WEAK_CIPHER_PATTERNS) {
182 if (cipherName.contains(weakCipherPattern)) {
183 it.remove();
184 break;
185 }
186 }
187 }
188 }
189
190 public static Pair<Jid, String> extractJidAndName(X509Certificate certificate)
191 throws CertificateEncodingException,
192 IllegalArgumentException,
193 CertificateParsingException {
194 Collection<List<?>> alternativeNames = certificate.getSubjectAlternativeNames();
195 List<String> emails = new ArrayList<>();
196 if (alternativeNames != null) {
197 for (List<?> san : alternativeNames) {
198 Integer type = (Integer) san.get(0);
199 if (type == 1) {
200 emails.add((String) san.get(1));
201 }
202 }
203 }
204 X500Name x500name = new JcaX509CertificateHolder(certificate).getSubject();
205 if (emails.size() == 0 && x500name.getRDNs(BCStyle.EmailAddress).length > 0) {
206 emails.add(
207 IETFUtils.valueToString(
208 x500name.getRDNs(BCStyle.EmailAddress)[0].getFirst().getValue()));
209 }
210 String name =
211 x500name.getRDNs(BCStyle.CN).length > 0
212 ? IETFUtils.valueToString(
213 x500name.getRDNs(BCStyle.CN)[0].getFirst().getValue())
214 : null;
215 if (emails.size() >= 1) {
216 return new Pair<>(Jid.of(emails.get(0)), name);
217 } else if (name != null) {
218 try {
219 Jid jid = Jid.of(name);
220 if (jid.isBareJid() && jid.getLocal() != null) {
221 return new Pair<>(jid, null);
222 }
223 } catch (IllegalArgumentException e) {
224 return null;
225 }
226 }
227 return null;
228 }
229
230 public static Bundle extractCertificateInformation(X509Certificate certificate) {
231 Bundle information = new Bundle();
232 try {
233 JcaX509CertificateHolder holder = new JcaX509CertificateHolder(certificate);
234 X500Name subject = holder.getSubject();
235 try {
236 information.putString(
237 "subject_cn",
238 subject.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString());
239 } catch (Exception e) {
240 // ignored
241 }
242 try {
243 information.putString(
244 "subject_o",
245 subject.getRDNs(BCStyle.O)[0].getFirst().getValue().toString());
246 } catch (Exception e) {
247 // ignored
248 }
249
250 X500Name issuer = holder.getIssuer();
251 try {
252 information.putString(
253 "issuer_cn",
254 issuer.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString());
255 } catch (Exception e) {
256 // ignored
257 }
258 try {
259 information.putString(
260 "issuer_o", issuer.getRDNs(BCStyle.O)[0].getFirst().getValue().toString());
261 } catch (Exception e) {
262 // ignored
263 }
264 try {
265 information.putString("sha1", getFingerprintCert(certificate.getEncoded()));
266 } catch (Exception e) {
267
268 }
269 return information;
270 } catch (CertificateEncodingException e) {
271 return information;
272 }
273 }
274
275 public static String getFingerprintCert(byte[] input) throws NoSuchAlgorithmException {
276 MessageDigest md = MessageDigest.getInstance("SHA-1");
277 byte[] fingerprint = md.digest(input);
278 return prettifyFingerprintCert(bytesToHex(fingerprint));
279 }
280
281 public static String getFingerprint(Jid jid, String androidId) {
282 return getFingerprint(jid.toString() + "\00" + androidId);
283 }
284
285 public static String getAccountFingerprint(Account account, String androidId) {
286 return getFingerprint(account.getJid().asBareJid(), androidId);
287 }
288
289 public static String getFingerprint(String value) {
290 try {
291 MessageDigest md = MessageDigest.getInstance("SHA-1");
292 return bytesToHex(md.digest(value.getBytes(StandardCharsets.UTF_8)));
293 } catch (Exception e) {
294 return "";
295 }
296 }
297
298 public static @StringRes int encryptionTypeToText(final int encryption) {
299 return switch (encryption) {
300 case Message.ENCRYPTION_OTR -> R.string.encryption_choice_otr;
301 case Message.ENCRYPTION_AXOLOTL,
302 Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE,
303 Message.ENCRYPTION_AXOLOTL_FAILED ->
304 R.string.encryption_choice_omemo;
305 case Message.ENCRYPTION_PGP -> R.string.encryption_choice_pgp;
306 default -> R.string.encryption_choice_unencrypted;
307 };
308 }
309
310 public static boolean isPgpEncryptedUrl(String url) {
311 if (url == null) {
312 return false;
313 }
314 final String u = url.toLowerCase();
315 return !u.contains(" ")
316 && (u.startsWith("https://") || u.startsWith("http://") || u.startsWith("p1s3://"))
317 && u.endsWith(".pgp");
318 }
319
320 public static String multihashAlgo(Multihash.Type type) throws NoSuchAlgorithmException {
321 switch(type) {
322 case sha1:
323 return "sha-1";
324 case sha2_256:
325 return "sha-256";
326 case sha2_512:
327 return "sha-512";
328 default:
329 throw new NoSuchAlgorithmException("" + type);
330 }
331 }
332
333 public static Multihash.Type multihashType(String algo) throws NoSuchAlgorithmException {
334 if (algo.equals("SHA-1") || algo.equals("sha-1") || algo.equals("sha1")) {
335 return Multihash.Type.sha1;
336 } else if (algo.equals("SHA-256") || algo.equals("sha-256")) {
337 return Multihash.Type.sha2_256;
338 } else if (algo.equals("SHA-512") | algo.equals("sha-512")) {
339 return Multihash.Type.sha2_512;
340 } else {
341 throw new NoSuchAlgorithmException(algo);
342 }
343 }
344
345 public static Cid cid(byte[] digest, String algo) throws NoSuchAlgorithmException {
346 return Cid.buildCidV1(Cid.Codec.Raw, multihashType(algo), digest);
347 }
348
349 public static Cid[] cid(InputStream in, String[] algo) throws NoSuchAlgorithmException, IOException {
350 byte[] buf = new byte[4096];
351 int len;
352 MessageDigest[] md = new MessageDigest[algo.length];
353 for (int i = 0; i < md.length; i++) {
354 md[i] = MessageDigest.getInstance(algo[i]);
355 }
356 while ((len = in.read(buf)) != -1) {
357 for (int i = 0; i < md.length; i++) {
358 md[i].update(buf, 0, len);
359 }
360 }
361
362 Cid[] cid = new Cid[md.length];
363 for (int i = 0; i < cid.length; i++) {
364 cid[i] = cid(md[i].digest(), algo[i]);
365 }
366
367 return cid;
368 }
369}