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