Dlaczego zadaję to pytanie:
Wiem, że było wiele pytań dotyczących szyfrowania AES, nawet w przypadku Androida. A jeśli przeszukujesz Internet, jest wiele fragmentów kodu. Ale na każdej stronie, w każdym pytaniu o przepełnienie stosu, znajduję inną implementację z dużymi różnicami.
Stworzyłem więc to pytanie, aby znaleźć „najlepszą praktykę”. Mam nadzieję, że uda nam się zebrać listę najważniejszych wymagań i skonfigurować implementację, która jest naprawdę bezpieczna!
Czytałem o wektorach inicjalizacyjnych i solach. Nie wszystkie implementacje, które znalazłem, miały te funkcje. Więc potrzebujesz tego? Czy to znacznie zwiększa bezpieczeństwo? Jak to realizujesz? Czy algorytm powinien zgłaszać wyjątki, jeśli zaszyfrowanych danych nie można odszyfrować? A może jest to niezabezpieczone i powinno po prostu zwrócić nieczytelny ciąg? Czy algorytm może używać Bcrypt zamiast SHA?
A co z tymi dwoma implementacjami, które znalazłem? Czy wszystko w porządku? Idealne lub brakuje kilku ważnych rzeczy? Co z tego jest bezpieczne?
Algorytm powinien wziąć ciąg znaków i „hasło” do zaszyfrowania, a następnie zaszyfrować ciąg za pomocą tego hasła. Wynik powinien być ponownie łańcuchem (hex czy base64?). Oczywiście deszyfrowanie powinno być również możliwe.
Jaka jest idealna implementacja AES dla Androida?
Wdrożenie nr 1:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AdvancedCrypto implements ICrypto {
public static final String PROVIDER = "BC";
public static final int SALT_LENGTH = 20;
public static final int IV_LENGTH = 16;
public static final int PBE_ITERATION_COUNT = 100;
private static final String RANDOM_ALGORITHM = "SHA1PRNG";
private static final String HASH_ALGORITHM = "SHA-512";
private static final String PBE_ALGORITHM = "PBEWithSHA256And256BitAES-CBC-BC";
private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String SECRET_KEY_ALGORITHM = "AES";
public String encrypt(SecretKey secret, String cleartext) throws CryptoException {
try {
byte[] iv = generateIv();
String ivHex = HexEncoder.toHex(iv);
IvParameterSpec ivspec = new IvParameterSpec(iv);
Cipher encryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
encryptionCipher.init(Cipher.ENCRYPT_MODE, secret, ivspec);
byte[] encryptedText = encryptionCipher.doFinal(cleartext.getBytes("UTF-8"));
String encryptedHex = HexEncoder.toHex(encryptedText);
return ivHex + encryptedHex;
} catch (Exception e) {
throw new CryptoException("Unable to encrypt", e);
}
}
public String decrypt(SecretKey secret, String encrypted) throws CryptoException {
try {
Cipher decryptionCipher = Cipher.getInstance(CIPHER_ALGORITHM, PROVIDER);
String ivHex = encrypted.substring(0, IV_LENGTH * 2);
String encryptedHex = encrypted.substring(IV_LENGTH * 2);
IvParameterSpec ivspec = new IvParameterSpec(HexEncoder.toByte(ivHex));
decryptionCipher.init(Cipher.DECRYPT_MODE, secret, ivspec);
byte[] decryptedText = decryptionCipher.doFinal(HexEncoder.toByte(encryptedHex));
String decrypted = new String(decryptedText, "UTF-8");
return decrypted;
} catch (Exception e) {
throw new CryptoException("Unable to decrypt", e);
}
}
public SecretKey getSecretKey(String password, String salt) throws CryptoException {
try {
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), HexEncoder.toByte(salt), PBE_ITERATION_COUNT, 256);
SecretKeyFactory factory = SecretKeyFactory.getInstance(PBE_ALGORITHM, PROVIDER);
SecretKey tmp = factory.generateSecret(pbeKeySpec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), SECRET_KEY_ALGORITHM);
return secret;
} catch (Exception e) {
throw new CryptoException("Unable to get secret key", e);
}
}
public String getHash(String password, String salt) throws CryptoException {
try {
String input = password + salt;
MessageDigest md = MessageDigest.getInstance(HASH_ALGORITHM, PROVIDER);
byte[] out = md.digest(input.getBytes("UTF-8"));
return HexEncoder.toHex(out);
} catch (Exception e) {
throw new CryptoException("Unable to get hash", e);
}
}
public String generateSalt() throws CryptoException {
try {
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
byte[] salt = new byte[SALT_LENGTH];
random.nextBytes(salt);
String saltHex = HexEncoder.toHex(salt);
return saltHex;
} catch (Exception e) {
throw new CryptoException("Unable to generate salt", e);
}
}
private byte[] generateIv() throws NoSuchAlgorithmException, NoSuchProviderException {
SecureRandom random = SecureRandom.getInstance(RANDOM_ALGORITHM);
byte[] iv = new byte[IV_LENGTH];
random.nextBytes(iv);
return iv;
}
}
Wdrożenie nr 2:
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* Usage:
* <pre>
* String crypto = SimpleCrypto.encrypt(masterpassword, cleartext)
* ...
* String cleartext = SimpleCrypto.decrypt(masterpassword, crypto)
* </pre>
* @author ferenc.hechler
*/
public class SimpleCrypto {
public static String encrypt(String seed, String cleartext) throws Exception {
byte[] rawKey = getRawKey(seed.getBytes());
byte[] result = encrypt(rawKey, cleartext.getBytes());
return toHex(result);
}
public static String decrypt(String seed, String encrypted) throws Exception {
byte[] rawKey = getRawKey(seed.getBytes());
byte[] enc = toByte(encrypted);
byte[] result = decrypt(rawKey, enc);
return new String(result);
}
private static byte[] getRawKey(byte[] seed) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
sr.setSeed(seed);
kgen.init(128, sr); // 192 and 256 bits may not be available
SecretKey skey = kgen.generateKey();
byte[] raw = skey.getEncoded();
return raw;
}
private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
byte[] encrypted = cipher.doFinal(clear);
return encrypted;
}
private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decrypted = cipher.doFinal(encrypted);
return decrypted;
}
public static String toHex(String txt) {
return toHex(txt.getBytes());
}
public static String fromHex(String hex) {
return new String(toByte(hex));
}
public static byte[] toByte(String hexString) {
int len = hexString.length()/2;
byte[] result = new byte[len];
for (int i = 0; i < len; i++)
result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();
return result;
}
public static String toHex(byte[] buf) {
if (buf == null)
return "";
StringBuffer result = new StringBuffer(2*buf.length);
for (int i = 0; i < buf.length; i++) {
appendHex(result, buf[i]);
}
return result.toString();
}
private final static String HEX = "0123456789ABCDEF";
private static void appendHex(StringBuffer sb, byte b) {
sb.append(HEX.charAt((b>>4)&0x0f)).append(HEX.charAt(b&0x0f));
}
}
Źródło: http://www.tutorials-android.com/learn/How_to_encrypt_and_decrypt_strings.rhtml
źródło
implements ICrypto
i zmienićthrows CryptoException
nathrows Exception
i tak dalej. Więc nie będziesz już potrzebować tych zajęć.getSecretKey
,getHash
,generateSalt
w pierwszej realizacji, które są niewykorzystane. Może się mylę, ale jak w praktyce można wykorzystać tę klasę do szyfrowania ciągu znaków?Odpowiedzi:
Żadna implementacja, którą podasz w swoim pytaniu, nie jest całkowicie poprawna i żadna z podanych przez Ciebie implementacji nie powinna być używana w takiej postaci, w jakiej jest.W dalszej części omówię aspekty szyfrowania opartego na hasłach w systemie Android.
Klucze i skróty
Zacznę omawiać system oparty na hasłach z solami. Sól to losowo generowana liczba. Nie jest to „wydedukowane”. Wdrożenie 1 obejmuje
generateSalt()
metodę, która generuje liczb losowych silnych pod względem kryptograficznym. Ponieważ sól jest ważna dla bezpieczeństwa, powinna być utrzymywana w tajemnicy po jej wytworzeniu, chociaż wystarczy ją wygenerować tylko raz. Jeśli jest to witryna internetowa, stosunkowo łatwo jest zachować tajemnicę, ale w przypadku zainstalowanych aplikacji (na komputery stacjonarne i urządzenia mobilne) będzie to znacznie trudniejsze.Metoda
getHash()
zwraca hash podanego hasła i soli, połączone w jeden ciąg. Użyty algorytm to SHA-512, który zwraca 512-bitowy skrót. Ta metoda zwraca hash, który jest przydatny do sprawdzania integralności ciągu, więc równie dobrze można go użyć, wywołującgetHash()
tylko hasło lub tylko sól, ponieważ po prostu łączy oba parametry. Ponieważ ta metoda nie będzie używana w systemie szyfrowania opartym na hasłach, nie będę go dalej omawiać.Metoda
getSecretKey()
wyprowadza klucz zchar
tablicy haseł i soli zakodowanej szesnastkowo, zwróconej zgenerateSalt()
. Użyty algorytm to PBKDF1 (chyba) z PKCS5 z SHA-256 jako funkcją skrótu i zwraca 256-bitowy klucz.getSecretKey()
generuje klucz poprzez wielokrotne generowanie skrótów hasła, soli i licznika (do liczby iteracji podanej wPBE_ITERATION_COUNT
tutaj 100) w celu zwiększenia czasu potrzebnego do przeprowadzenia ataku siłowego. Długość soli powinna wynosić co najmniej tak długo, jak długość generowanego klucza, w tym przypadku co najmniej 256 bitów. Licznik iteracji powinien być ustawiony tak długo, jak to możliwe, bez powodowania nieuzasadnionego opóźnienia. Aby uzyskać więcej informacji na temat soli i liczby iteracji w wyprowadzaniu klucza, zobacz sekcję 4 w RFC2898 .Implementacja w PBE Javy jest jednak błędna, jeśli hasło zawiera znaki Unicode, to znaczy takie, które wymagają reprezentacji więcej niż 8 bitów. Jak stwierdzono w
PBEKeySpec
, „mechanizm PBE zdefiniowany w PKCS # 5 sprawdza tylko 8 mniej znaczących bitów każdego znaku”. Aby obejść ten problem, możesz spróbować wygenerować ciąg szesnastkowy (który będzie zawierał tylko znaki 8-bitowe) wszystkich 16-bitowych znaków w haśle przed przekazaniem go doPBEKeySpec
. Na przykład „ABC” zmieni się na „004100420043”. Zauważ również, że PBEKeySpec „żąda hasła jako tablicy znaków, więc można je nadpisać [zclearPassword()
] po zakończeniu ”. (Jeśli chodzi o „ochronę ciągów w pamięci”, zobacz to pytanie ). Nie widzę jednak żadnych problemów,Szyfrowanie
Po wygenerowaniu klucza możemy go użyć do zaszyfrowania i odszyfrowania tekstu.
W implementacji 1 zastosowany algorytm szyfrujący
AES/CBC/PKCS5Padding
to AES w trybie szyfrowania Cipher Block Chaining (CBC) z dopełnieniem zdefiniowanym w PKCS # 5. (Inne tryby szyfrowania AES obejmują tryb licznika (CTR), tryb elektronicznej książki kodowej (ECB) i tryb licznika Galois (GCM). Kolejne pytanie dotyczące przepełnienia stosu zawiera odpowiedzi, które szczegółowo omawiają różne tryby szyfrowania AES i zalecane do użycia. Należy również pamiętać, że istnieje kilka ataków na szyfrowanie w trybie CBC, z których niektóre są wymienione w dokumencie RFC 7457).Należy pamiętać, że należy używać trybu szyfrowania, który sprawdza również integralność zaszyfrowanych danych (np. Uwierzytelnione szyfrowanie z powiązanymi danymi , AEAD, opisane w RFC 5116). Jednak
AES/CBC/PKCS5Padding
nie zapewnia sprawdzania integralności, więc samo to nie jest zalecane . Do celów AEAD zaleca się użycie klucza tajnego, który jest co najmniej dwa razy dłuższy niż normalny klucz szyfrowania, aby uniknąć powiązanych ataków kluczem: pierwsza połowa służy jako klucz szyfrowania, a druga połowa służy jako klucz do sprawdzenia integralności. (Oznacza to, że w tym przypadku należy wygenerować jeden sekret z hasła i soli i podzielić ten sekret na dwie części).Implementacja Java
Różne funkcje w implementacji 1 używają określonego dostawcy, mianowicie „BC”, dla swoich algorytmów. Ogólnie jednak nie zaleca się proszenia o określonych dostawców, ponieważ nie wszyscy dostawcy są dostępni we wszystkich implementacjach Java, czy to z powodu braku obsługi, aby uniknąć duplikowania kodu lub z innych powodów. Ta rada stała się szczególnie ważna od czasu udostępnienia wersji zapoznawczej Androida P na początku 2018 r., Ponieważ niektóre funkcje dostawcy „BC” zostały tam wycofane - zobacz artykuł „Zmiany kryptografii w Androidzie P” na blogu deweloperów Androida. Zobacz także Wprowadzenie do dostawców Oracle .
Dlatego
PROVIDER
nie powinno istnieć, a ciąg-BC
powinien zostać usunięty zPBE_ALGORITHM
. Implementacja 2 jest pod tym względem poprawna.Metoda wyłapywania wszystkich wyjątków jest niewłaściwa, ale raczej obsługuje tylko te wyjątki, które może. Implementacje podane w pytaniu mogą generować różne sprawdzone wyjątki. Metoda może wybrać zawijanie tylko tych zaznaczonych wyjątków za pomocą CryptoException lub określić te zaznaczone wyjątki w
throws
klauzuli. Dla wygody, zawijanie oryginalnego wyjątku za pomocą CryptoException może być tutaj odpowiednie, ponieważ istnieje potencjalnie wiele sprawdzonych wyjątków, które klasy mogą zgłaszać.SecureRandom
w systemie AndroidJak szczegółowo opisano w artykule „Some SecureRandom Thoughts” na blogu Android Developers Blog, implementacja
java.security.SecureRandom
w wersjach systemu Android przed 2013 rokiem ma wadę, która zmniejsza siłę liczb losowych, które dostarcza. Ta usterka może zostać złagodzona zgodnie z opisem w tym artykule.źródło
getInstance
ma przeciążenie, które przyjmuje tylko nazwę algorytmu. Przykład: Cipher.getInstance () Kilku dostawców, w tym Bouncy Castle, może być zarejestrowanych w implementacji Java i tego rodzaju przeciążenie przeszukuje listę dostawców pod kątem jednego z nich, który implementuje dany algorytm. Powinieneś spróbować i zobaczyć.Nr 2 nigdy nie powinien być używany, ponieważ używa tylko "AES" (co oznacza szyfrowanie tekstu w trybie EBC, duże nie-nie) do szyfrowania. Porozmawiam tylko o nr 1.
Wydaje się, że pierwsza implementacja jest zgodna z najlepszymi praktykami dotyczącymi szyfrowania. Stałe są na ogół OK, chociaż zarówno rozmiar soli, jak i liczba iteracji do wykonania PBE są krótkie. Co więcej, wydaje się, że dotyczy to AES-256, ponieważ generowanie kluczy PBE używa 256 jako wartości zakodowanej na stałe (szkoda po tych wszystkich stałych). Używa CBC i PKCS5Padding, co jest przynajmniej tym, czego można się spodziewać.
Całkowicie brakuje jakiejkolwiek ochrony uwierzytelniania / integralności, więc atakujący może zmienić zaszyfrowany tekst. Oznacza to, że ataki typu padding oracle są możliwe w modelu klient / serwer. Oznacza to również, że osoba atakująca może próbować zmienić zaszyfrowane dane. Prawdopodobnie spowoduje to gdzieś błąd, ponieważ dopełnienie lub zawartość nie są akceptowane przez aplikację, ale to nie jest sytuacja, w której chcesz się znaleźć.
Można ulepszyć obsługę wyjątków i sprawdzanie poprawności danych wejściowych, wychwytywanie wyjątków jest zawsze błędne w mojej książce. Co więcej, klasa implementuje ICrypt, którego nie znam. Wiem, że posiadanie tylko metod bez efektów ubocznych w klasie jest trochę dziwne. Normalnie zrobiłbyś te statyczne. Nie ma buforowania instancji szyfrów itp., Więc każdy wymagany obiekt jest tworzony ad-nauseum. Możesz jednak bezpiecznie usunąć ICrypto z definicji, jak się wydaje, w takim przypadku możesz również refaktoryzować kod na metody statyczne (lub przepisać go, aby był bardziej zorientowany obiektowo, według własnego uznania).
Problem polega na tym, że każdy wrapper zawsze przyjmuje założenia dotyczące przypadku użycia. Stwierdzenie, że opakowanie jest dobre lub złe, jest więc bzdurą. Dlatego zawsze staram się unikać generowania klas opakowujących. Ale przynajmniej nie wydaje się to wyraźnie błędne.
źródło
Zadałeś całkiem interesujące pytanie. Jak w przypadku wszystkich algorytmów, klucz szyfrujący jest „tajnym sosem”, ponieważ skoro jest znany opinii publicznej, wszystko inne też jest. Zastanawiasz się więc, jak uzyskać dostęp do tego dokumentu przez Google
bezpieczeństwo
Oprócz rozliczeń w aplikacji Google przedstawia również przemyślenia na temat bezpieczeństwa, które są również wnikliwe
billing_best_practices
źródło
Użyj BouncyCastle Lightweight API. Zapewnia 256 AES z PBE i solą.
Tutaj przykładowy kod, który może szyfrować / odszyfrowywać pliki.
public void encrypt(InputStream fin, OutputStream fout, String password) { try { PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(new SHA256Digest()); char[] passwordChars = password.toCharArray(); final byte[] pkcs12PasswordBytes = PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars); pGen.init(pkcs12PasswordBytes, salt.getBytes(), iterationCount); CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine()); ParametersWithIV aesCBCParams = (ParametersWithIV) pGen.generateDerivedParameters(256, 128); aesCBC.init(true, aesCBCParams); PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding()); aesCipher.init(true, aesCBCParams); // Read in the decrypted bytes and write the cleartext to out int numRead = 0; while ((numRead = fin.read(buf)) >= 0) { if (numRead == 1024) { byte[] plainTemp = new byte[aesCipher.getUpdateOutputSize(numRead)]; int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0); final byte[] plain = new byte[offset]; System.arraycopy(plainTemp, 0, plain, 0, plain.length); fout.write(plain, 0, plain.length); } else { byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)]; int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0); int last = aesCipher.doFinal(plainTemp, offset); final byte[] plain = new byte[offset + last]; System.arraycopy(plainTemp, 0, plain, 0, plain.length); fout.write(plain, 0, plain.length); } } fout.close(); fin.close(); } catch (Exception e) { e.printStackTrace(); } } public void decrypt(InputStream fin, OutputStream fout, String password) { try { PKCS12ParametersGenerator pGen = new PKCS12ParametersGenerator(new SHA256Digest()); char[] passwordChars = password.toCharArray(); final byte[] pkcs12PasswordBytes = PBEParametersGenerator.PKCS12PasswordToBytes(passwordChars); pGen.init(pkcs12PasswordBytes, salt.getBytes(), iterationCount); CBCBlockCipher aesCBC = new CBCBlockCipher(new AESEngine()); ParametersWithIV aesCBCParams = (ParametersWithIV) pGen.generateDerivedParameters(256, 128); aesCBC.init(false, aesCBCParams); PaddedBufferedBlockCipher aesCipher = new PaddedBufferedBlockCipher(aesCBC, new PKCS7Padding()); aesCipher.init(false, aesCBCParams); // Read in the decrypted bytes and write the cleartext to out int numRead = 0; while ((numRead = fin.read(buf)) >= 0) { if (numRead == 1024) { byte[] plainTemp = new byte[aesCipher.getUpdateOutputSize(numRead)]; int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0); // int last = aesCipher.doFinal(plainTemp, offset); final byte[] plain = new byte[offset]; System.arraycopy(plainTemp, 0, plain, 0, plain.length); fout.write(plain, 0, plain.length); } else { byte[] plainTemp = new byte[aesCipher.getOutputSize(numRead)]; int offset = aesCipher.processBytes(buf, 0, numRead, plainTemp, 0); int last = aesCipher.doFinal(plainTemp, offset); final byte[] plain = new byte[offset + last]; System.arraycopy(plainTemp, 0, plain, 0, plain.length); fout.write(plain, 0, plain.length); } } fout.close(); fin.close(); } catch (Exception e) { e.printStackTrace(); } }
źródło
buf
(I naprawdę nadzieję, że nie jest tostatic
pole). Wygląda również na to, że obaencrypt()
idecrypt()
nie będą poprawnie przetwarzać końcowego bloku, jeśli dane wejściowe są wielokrotnością 1024 bajtów.Znalazłem tutaj fajną implementację: http://nelenkov.blogspot.fr/2012/04/using-password-based-encryption-on.html i https://github.com/nelenkov/android-pbe To też było pomocne w moim poszukiwaniu wystarczająco dobrej implementacji AES dla Androida
źródło