CryptoHelper.java

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