CryptoHelper.java

  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}