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